@@ -385,39 +385,105 @@ export default function InvestCandleChart({
385385 visibleCandles > 1 ? chartWidth / ( visibleCandles - 1 ) : chartWidth ;
386386 const candleWidth = Math . min ( 40 , candleSpacing * 0.7 , 24 ) ;
387387
388+ // function getLinePoints(
389+ // arr: (number | null)[],
390+ // candleSpacing: number,
391+ // getY: (v: number) => number
392+ // ) {
393+ // return arr
394+ // .map((val, i) =>
395+ // typeof val === "number" && !isNaN(val)
396+ // ? `${i * candleSpacing},${getY(val)}`
397+ // : null
398+ // )
399+ // .filter(Boolean)
400+ // .join(" ");
401+ // }
388402 function getLinePoints (
389403 arr : ( number | null ) [ ] ,
390404 candleSpacing : number ,
391- getY : ( v : number ) => number
405+ getY : ( v : number ) => number ,
406+ slicedData : Candle [ ]
392407 ) {
393408 return arr
394409 . map ( ( val , i ) =>
395- typeof val === "number" && ! isNaN ( val )
410+ typeof val === "number" &&
411+ ! isNaN ( val ) &&
412+ slicedData [ i ] &&
413+ ! isDotOnlyCandle ( slicedData [ i ] )
396414 ? `${ i * candleSpacing } ,${ getY ( val ) } `
397415 : null
398416 )
399417 . filter ( Boolean )
400418 . join ( " " ) ;
401419 }
402420
403- const ma5Points = getLinePoints ( ma5_visible , candleSpacing , getY ) ;
404- const ma20Points = getLinePoints ( ma20_visible , candleSpacing , getY ) ;
405- const ma60Points = getLinePoints ( ma60_visible , candleSpacing , getY ) ;
406- const ma120Points = getLinePoints ( ma120_visible , candleSpacing , getY ) ;
421+ // 캔들이 정상적인 날인지 확인하는 유틸 함수
422+ function isDotOnlyCandle ( candle : Candle ) {
423+ return (
424+ candle . open === candle . close &&
425+ candle . high === candle . close &&
426+ candle . low === candle . close &&
427+ candle . volume === 0
428+ ) ;
429+ }
430+
431+ // const ma5Points = getLinePoints(ma5_visible, candleSpacing, getY);
432+ // const ma20Points = getLinePoints(ma20_visible, candleSpacing, getY);
433+ // const ma60Points = getLinePoints(ma60_visible, candleSpacing, getY);
434+ // const ma120Points = getLinePoints(ma120_visible, candleSpacing, getY);
435+ // const bb_upper_points = getLinePoints(
436+ // bb_visible.map((b) => b?.upper),
437+ // candleSpacing,
438+ // getY
439+ // );
440+ // const bb_middle_points = getLinePoints(
441+ // bb_visible.map((b) => b?.middle),
442+ // candleSpacing,
443+ // getY
444+ // );
445+ // const bb_lower_points = getLinePoints(
446+ // bb_visible.map((b) => b?.lower),
447+ // candleSpacing,
448+ // getY
449+ // );
450+ const ma5Points = getLinePoints ( ma5_visible , candleSpacing , getY , slicedData ) ;
451+ const ma20Points = getLinePoints (
452+ ma20_visible ,
453+ candleSpacing ,
454+ getY ,
455+ slicedData
456+ ) ;
457+ const ma60Points = getLinePoints (
458+ ma60_visible ,
459+ candleSpacing ,
460+ getY ,
461+ slicedData
462+ ) ;
463+ const ma120Points = getLinePoints (
464+ ma120_visible ,
465+ candleSpacing ,
466+ getY ,
467+ slicedData
468+ ) ;
469+
407470 const bb_upper_points = getLinePoints (
408471 bb_visible . map ( ( b ) => b ?. upper ) ,
409472 candleSpacing ,
410- getY
473+ getY ,
474+ slicedData
411475 ) ;
412476 const bb_middle_points = getLinePoints (
413477 bb_visible . map ( ( b ) => b ?. middle ) ,
414478 candleSpacing ,
415- getY
479+ getY ,
480+ slicedData
416481 ) ;
417482 const bb_lower_points = getLinePoints (
418483 bb_visible . map ( ( b ) => b ?. lower ) ,
419484 candleSpacing ,
420- getY
485+ getY ,
486+ slicedData
421487 ) ;
422488
423489 const chartRef = useRef < HTMLDivElement > ( null ) ;
@@ -481,7 +547,13 @@ export default function InvestCandleChart({
481547 const lowerPoints : string [ ] = [ ] ;
482548
483549 bb_visible . forEach ( ( bb , i ) => {
484- if ( bb ?. upper && bb ?. lower ) {
550+ const candle = slicedData [ i ] ;
551+ if (
552+ bb ?. upper &&
553+ bb ?. lower &&
554+ candle && // candle 존재 확인
555+ ! isDotOnlyCandle ( candle ) // dot 전용 데이터 제외
556+ ) {
485557 const x = i * candleSpacing ;
486558 upperPoints . push ( `${ x } ,${ getY ( bb . upper ) } ` ) ;
487559 lowerPoints . push ( `${ x } ,${ getY ( bb . lower ) } ` ) ;
@@ -490,12 +562,11 @@ export default function InvestCandleChart({
490562
491563 if ( upperPoints . length === 0 ) return "" ;
492564
493- // 상단선을 그리고, 하단선을 역순으로 연결해서 닫힌 영역 만들기
494565 const pathData = [
495- `M ${ upperPoints [ 0 ] } ` , // 시작점으로 이동
496- `L ${ upperPoints . slice ( 1 ) . join ( " L " ) } ` , // 상단선 그리기
497- `L ${ lowerPoints . slice ( ) . reverse ( ) . join ( " L " ) } ` , // 하단선을 역순으로 그리기
498- "Z" , // path 닫기
566+ `M ${ upperPoints [ 0 ] } ` ,
567+ `L ${ upperPoints . slice ( 1 ) . join ( " L " ) } ` ,
568+ `L ${ lowerPoints . slice ( ) . reverse ( ) . join ( " L " ) } ` ,
569+ "Z" ,
499570 ] . join ( " " ) ;
500571
501572 return pathData ;
@@ -504,7 +575,7 @@ export default function InvestCandleChart({
504575 // --- 렌더 ---
505576 return (
506577 < div
507- className = "flex flex-col"
578+ className = "flex flex-col "
508579 style = { {
509580 width : "100%" ,
510581 maxWidth : w ,
@@ -921,12 +992,13 @@ export default function InvestCandleChart({
921992 strokeWidth = "2"
922993 points = { rsi_visible
923994 . map ( ( val , i ) =>
924- typeof val === "number" && isFinite ( val )
995+ typeof val === "number" &&
996+ isFinite ( val ) &&
997+ ! isDotOnlyCandle ( slicedData [ i ] )
925998 ? `${ i * candleSpacing } ,${ ( 1 - val / 100 ) * RSI_HEIGHT } `
926999 : null
9271000 )
9281001 . filter ( ( v ) : v is string => v !== null )
929-
9301002 . join ( " " ) }
9311003 opacity = { 0.96 }
9321004 />
@@ -995,13 +1067,15 @@ export default function InvestCandleChart({
9951067 </ div >
9961068 { /* 일반 candle 값 or dot 전용 candle 값 구분 */ }
9971069 { ( ( ) => {
1070+ // dotData도 있는 날짜면
9981071 const dot = dotData ?. find ( ( d ) =>
9991072 dayjs ( d . date ) . isSame ( tooltip . data ! . date , "day" )
10001073 ) ;
1074+ // "오늘" 날짜인지 확인
10011075 const isToday = dayjs ( tooltip . data ! . date ) . isSame ( dayjs ( ) , "day" ) ;
10021076
1003- // 실시간 시세 + 예측값만 있는 경우 (오늘 )
1004- if ( isToday && todayPrice && dot ? .close ) {
1077+ // 오늘이고 todayPrice가 있을 때 (실시간 )
1078+ if ( isToday && todayPrice && dot && dot . close ) {
10051079 return (
10061080 < >
10071081 < div >
@@ -1012,6 +1086,7 @@ export default function InvestCandleChart({
10121086 </ div >
10131087 < div style = { { marginTop : 4 } } >
10141088 < span style = { { color : "#C9DF00" , fontWeight : 600 } } >
1089+
10151090 예측값
10161091 </ span >
10171092 : { dot . close . toLocaleString ( ) }
@@ -1020,55 +1095,47 @@ export default function InvestCandleChart({
10201095 ) ;
10211096 }
10221097
1023- // 예측값만 있는 경우
1024- if (
1025- dot ?. close &&
1026- ( ! tooltip . data ?. open || tooltip . data . volume === 0 )
1027- ) {
1098+ // dotData(예측값)가 있는 경우
1099+ if ( dot ) {
10281100 return (
10291101 < div >
10301102 < span style = { { color : "#C9DF00" , fontWeight : 600 } } >
10311103 예측값
1032- </ span >
1104+ </ span > { " " }
10331105 : { dot . close . toLocaleString ( ) }
10341106 </ div >
10351107 ) ;
10361108 }
1037-
1038- // 일반 캔들값 (or 예측값도 있는 경우 같이 표시)
1039- const rows = [ ] ;
1040-
1041- rows . push (
1042- < div key = "open" > 시: { tooltip . data . open . toLocaleString ( ) } </ div > ,
1043- < div key = "high" > 고: { tooltip . data . high . toLocaleString ( ) } </ div > ,
1044- < div key = "low" > 저: { tooltip . data . low . toLocaleString ( ) } </ div > ,
1045- < div key = "close" > 종: { tooltip . data . close . toLocaleString ( ) } </ div > ,
1046- < div key = "vol" >
1047- 거래량: { tooltip . data . volume . toLocaleString ( ) }
1048- </ div > ,
1049- < div key = "rsi" > RSI: { rsi } </ div >
1050- ) ;
1051-
1052- if ( dot ?. close ) {
1053- rows . push (
1054- < div key = "pred" style = { { marginTop : 6 } } >
1055- < span style = { { color : "#396FFB" , fontWeight : 600 } } >
1056- 예측값
1057- </ span >
1058- : { dot . close . toLocaleString ( ) }
1059- </ div > ,
1060- < div key = "diff" className = "text-[#d23e3e] font-bold" >
1061- 오차: { ( dot . close - tooltip . data . close ) . toFixed ( 2 ) } (
1062- { (
1063- ( ( dot . close - tooltip . data . close ) / tooltip . data . close ) *
1064- 100
1065- ) . toFixed ( 2 ) }
1066- %)
1109+ // 일반 candle 값 표기
1110+ return (
1111+ < >
1112+ < div > 시: { tooltip . data . open . toLocaleString ( ) } </ div >
1113+ < div > 고: { tooltip . data . high . toLocaleString ( ) } </ div >
1114+ < div > 저: { tooltip . data . low . toLocaleString ( ) } </ div >
1115+ < div > 종: { tooltip . data . close . toLocaleString ( ) } </ div >
1116+ < div > 거래량: { tooltip . data . volume . toLocaleString ( ) } </ div >
1117+ < div >
1118+ { /* RSI:{" "}
1119+ {typeof rsi_visible[tooltip.idx] === "number"
1120+ ? rsi_visible[tooltip.idx].toFixed(2)
1121+ : "-"} */ }
1122+ RSI: { rsi }
10671123 </ div >
1068- ) ;
1069- }
1070-
1071- return rows ;
1124+ { /* dot값이 겹치는 경우 오차 등도 표시 */ }
1125+ { dot && ( dot as any ) . close !== undefined && (
1126+ < div className = "text-[#e75480] font-bold" >
1127+ 오차: { ( ( dot as any ) . close - tooltip . data . close ) . toFixed ( 2 ) } { " " }
1128+ (
1129+ { (
1130+ ( ( ( dot as any ) . close - tooltip . data . close ) /
1131+ tooltip . data . close ) *
1132+ 100
1133+ ) . toFixed ( 2 ) }
1134+ %)
1135+ </ div >
1136+ ) }
1137+ </ >
1138+ ) ;
10721139 } ) ( ) }
10731140 { /* 뉴스 영역 그대로 */ }
10741141 { tooltipNews . length > 0 && (
0 commit comments