diff --git a/src/components/blocks/Investment/InvestmentStock/InvestmentStock.client.tsx b/src/components/blocks/Investment/InvestmentStock/InvestmentStock.client.tsx index 8b480bf..21cc373 100644 --- a/src/components/blocks/Investment/InvestmentStock/InvestmentStock.client.tsx +++ b/src/components/blocks/Investment/InvestmentStock/InvestmentStock.client.tsx @@ -393,7 +393,7 @@ export default function InvestmentStockClient() { )} setShowIndicators((prev) => !prev)} > {showIndicators ? "– 보조지표 접기" : "+ 보조지표 설정"} diff --git a/src/components/blocks/PracticePage/PracticePage.client.tsx b/src/components/blocks/PracticePage/PracticePage.client.tsx index 7f82e91..b30867c 100644 --- a/src/components/blocks/PracticePage/PracticePage.client.tsx +++ b/src/components/blocks/PracticePage/PracticePage.client.tsx @@ -307,7 +307,7 @@ export default function PracticeClient() { )} setShowIndicators((prev) => !prev)} > {showIndicators ? "– 보조지표 접기" : "+ 보조지표 설정"} diff --git a/src/components/charts/InvestCandleChart.tsx b/src/components/charts/InvestCandleChart.tsx index 61ce3b9..ae49855 100644 --- a/src/components/charts/InvestCandleChart.tsx +++ b/src/components/charts/InvestCandleChart.tsx @@ -385,14 +385,32 @@ export default function InvestCandleChart({ visibleCandles > 1 ? chartWidth / (visibleCandles - 1) : chartWidth; const candleWidth = Math.min(40, candleSpacing * 0.7, 24); + // function getLinePoints( + // arr: (number | null)[], + // candleSpacing: number, + // getY: (v: number) => number + // ) { + // return arr + // .map((val, i) => + // typeof val === "number" && !isNaN(val) + // ? `${i * candleSpacing},${getY(val)}` + // : null + // ) + // .filter(Boolean) + // .join(" "); + // } function getLinePoints( arr: (number | null)[], candleSpacing: number, - getY: (v: number) => number + getY: (v: number) => number, + slicedData: Candle[] ) { return arr .map((val, i) => - typeof val === "number" && !isNaN(val) + typeof val === "number" && + !isNaN(val) && + slicedData[i] && + !isDotOnlyCandle(slicedData[i]) ? `${i * candleSpacing},${getY(val)}` : null ) @@ -400,24 +418,72 @@ export default function InvestCandleChart({ .join(" "); } - const ma5Points = getLinePoints(ma5_visible, candleSpacing, getY); - const ma20Points = getLinePoints(ma20_visible, candleSpacing, getY); - const ma60Points = getLinePoints(ma60_visible, candleSpacing, getY); - const ma120Points = getLinePoints(ma120_visible, candleSpacing, getY); + // 캔들이 정상적인 날인지 확인하는 유틸 함수 + function isDotOnlyCandle(candle: Candle) { + return ( + candle.open === candle.close && + candle.high === candle.close && + candle.low === candle.close && + candle.volume === 0 + ); + } + + // const ma5Points = getLinePoints(ma5_visible, candleSpacing, getY); + // const ma20Points = getLinePoints(ma20_visible, candleSpacing, getY); + // const ma60Points = getLinePoints(ma60_visible, candleSpacing, getY); + // const ma120Points = getLinePoints(ma120_visible, candleSpacing, getY); + // const bb_upper_points = getLinePoints( + // bb_visible.map((b) => b?.upper), + // candleSpacing, + // getY + // ); + // const bb_middle_points = getLinePoints( + // bb_visible.map((b) => b?.middle), + // candleSpacing, + // getY + // ); + // const bb_lower_points = getLinePoints( + // bb_visible.map((b) => b?.lower), + // candleSpacing, + // getY + // ); + const ma5Points = getLinePoints(ma5_visible, candleSpacing, getY, slicedData); + const ma20Points = getLinePoints( + ma20_visible, + candleSpacing, + getY, + slicedData + ); + const ma60Points = getLinePoints( + ma60_visible, + candleSpacing, + getY, + slicedData + ); + const ma120Points = getLinePoints( + ma120_visible, + candleSpacing, + getY, + slicedData + ); + const bb_upper_points = getLinePoints( bb_visible.map((b) => b?.upper), candleSpacing, - getY + getY, + slicedData ); const bb_middle_points = getLinePoints( bb_visible.map((b) => b?.middle), candleSpacing, - getY + getY, + slicedData ); const bb_lower_points = getLinePoints( bb_visible.map((b) => b?.lower), candleSpacing, - getY + getY, + slicedData ); const chartRef = useRef(null); @@ -481,7 +547,13 @@ export default function InvestCandleChart({ const lowerPoints: string[] = []; bb_visible.forEach((bb, i) => { - if (bb?.upper && bb?.lower) { + const candle = slicedData[i]; + if ( + bb?.upper && + bb?.lower && + candle && // candle 존재 확인 + !isDotOnlyCandle(candle) // dot 전용 데이터 제외 + ) { const x = i * candleSpacing; upperPoints.push(`${x},${getY(bb.upper)}`); lowerPoints.push(`${x},${getY(bb.lower)}`); @@ -490,12 +562,11 @@ export default function InvestCandleChart({ if (upperPoints.length === 0) return ""; - // 상단선을 그리고, 하단선을 역순으로 연결해서 닫힌 영역 만들기 const pathData = [ - `M ${upperPoints[0]}`, // 시작점으로 이동 - `L ${upperPoints.slice(1).join(" L ")}`, // 상단선 그리기 - `L ${lowerPoints.slice().reverse().join(" L ")}`, // 하단선을 역순으로 그리기 - "Z", // path 닫기 + `M ${upperPoints[0]}`, + `L ${upperPoints.slice(1).join(" L ")}`, + `L ${lowerPoints.slice().reverse().join(" L ")}`, + "Z", ].join(" "); return pathData; @@ -504,7 +575,7 @@ export default function InvestCandleChart({ // --- 렌더 --- return (
- typeof val === "number" && isFinite(val) + typeof val === "number" && + isFinite(val) && + !isDotOnlyCandle(slicedData[i]) ? `${i * candleSpacing},${(1 - val / 100) * RSI_HEIGHT}` : null ) .filter((v): v is string => v !== null) - .join(" ")} opacity={0.96} /> @@ -995,13 +1067,15 @@ export default function InvestCandleChart({
{/* 일반 candle 값 or dot 전용 candle 값 구분 */} {(() => { + // dotData도 있는 날짜면 const dot = dotData?.find((d) => dayjs(d.date).isSame(tooltip.data!.date, "day") ); + // "오늘" 날짜인지 확인 const isToday = dayjs(tooltip.data!.date).isSame(dayjs(), "day"); - // 실시간 시세 + 예측값만 있는 경우 (오늘) - if (isToday && todayPrice && dot?.close) { + // 오늘이고 todayPrice가 있을 때 (실시간) + if (isToday && todayPrice && dot && dot.close) { return ( <>
@@ -1012,6 +1086,7 @@ export default function InvestCandleChart({
+ 예측값 : {dot.close.toLocaleString()} @@ -1020,55 +1095,47 @@ export default function InvestCandleChart({ ); } - // 예측값만 있는 경우 - if ( - dot?.close && - (!tooltip.data?.open || tooltip.data.volume === 0) - ) { + // dotData(예측값)가 있는 경우 + if (dot) { return (
예측값 - + {" "} : {dot.close.toLocaleString()}
); } - - // 일반 캔들값 (or 예측값도 있는 경우 같이 표시) - const rows = []; - - rows.push( -
시: {tooltip.data.open.toLocaleString()}
, -
고: {tooltip.data.high.toLocaleString()}
, -
저: {tooltip.data.low.toLocaleString()}
, -
종: {tooltip.data.close.toLocaleString()}
, -
- 거래량: {tooltip.data.volume.toLocaleString()} -
, -
RSI: {rsi}
- ); - - if (dot?.close) { - rows.push( -
- - 예측값 - - : {dot.close.toLocaleString()} -
, -
- 오차: {(dot.close - tooltip.data.close).toFixed(2)} ( - {( - ((dot.close - tooltip.data.close) / tooltip.data.close) * - 100 - ).toFixed(2)} - %) + // 일반 candle 값 표기 + return ( + <> +
시: {tooltip.data.open.toLocaleString()}
+
고: {tooltip.data.high.toLocaleString()}
+
저: {tooltip.data.low.toLocaleString()}
+
종: {tooltip.data.close.toLocaleString()}
+
거래량: {tooltip.data.volume.toLocaleString()}
+
+ {/* RSI:{" "} + {typeof rsi_visible[tooltip.idx] === "number" + ? rsi_visible[tooltip.idx].toFixed(2) + : "-"} */} + RSI: {rsi}
- ); - } - - return rows; + {/* dot값이 겹치는 경우 오차 등도 표시 */} + {dot && (dot as any).close !== undefined && ( +
+ 오차: {((dot as any).close - tooltip.data.close).toFixed(2)}{" "} + ( + {( + (((dot as any).close - tooltip.data.close) / + tooltip.data.close) * + 100 + ).toFixed(2)} + %) +
+ )} + + ); })()} {/* 뉴스 영역 그대로 */} {tooltipNews.length > 0 && (