"use client"; import { useEffect, useRef } from "react"; import { createChart, type CandlestickData, type IChartApi, type ISeriesApi, type LineData, type SeriesMarker, type Time, type UTCTimestamp, } from "lightweight-charts"; import type { ChartPayload, LatestPredictionResponse } from "../lib/api"; type Props = { chart: ChartPayload; prediction?: LatestPredictionResponse | null; }; // 'YYYY-MM-DD' 또는 'YYYY-MM-DDTHH:MM:SS' (KST naive, 백엔드가 +09:00 시각의 wall-clock 을 // 그대로 ISO 로 직렬화) 를 UTCTimestamp 로. lightweight-charts 는 timestamp 가 UTC 라고 // 가정하지만, 우리는 KST wall-clock 을 UTC 인 척 넣는다 — timeScale 의 표시도 KST 그대로 // 나와서 한국 사용자에겐 가장 직관적. function isoToUtcTs(s: string): UTCTimestamp { if (s.length <= 10) { return (Date.UTC( Number(s.slice(0, 4)), Number(s.slice(5, 7)) - 1, Number(s.slice(8, 10)), ) / 1000) as UTCTimestamp; } // datetime: YYYY-MM-DDTHH:MM:SS return (Date.UTC( Number(s.slice(0, 4)), Number(s.slice(5, 7)) - 1, Number(s.slice(8, 10)), Number(s.slice(11, 13)), Number(s.slice(14, 16)), Number(s.slice(17, 19) || "0"), ) / 1000) as UTCTimestamp; } export function StockChart({ chart, prediction }: Props) { const containerRef = useRef(null); const chartRef = useRef(null); const candleRef = useRef | null>(null); const predRef = useRef | null>(null); const predLowRef = useRef | null>(null); const predHighRef = useRef | null>(null); const isIntraday = chart.interval === "10m"; // create chart once (interval 바뀌면 timeVisible 토글 위해 의존성에 isIntraday 포함 — 재생성) useEffect(() => { if (!containerRef.current) return; const c = createChart(containerRef.current, { layout: { background: { color: "transparent" }, textColor: "#cbd5e1", }, grid: { vertLines: { color: "#1f2937" }, horzLines: { color: "#1f2937" }, }, rightPriceScale: { borderColor: "#374151" }, timeScale: { borderColor: "#374151", timeVisible: isIntraday, secondsVisible: false, }, autoSize: true, }); const candle = c.addCandlestickSeries({ upColor: "#22c55e", downColor: "#ef4444", borderUpColor: "#22c55e", borderDownColor: "#ef4444", wickUpColor: "#22c55e", wickDownColor: "#ef4444", }); chartRef.current = c; candleRef.current = candle; return () => { c.remove(); chartRef.current = null; candleRef.current = null; predRef.current = null; predLowRef.current = null; predHighRef.current = null; }; }, [isIntraday]); // push candle data + today marker useEffect(() => { if (!candleRef.current) return; const data: CandlestickData[] = chart.ohlcv .filter((p) => p.open !== null && p.high !== null && p.low !== null && p.close !== null) .map((p) => ({ time: isoToUtcTs(p.date), open: p.open as number, high: p.high as number, low: p.low as number, close: p.close as number, })); candleRef.current.setData(data); // 오늘 날짜 마커: 일/주/월봉에서만 표시 (10분봉은 데이터 자체가 오늘 하루라 무의미). // markers 는 데이터 포인트의 time 과 일치해야 표시되므로, 오늘 또는 가장 가까운 과거 // 거래일을 찾는다. if (!isIntraday && chart.today) { const todayTs = isoToUtcTs(chart.today); // 차트의 마지막 데이터가 오늘이면 그 위에, 아니면 마지막 데이터 위에 "오늘" 표시. const lastTs = data.length > 0 ? (data[data.length - 1].time as UTCTimestamp) : null; const markerTime = (lastTs && lastTs <= todayTs ? lastTs : todayTs) as UTCTimestamp; const markers: SeriesMarker