diff --git a/frontend/src/components/TradingChart.tsx b/frontend/src/components/TradingChart.tsx index b7762aa..41c788d 100644 --- a/frontend/src/components/TradingChart.tsx +++ b/frontend/src/components/TradingChart.tsx @@ -7,6 +7,20 @@ import { DISPLAY_COUNT } from '../hooks/useUpbitData'; /** Upbit 실시간 — 초기 히스토리가 이 개수 미만이면 setData 하지 않음 (종목 전환 직후 1~2봉만 로드되는 깨짐 방지) */ const MIN_BARS_FOR_FULL_RELOAD = Math.min(50, Math.floor(DISPLAY_COUNT / 4)); + +/** 종목 전환 직후 stale/partial 데이터 차단 — fetch 완료(barsMarket 확정) 후 sparse history도 렌더 허용 */ +function canApplyChartBars( + barData: OHLCVBar[], + barsMarket: string | null | undefined, + market: string, +): boolean { + if (barData.length === 0) return false; + if (barsMarket === undefined) return true; + if (barsMarket == null || barsMarket !== market) return false; + if (!market.startsWith('KRW-')) return true; + if (barData.length >= MIN_BARS_FOR_FULL_RELOAD) return true; + return barData.length >= 2; +} import DrawingCanvas, { hitTestDrawing } from './DrawingCanvas'; import PaneLegend, { type PaneLegendProps } from './PaneLegend'; import ChartHoverToolbar from './ChartHoverToolbar'; @@ -167,14 +181,9 @@ const TradingChart: React.FC = ({ const [candleOnlyMode, setCandleOnlyMode] = useState(false); useEffect(() => { - const ready = barsMarket == null - ? false - : barsMarket === undefined - ? true - : barsMarket === market; - if (ready) { + if (barsMarket === market) { barsRef.current = bars; - } else if (barsMarket != null && barsMarket !== market) { + } else { barsRef.current = []; } }, [bars, barsMarket, market]); @@ -296,8 +305,7 @@ const TradingChart: React.FC = ({ const barData = barsRef.current; const inds = indicatorsRef.current; if (!m || barData.length === 0) return; - if (barsMarket !== undefined && barsMarket !== market) return; - if (market.startsWith('KRW-') && barData.length < MIN_BARS_FOR_FULL_RELOAD) return; + if (!canApplyChartBars(barData, barsMarket, market)) return; prevBarsKey.current = ''; reloadAllRef.current?.( m, barData, @@ -315,7 +323,7 @@ const TradingChart: React.FC = ({ const barData = barsRef.current; const inds = indicatorsRef.current; if (!m || barData.length === 0) return; - if (barsMarket !== undefined && barsMarket !== market) return; + if (!canApplyChartBars(barData, barsMarket, market)) return; const expected = countExpectedVisibleIndicators(inds); if (expected === 0) return; @@ -699,8 +707,7 @@ const TradingChart: React.FC = ({ // 데이터가 이미 barsRef에 있지만 차트에 아직 세팅되지 않은 경우 (예: 멀티레이아웃 초기 렌더) 재로드 if (barsRef.current.length > 0 && !m.hasMainSeries()) { const bm = barsMarketRef.current; - if (bm != null && bm !== marketRef.current) return; - if (marketRef.current.startsWith('KRW-') && barsRef.current.length < MIN_BARS_FOR_FULL_RELOAD) return; + if (!canApplyChartBars(barsRef.current, bm, marketRef.current)) return; void reloadAll( m, barsRef.current, @@ -723,6 +730,22 @@ const TradingChart: React.FC = ({ }); ro.observe(containerRef.current); + const bootstrapIfReady = () => { + const m = managerRef.current; + if (!m || m.hasMainSeries()) return; + const data = barsRef.current; + if (!canApplyChartBars(data, barsMarketRef.current, marketRef.current)) return; + void reloadAllRef.current( + m, + data, + prevChartType.current, + prevTheme.current, + prevLogScale.current, + indicatorsRef.current, + ); + }; + requestAnimationFrame(() => requestAnimationFrame(bootstrapIfReady)); + return () => { unsub(); unsubClick(); @@ -772,12 +795,7 @@ const TradingChart: React.FC = ({ useEffect(() => { const mgr = managerRef.current; if (!mgr || !chartMgr || bars.length === 0) return; - - const barsReady = barsMarket === undefined - ? true - : barsMarket === market; - if (!barsReady) return; - if (market.startsWith('KRW-') && bars.length < MIN_BARS_FOR_FULL_RELOAD) return; + if (!canApplyChartBars(bars, barsMarket, market)) return; // manager 준비 전 bars 가 먼저 도착한 경우(로그인 직후 등) 메인 시리즈 누락 방지 if (!mgr.hasMainSeries()) { @@ -887,7 +905,7 @@ const TradingChart: React.FC = ({ useEffect(() => { const mgr = managerRef.current; if (!mgr || !chartMgr || bars.length === 0) return; - if (barsMarket !== undefined && barsMarket !== market) return; + if (!canApplyChartBars(bars, barsMarket, market)) return; const expected = countExpectedVisibleIndicators(indicators); if (expected === 0) return; @@ -897,7 +915,7 @@ const TradingChart: React.FC = ({ window.setTimeout(() => { const m = managerRef.current; if (!m || !m.hasMainSeries()) return; - if (barsMarket !== undefined && barsMarket !== market) return; + if (!canApplyChartBars(bars, barsMarket, market)) return; const exp = countExpectedVisibleIndicators(indicatorsRef.current); if (exp === 0 || m.getLoadedIndicatorCount() >= exp) return; prevBarsKey.current = ''; diff --git a/frontend/src/hooks/useStompChartData.ts b/frontend/src/hooks/useStompChartData.ts index f37c9d3..a2596be 100644 --- a/frontend/src/hooks/useStompChartData.ts +++ b/frontend/src/hooks/useStompChartData.ts @@ -141,7 +141,7 @@ export function useStompChartData( .then(newBars => { if (cancelled) return; barsRef.current = newBars; - historyReadyRef.current = newBars.length > 0; + historyReadyRef.current = true; setBarsMarket(market); setBars(newBars); setLatestBar(newBars[newBars.length - 1] ?? null); @@ -269,6 +269,9 @@ export function useStompChartData( last1mVolRef.current = 0; last1mTimeRef.current = 0; void pinChartWatch(market, STOMP_TICK_CANDLE_TYPE); + if (candleType !== STOMP_TICK_CANDLE_TYPE) { + void pinChartWatch(market, candleType); + } const topic = `/sub/charts/${market}/${STOMP_TICK_CANDLE_TYPE}`; let alive = true;