Files
goldenChart/frontend/src/components/paper/PaperMiniChart.tsx
T
2026-05-25 14:05:13 +09:00

171 lines
5.2 KiB
TypeScript

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<Props> = ({
market,
theme = 'dark',
wsLabel = 'WebSocket <100ms',
timeframe: timeframeProp,
onTimeframeChange,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
const [timeframeInternal, setTimeframeInternal] = useState<Timeframe>('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 (
<div className="ptd-chart-wrap">
<div className="ptd-chart-head">
<span className="ptd-chart-title">실시간 분석 차트</span>
<span className="ptd-ws-badge">{wsLabel} 실시간 데이터 반영</span>
</div>
<div ref={containerRef} className="ptd-chart-canvas" />
{loading && <div className="ptd-chart-loading">차트 로딩…</div>}
<div className="ptd-tf-row">
{TF_OPTIONS.map(tf => (
<button
key={tf}
type="button"
className={`ptd-tf-btn${timeframe === tf ? ' ptd-tf-btn--active' : ''}`}
onClick={() => setTimeframe(tf)}
>
{tf}
</button>
))}
</div>
</div>
);
};
export default PaperMiniChart;