Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ export default function InvestmentStockClient() {
</>
)}
<span
className="px-1 cursor-pointer text-gray-400 hover:bg-gray-800 rounded-sm"
className="cursor-pointer text-gray-400 hover:bg-gray-800 rounded-sm"
onClick={() => setShowIndicators((prev) => !prev)}
>
{showIndicators ? "– 보조지표 접기" : "+ 보조지표 설정"}
Expand Down
2 changes: 1 addition & 1 deletion src/components/blocks/PracticePage/PracticePage.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ export default function PracticeClient() {
)}

<span
className="px-1 cursor-pointer text-gray-400 hover:bg-gray-800 rounded-sm"
className="cursor-pointer text-gray-400 hover:bg-gray-800 rounded-sm"
onClick={() => setShowIndicators((prev) => !prev)}
>
{showIndicators ? "– 보조지표 접기" : "+ 보조지표 설정"}
Expand Down
187 changes: 127 additions & 60 deletions src/components/charts/InvestCandleChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,39 +385,105 @@ 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
)
.filter(Boolean)
.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<HTMLDivElement>(null);
Expand Down Expand Up @@ -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)}`);
Expand All @@ -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;
Expand All @@ -504,7 +575,7 @@ export default function InvestCandleChart({
// --- 렌더 ---
return (
<div
className="flex flex-col"
className="flex flex-col "
style={{
width: "100%",
maxWidth: w,
Expand Down Expand Up @@ -921,12 +992,13 @@ export default function InvestCandleChart({
strokeWidth="2"
points={rsi_visible
.map((val, i) =>
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}
/>
Expand Down Expand Up @@ -995,13 +1067,15 @@ export default function InvestCandleChart({
</div>
{/* 일반 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 (
<>
<div>
Expand All @@ -1012,6 +1086,7 @@ export default function InvestCandleChart({
</div>
<div style={{ marginTop: 4 }}>
<span style={{ color: "#C9DF00", fontWeight: 600 }}>

예측값
</span>
: {dot.close.toLocaleString()}
Expand All @@ -1020,55 +1095,47 @@ export default function InvestCandleChart({
);
}

// 예측값만 있는 경우
if (
dot?.close &&
(!tooltip.data?.open || tooltip.data.volume === 0)
) {
// dotData(예측값)가 있는 경우
if (dot) {
return (
<div>
<span style={{ color: "#C9DF00", fontWeight: 600 }}>
예측값
</span>
</span>{" "}
: {dot.close.toLocaleString()}
</div>
);
}

// 일반 캔들값 (or 예측값도 있는 경우 같이 표시)
const rows = [];

rows.push(
<div key="open">시: {tooltip.data.open.toLocaleString()}</div>,
<div key="high">고: {tooltip.data.high.toLocaleString()}</div>,
<div key="low">저: {tooltip.data.low.toLocaleString()}</div>,
<div key="close">종: {tooltip.data.close.toLocaleString()}</div>,
<div key="vol">
거래량: {tooltip.data.volume.toLocaleString()}
</div>,
<div key="rsi">RSI: {rsi}</div>
);

if (dot?.close) {
rows.push(
<div key="pred" style={{ marginTop: 6 }}>
<span style={{ color: "#396FFB", fontWeight: 600 }}>
예측값
</span>
: {dot.close.toLocaleString()}
</div>,
<div key="diff" className="text-[#d23e3e] font-bold">
오차: {(dot.close - tooltip.data.close).toFixed(2)} (
{(
((dot.close - tooltip.data.close) / tooltip.data.close) *
100
).toFixed(2)}
%)
// 일반 candle 값 표기
return (
<>
<div>시: {tooltip.data.open.toLocaleString()}</div>
<div>고: {tooltip.data.high.toLocaleString()}</div>
<div>저: {tooltip.data.low.toLocaleString()}</div>
<div>종: {tooltip.data.close.toLocaleString()}</div>
<div>거래량: {tooltip.data.volume.toLocaleString()}</div>
<div>
{/* RSI:{" "}
{typeof rsi_visible[tooltip.idx] === "number"
? rsi_visible[tooltip.idx].toFixed(2)
: "-"} */}
RSI: {rsi}
</div>
);
}

return rows;
{/* dot값이 겹치는 경우 오차 등도 표시 */}
{dot && (dot as any).close !== undefined && (
<div className="text-[#e75480] font-bold">
오차: {((dot as any).close - tooltip.data.close).toFixed(2)}{" "}
(
{(
(((dot as any).close - tooltip.data.close) /
tooltip.data.close) *
100
).toFixed(2)}
%)
</div>
)}
</>
);
})()}
{/* 뉴스 영역 그대로 */}
{tooltipNews.length > 0 && (
Expand Down