실시간 차트 갱신오류 수정

This commit is contained in:
Macbook
2026-05-25 03:49:53 +09:00
parent 3b67fca4ee
commit 67a6aa0dee
5 changed files with 183 additions and 97 deletions
+1 -1
View File
@@ -631,7 +631,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
)}
<TradingChart
key={`${symbol}:${timeframe}:${reloadTick}`}
key={reloadTick}
bars={bars}
barsMarket={barsMarket}
market={symbol}
+152 -91
View File
@@ -287,6 +287,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
// ── 전체 재로드 (데이터 + 인디케이터) ─────────────────────────────────────
const reloadSafetyTimers = useRef<ReturnType<typeof setTimeout>[]>([]);
const reloadGenRef = useRef(0);
const reloadInFlightRef = useRef(false);
const reloadCoalesceRef = useRef(false);
const reloadRetryRef = useRef(0);
const reloadAllRef = useRef<(
mgr: ChartManager,
@@ -350,6 +352,17 @@ const TradingChart: React.FC<TradingChartProps> = ({
}, 120);
}, [market, barsMarket, applyPaneLayout]);
/** setData 직후 캔들·볼륨 pane 레이아웃 확정 (지표 로드 중단 시에도 빈 화면 방지) */
const commitCandleLayout = useCallback((mgr: ChartManager) => {
requestAnimationFrame(() => {
applyPaneLayout(mgr);
requestAnimationFrame(() => {
const futureBars = mgr.hasIchimoku() ? 28 : 0;
mgr.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
});
});
}, [applyPaneLayout]);
const reloadAll = useCallback(async (
mgr: ChartManager,
newBars: OHLCVBar[],
@@ -360,104 +373,123 @@ const TradingChart: React.FC<TradingChartProps> = ({
) => {
if (newBars.length === 0) return;
const gen = ++reloadGenRef.current;
reloadSafetyTimers.current.forEach(clearTimeout);
reloadSafetyTimers.current = [];
barsRef.current = newBars;
mgr.setData(newBars, ct, th);
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
queueFullReload();
return;
}
mgr.setTheme(th);
mgr.setLogScale(ls);
mgr.setDisplayTimezone(displayTimezone, timeframe);
for (const ind of inds) {
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
queueFullReload();
return;
}
await mgr.addIndicator(ind);
}
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
queueFullReload();
if (reloadInFlightRef.current) {
reloadCoalesceRef.current = true;
barsRef.current = newBars;
return;
}
const expected = countExpectedVisibleIndicators(inds);
const loaded = mgr.getLoadedIndicatorCount();
reloadInFlightRef.current = true;
if (expected > 0 && loaded < expected) {
prevBarsKey.current = '';
if (reloadRetryRef.current < 4 && mgr.hasMainSeries()) {
reloadRetryRef.current += 1;
await mgr.reloadIndicatorsOnly(inds);
try {
do {
reloadCoalesceRef.current = false;
const gen = reloadGenRef.current;
const barData = barsRef.current.length > 0 ? barsRef.current : newBars;
if (barData.length === 0) break;
reloadSafetyTimers.current.forEach(clearTimeout);
reloadSafetyTimers.current = [];
barsRef.current = barData;
mgr.setData(barData, ct, th);
if (gen !== reloadGenRef.current) {
queueFullReload();
return;
}
}
if (mgr.getLoadedIndicatorCount() < expected) {
if (reloadRetryRef.current < 6) {
reloadRetryRef.current += 1;
queueIndicatorRecovery();
}
return;
}
}
reloadRetryRef.current = 0;
prevChartType.current = ct;
prevTheme.current = th;
prevLogScale.current = ls;
prevBarsKey.current = barsKey(newBars, market);
prevIndKey.current = indKey(inds);
prevSortedPKRef.current = sortedParamKey(inds);
await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => {
applyPaneLayout(mgr);
requestAnimationFrame(() => {
const futureBars = mgr.hasIchimoku() ? 28 : 0;
mgr.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
resolve();
});
})));
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
queueFullReload();
return;
}
onDataLoaded?.();
reloadSafetyTimers.current = [300, 700, 1400].map(delay =>
setTimeout(() => {
const m = managerRef.current;
if (!m || !m.hasMainSeries()) return;
const expectedLater = countExpectedVisibleIndicators(indicatorsRef.current);
if (expectedLater > 0 && m.getLoadedIndicatorCount() < expectedLater) {
prevBarsKey.current = '';
queueIndicatorRecovery();
return;
break;
}
const w = wrapperRef.current;
if (!w || w.clientHeight <= 0) return;
m.resetPaneHeights(w.clientHeight);
const fb = m.hasIchimoku() ? 28 : 0;
m.setInitialVisibleRange(DISPLAY_COUNT, fb);
if (delay === 1400) onDataLoaded?.();
}, delay)
);
}, [applyPaneLayout, onDataLoaded, displayTimezone, timeframe, market, queueFullReload]);
mgr.setTheme(th);
mgr.setLogScale(ls);
mgr.setDisplayTimezone(displayTimezone, timeframe);
commitCandleLayout(mgr);
let stale = false;
for (const ind of inds) {
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
stale = true;
break;
}
await mgr.addIndicator(ind);
}
if (stale) break;
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
break;
}
const expected = countExpectedVisibleIndicators(inds);
const loaded = mgr.getLoadedIndicatorCount();
if (expected > 0 && loaded < expected) {
prevBarsKey.current = '';
if (reloadRetryRef.current < 4 && mgr.hasMainSeries()) {
reloadRetryRef.current += 1;
await mgr.reloadIndicatorsOnly(inds);
if (gen !== reloadGenRef.current) break;
}
if (mgr.getLoadedIndicatorCount() < expected) {
commitCandleLayout(mgr);
if (reloadRetryRef.current < 6) {
reloadRetryRef.current += 1;
queueIndicatorRecovery();
}
break;
}
}
reloadRetryRef.current = 0;
prevChartType.current = ct;
prevTheme.current = th;
prevLogScale.current = ls;
prevBarsKey.current = barsKey(barData, market);
prevIndKey.current = indKey(inds);
prevSortedPKRef.current = sortedParamKey(inds);
await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => {
applyPaneLayout(mgr);
requestAnimationFrame(() => {
const futureBars = mgr.hasIchimoku() ? 28 : 0;
mgr.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
resolve();
});
})));
if (gen !== reloadGenRef.current) {
prevBarsKey.current = '';
break;
}
onDataLoaded?.();
reloadSafetyTimers.current = [300, 700, 1400].map(delay =>
setTimeout(() => {
const m = managerRef.current;
if (!m || !m.hasMainSeries()) return;
const expectedLater = countExpectedVisibleIndicators(indicatorsRef.current);
if (expectedLater > 0 && m.getLoadedIndicatorCount() < expectedLater) {
prevBarsKey.current = '';
queueIndicatorRecovery();
return;
}
const w = wrapperRef.current;
if (!w || w.clientHeight <= 0) return;
m.resetPaneHeights(w.clientHeight);
const fb = m.hasIchimoku() ? 28 : 0;
m.setInitialVisibleRange(DISPLAY_COUNT, fb);
if (delay === 1400) onDataLoaded?.();
}, delay)
);
} while (reloadCoalesceRef.current);
} finally {
reloadInFlightRef.current = false;
if (reloadCoalesceRef.current) {
reloadCoalesceRef.current = false;
queueFullReload();
}
}
}, [applyPaneLayout, commitCandleLayout, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
useEffect(() => {
reloadAllRef.current = reloadAll;
@@ -786,6 +818,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
barsRef.current = [];
reloadRetryRef.current = 0;
reloadGenRef.current += 1;
reloadInFlightRef.current = false;
reloadCoalesceRef.current = false;
reloadSafetyTimers.current.forEach(clearTimeout);
reloadSafetyTimers.current = [];
managerRef.current?.cancelPendingIndicatorUpdates();
@@ -901,6 +935,33 @@ const TradingChart: React.FC<TradingChartProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators]);
// 데이터는 준비됐으나 메인 시리즈가 없는 빈 차트 자동 복구 (종목 전환·레이아웃 전환 직후)
useEffect(() => {
const mgr = managerRef.current;
if (!mgr || !chartMgr || bars.length === 0) return;
if (!canApplyChartBars(bars, barsMarket, market)) return;
if (mgr.hasMainSeries()) return;
const timers = [0, 80, 200, 500].map(delay =>
window.setTimeout(() => {
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,
);
}, delay),
);
return () => { timers.forEach(clearTimeout); };
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators]);
// 종목 전환 직후 reloadAll 이 중단되어 캔들만 남은 경우 자동 복구
useEffect(() => {
const mgr = managerRef.current;