From 3b67fca4eebc7f5da74f22588507f51a6ad1f87f Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 25 May 2026 03:22:46 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=B1=EC=8B=A0=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/TradingChart.tsx | 58 ++++++++++++++++-------- frontend/src/hooks/useStompChartData.ts | 5 +- 2 files changed, 42 insertions(+), 21 deletions(-) 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;