실시간 차트 갱신오류 수정

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
+2 -5
View File
@@ -1092,11 +1092,8 @@ function App() {
noAutoSave: !dbLoaded,
});
/** 로그인·로그아웃·종목·타임프레임 변경 시 차트 재마운트 (종목 전환 직후 stale 데이터·중단된 reload 방지) */
const chartMountKey = useMemo(
() => `${sessionKey}:${symbol}:${timeframe}`,
[sessionKey, symbol, timeframe],
);
/** 로그인·로그아웃 시에만 차트 재마운트 (종목·타임프레임은 TradingChart 내부 reloadAll로 처리) */
const chartMountKey = useMemo(() => sessionKey, [sessionKey]);
// DB에 워크스페이스 없음(204) → appDefaults 레이아웃 적용 후 저장 허용
useEffect(() => {
+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;
+22
View File
@@ -1423,6 +1423,28 @@
box-shadow: inset 3px 0 0 var(--se-palette-logic-not-accent), 0 4px 14px color-mix(in srgb, var(--se-palette-logic-not-accent) 22%, transparent);
}
/* ── START (시간봉 시작점) ── */
.se-palette-card--logic-start {
border-color: color-mix(in srgb, var(--se-palette-logic-start-accent) 52%, transparent);
background: var(--se-palette-logic-start-bg);
box-shadow: inset 3px 0 0 var(--se-palette-logic-start-accent);
}
.se-palette-card--selected.se-palette-card--logic-start {
outline-color: var(--se-palette-logic-start-accent);
}
.se-palette-card--logic-start .se-palette-card-icon {
background: color-mix(in srgb, var(--se-palette-logic-start-accent) 28%, transparent);
color: var(--se-palette-logic-start-accent);
font-weight: 900;
}
.se-palette-card--logic-start .se-palette-card-name {
color: var(--se-palette-logic-start-accent);
}
.se-palette-card--logic-start:hover {
border-color: color-mix(in srgb, var(--se-palette-logic-start-accent) 72%, transparent);
box-shadow: inset 3px 0 0 var(--se-palette-logic-start-accent), 0 4px 14px color-mix(in srgb, var(--se-palette-logic-start-accent) 28%, transparent);
}
/* ── 밴드 · 추세 ── */
.se-palette-card--band {
border-color: color-mix(in srgb, var(--se-palette-band-accent) 42%, transparent);
@@ -63,6 +63,8 @@
--se-palette-logic-or-bg: color-mix(in srgb, #22d3ee 14%, var(--bg2));
--se-palette-logic-not-accent: #fbbf24;
--se-palette-logic-not-bg: color-mix(in srgb, #fbbf24 14%, var(--bg2));
--se-palette-logic-start-accent: #ffb020;
--se-palette-logic-start-bg: color-mix(in srgb, #ffb020 22%, var(--bg2));
--se-palette-band-accent: #a78bfa;
--se-palette-band-bg: color-mix(in srgb, #a78bfa 14%, var(--bg2));
--se-palette-ind-accent: #34d399;
@@ -136,6 +138,8 @@
--se-palette-logic-or-bg: rgba(2, 136, 209, 0.08);
--se-palette-logic-not-accent: #f57f17;
--se-palette-logic-not-bg: rgba(245, 127, 23, 0.08);
--se-palette-logic-start-accent: #e65100;
--se-palette-logic-start-bg: rgba(255, 152, 0, 0.14);
--se-palette-band-accent: #7b1fa2;
--se-palette-band-bg: rgba(123, 31, 162, 0.07);
--se-palette-ind-accent: #2e7d32;
@@ -176,6 +180,8 @@
--se-palette-logic-or-bg: rgba(77, 208, 225, 0.12);
--se-palette-logic-not-accent: #ffd54f;
--se-palette-logic-not-bg: rgba(255, 213, 79, 0.12);
--se-palette-logic-start-accent: #ffb300;
--se-palette-logic-start-bg: rgba(255, 179, 0, 0.18);
--se-palette-band-accent: #ce93d8;
--se-palette-band-bg: rgba(206, 147, 216, 0.12);
--se-palette-ind-accent: #69f0ae;