실시간 차트 오류 수정

This commit is contained in:
Macbook
2026-05-25 04:18:30 +09:00
parent ac2e6a1dae
commit 0cfe7fc84c
3 changed files with 96 additions and 27 deletions
+63 -6
View File
@@ -10,6 +10,10 @@ import { getCandleStart, TF_SECONDS } from '../utils/upbitApi';
import { DISPLAY_COUNT, WARMUP_COUNT } from './useUpbitData';
import type { WsStatus } from './useUpbitData';
import { API_BASE } from '../utils/backendApi';
import { fetchUpbitCandlesCached } from '../utils/requestCache';
/** 초기 렌더 최소 봉 수 — 미만이면 업비트 REST 폴백 */
const MIN_INITIAL_BARS = Math.min(50, Math.floor(DISPLAY_COUNT / 4));
/** 백엔드 BarBuilder 는 틱마다 1m 만 STOMP 발행 — 차트 분봉은 클라이언트에서 집계 */
const STOMP_TICK_CANDLE_TYPE = '1m';
@@ -100,6 +104,36 @@ async function fetchInitialBarsWithRetry(
throw lastErr ?? new Error('history fetch failed');
}
/** 백엔드 history → 부족·실패 시 업비트 REST 폴백 (종목별 Ta4j 미적재 대응) */
async function loadInitialBars(
market: string,
timeframe: Timeframe,
count: number,
marketChanged: boolean,
): Promise<OHLCVBar[]> {
const candleType = timeframeToCandleType(timeframe);
let bars: OHLCVBar[] = [];
try {
bars = await fetchInitialBarsWithRetry(market, candleType, count);
} catch {
bars = [];
}
if (bars.length < MIN_INITIAL_BARS) {
try {
const fallback = await fetchUpbitCandlesCached(
market,
timeframe,
Math.min(count, 200),
marketChanged,
);
if (fallback.length > bars.length) bars = fallback;
} catch {
/* 폴백 실패 시 backend 결과 유지 */
}
}
return bars;
}
export function useStompChartData(
market: string,
timeframe: Timeframe,
@@ -122,11 +156,15 @@ export function useStompChartData(
const last1mTimeRef = useRef<number>(0);
/** 초기 히스토리 로드 완료 전에는 차트 직접 갱신 콜백을 호출하지 않음 */
const historyReadyRef = useRef(false);
/** 백엔드 history 비어 있을 때 STOMP 누적 → React bars 동기화 */
const bootstrapFromStompRef = useRef(false);
const prevMarketRef = useRef('');
useEffect(() => {
if (opts.enabled === false) return;
let cancelled = false;
historyReadyRef.current = false;
bootstrapFromStompRef.current = false;
setIsLoading(true);
setError(null);
setBars([]);
@@ -136,16 +174,26 @@ export function useStompChartData(
last1mTimeRef.current = 0;
lastTimeRef.current = 0;
const marketChanged = prevMarketRef.current !== '' && prevMarketRef.current !== market;
prevMarketRef.current = market;
const count = DISPLAY_COUNT + WARMUP_COUNT;
fetchInitialBarsWithRetry(market, candleType, count)
loadInitialBars(market, timeframe, count, marketChanged)
.then(newBars => {
if (cancelled) return;
barsRef.current = newBars;
historyReadyRef.current = true;
setBarsMarket(market);
setBars(newBars);
setLatestBar(newBars[newBars.length - 1] ?? null);
lastTimeRef.current = newBars[newBars.length - 1]?.time ?? 0;
if (newBars.length >= 2) {
bootstrapFromStompRef.current = false;
setBars(newBars);
setLatestBar(newBars[newBars.length - 1] ?? null);
lastTimeRef.current = newBars[newBars.length - 1]?.time ?? 0;
} else {
bootstrapFromStompRef.current = true;
setBars([]);
setLatestBar(null);
}
setIsLoading(false);
})
.catch(err => {
@@ -154,7 +202,15 @@ export function useStompChartData(
setIsLoading(false);
});
return () => { cancelled = true; };
}, [market, candleType, opts.enabled]);
}, [market, candleType, timeframe, opts.enabled]);
const syncBarsToReactIfNeeded = useCallback((marketId: string) => {
if (!bootstrapFromStompRef.current) return;
if (barsRef.current.length < 2) return;
bootstrapFromStompRef.current = false;
setBarsMarket(marketId);
setBars([...barsRef.current]);
}, []);
const applyCandle = useCallback((data: StompCandlePayload, displayTf: Timeframe) => {
if (!historyReadyRef.current) return;
@@ -193,6 +249,7 @@ export function useStompChartData(
if (notifyChart) optsRef.current.onTickUpdate(patched);
}
lastTimeRef.current = bar.time;
syncBarsToReactIfNeeded(market);
};
const lastChart = barsRef.current[barsRef.current.length - 1];
@@ -256,7 +313,7 @@ export function useStompChartData(
last1mTimeRef.current = oneMin.time;
last1mVolRef.current = oneMin.volume;
}
}, []);
}, [market, syncBarsToReactIfNeeded]);
useEffect(() => {
if (opts.enabled === false) {