실시간 차트 갱신오류 수정
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user