import React, { useCallback, useEffect, useRef, useState } from 'react'; import { createChart, CandlestickSeries, type IChartApi, type ISeriesApi, type Time, ColorType } from 'lightweight-charts'; import type { Theme, Timeframe } from '../../types'; import { fetchUpbitCandles } from '../../utils/upbitApi'; const TF_OPTIONS: Timeframe[] = ['1m', '5m', '10m', '15m', '1h', '4h', '1D']; interface Props { market: string; theme?: Theme; wsLabel?: string; timeframe?: Timeframe; onTimeframeChange?: (tf: Timeframe) => void; } function readChartTheme(container: HTMLElement | null) { const root = container?.closest('.app') ?? document.documentElement; const s = getComputedStyle(root); const v = (name: string, fallback: string) => s.getPropertyValue(name).trim() || fallback; return { bgColor: v('--bg3', '#1f2335'), textColor: v('--text', '#c0caf5'), gridColor: v('--sep', 'rgba(122, 162, 247, 0.15)'), borderColor: v('--border', 'rgba(122, 162, 247, 0.15)'), upColor: v('--up', '#ff6b6b'), downColor: v('--down', '#4dabf7'), }; } const PaperMiniChart: React.FC = ({ market, theme = 'dark', wsLabel = 'WebSocket <100ms', timeframe: timeframeProp, onTimeframeChange, }) => { const containerRef = useRef(null); const chartRef = useRef(null); const seriesRef = useRef | null>(null); const [timeframeInternal, setTimeframeInternal] = useState('1h'); const timeframe = timeframeProp ?? timeframeInternal; const setTimeframe = onTimeframeChange ?? setTimeframeInternal; const [loading, setLoading] = useState(true); const applyTheme = useCallback((chart: IChartApi, series: ISeriesApi<'Candlestick'>) => { const t = readChartTheme(containerRef.current); chart.applyOptions({ layout: { background: { type: ColorType.Solid, color: t.bgColor }, textColor: t.textColor, }, grid: { vertLines: { color: t.gridColor }, horzLines: { color: t.gridColor }, }, rightPriceScale: { borderColor: t.borderColor }, timeScale: { borderColor: t.borderColor }, }); series.applyOptions({ upColor: t.upColor, downColor: t.downColor, borderUpColor: t.upColor, borderDownColor: t.downColor, wickUpColor: t.upColor, wickDownColor: t.downColor, }); }, []); useEffect(() => { if (!containerRef.current) return undefined; const t = readChartTheme(containerRef.current); const chart = createChart(containerRef.current, { layout: { background: { type: ColorType.Solid, color: t.bgColor }, textColor: t.textColor, }, grid: { vertLines: { color: t.gridColor }, horzLines: { color: t.gridColor }, }, rightPriceScale: { borderColor: t.borderColor }, timeScale: { borderColor: t.borderColor }, crosshair: { mode: 1 }, }); const series = chart.addSeries(CandlestickSeries, { upColor: t.upColor, downColor: t.downColor, borderUpColor: t.upColor, borderDownColor: t.downColor, wickUpColor: t.upColor, wickDownColor: t.downColor, }); chartRef.current = chart; seriesRef.current = series; const ro = new ResizeObserver(() => { if (containerRef.current) { chart.applyOptions({ width: containerRef.current.clientWidth, height: containerRef.current.clientHeight, }); } }); ro.observe(containerRef.current); return () => { ro.disconnect(); chart.remove(); chartRef.current = null; seriesRef.current = null; }; }, []); useEffect(() => { if (chartRef.current && seriesRef.current) { applyTheme(chartRef.current, seriesRef.current); } }, [theme, applyTheme]); useEffect(() => { let cancelled = false; setLoading(true); (async () => { try { const bars = await fetchUpbitCandles(market, timeframe, 200); if (cancelled || !seriesRef.current) return; seriesRef.current.setData( bars.map(b => ({ time: b.time as Time, open: b.open, high: b.high, low: b.low, close: b.close, })), ); chartRef.current?.timeScale().fitContent(); } catch { /* ignore */ } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [market, timeframe]); return (
실시간 분석 차트 {wsLabel} 실시간 데이터 반영
{loading &&
차트 로딩…
}
{TF_OPTIONS.map(tf => ( ))}
); }; export default PaperMiniChart;