실시간 차트 갱신오류 수정
This commit is contained in:
@@ -631,7 +631,7 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
)}
|
||||
|
||||
<TradingChart
|
||||
key={`${symbol}:${timeframe}:${reloadTick}`}
|
||||
key={reloadTick}
|
||||
bars={bars}
|
||||
barsMarket={barsMarket}
|
||||
market={symbol}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user