실시간 차트 갱신시 화면떨림 문제 해결
This commit is contained in:
@@ -8,6 +8,27 @@ import { DISPLAY_COUNT } from '../hooks/useUpbitData';
|
||||
/** Upbit 실시간 — 초기 히스토리가 이 개수 미만이면 setData 하지 않음 (종목 전환 직후 sparse 봉 → 캔들 폭 비정상) */
|
||||
const MIN_BARS_FOR_FULL_RELOAD = Math.min(50, Math.floor(DISPLAY_COUNT / 4));
|
||||
|
||||
function createIndicatorReloadCover(containerEl: HTMLDivElement | null): HTMLDivElement | null {
|
||||
if (!containerEl) return null;
|
||||
const cover = document.createElement('div');
|
||||
cover.style.cssText = [
|
||||
'position:absolute', 'inset:0', 'z-index:900',
|
||||
'pointer-events:none', 'opacity:1',
|
||||
'background:var(--bg,#131722)',
|
||||
].join(';');
|
||||
containerEl.parentElement?.appendChild(cover);
|
||||
return cover;
|
||||
}
|
||||
|
||||
function fadeOutIndicatorReloadCover(cover: HTMLDivElement | null): void {
|
||||
if (!cover) return;
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
cover.style.transition = 'opacity 0.25s ease';
|
||||
cover.style.opacity = '0';
|
||||
setTimeout(() => { cover.remove(); }, 300);
|
||||
}));
|
||||
}
|
||||
|
||||
/** 종목 전환 직후 stale/partial 데이터 차단 — barsMarket 확정 후 렌더 허용 */
|
||||
function canApplyChartBars(
|
||||
barData: OHLCVBar[],
|
||||
@@ -29,6 +50,7 @@ import ChartContextMenu from './ChartContextMenu';
|
||||
import CandlePaneControls from './CandlePaneControls';
|
||||
import { getKoreanName } from '../utils/marketNameCache';
|
||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../utils/timezone';
|
||||
import { classifyIndicatorChartChange, singleIndParamKey } from '../utils/indicatorChartSync';
|
||||
|
||||
interface TradingChartProps {
|
||||
bars: OHLCVBar[];
|
||||
@@ -102,6 +124,8 @@ interface TradingChartProps {
|
||||
showHoverToolbar?: boolean;
|
||||
/** 보조지표 우측 가격축 라벨·설명 (차트 설정, 기본 true) */
|
||||
seriesPriceLabelsEnabled?: boolean;
|
||||
/** 실시간 차트 화면 표시 여부 — false 이면 숨김 중 무거운 재로드 지연 */
|
||||
chartVisible?: boolean;
|
||||
}
|
||||
|
||||
const TradingChart: React.FC<TradingChartProps> = ({
|
||||
@@ -131,6 +155,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
volumeVisible = true,
|
||||
showHoverToolbar = true,
|
||||
seriesPriceLabelsEnabled = true,
|
||||
chartVisible = true,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const wrapperRef = useRef<HTMLDivElement>(null); // 스크롤 래퍼
|
||||
@@ -180,6 +205,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
const prevBarsKey = useRef<string>('');
|
||||
const prevIndKey = useRef<string>('');
|
||||
const prevSortedPKRef = useRef<string>(''); // 순서 무관 paramKey (reorder 감지용)
|
||||
const prevIndicatorsListRef = useRef<IndicatorConfig[]>(indicators);
|
||||
const indicatorSyncInFlightRef = useRef(false);
|
||||
const prevMarket = useRef<string>(market);
|
||||
const prevMarketTf = useRef<Timeframe>(timeframe);
|
||||
const lastAppliedMarketRef = useRef<string>('');
|
||||
@@ -188,6 +215,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
const prevLogScale = useRef<boolean>(logScale);
|
||||
const indicatorsRef = useRef(indicators);
|
||||
const recoveryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const resizeDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const chartVisibleRef = useRef(chartVisible);
|
||||
|
||||
const [chartMgr, setChartMgr] = useState<ChartManager | null>(null);
|
||||
/** 캔들 pane 전체보기 (오버레이 지표 유지, 하단 보조지표 pane 숨김) */
|
||||
@@ -205,6 +234,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
indicatorsRef.current = indicators;
|
||||
}, [indicators]);
|
||||
|
||||
useEffect(() => {
|
||||
chartVisibleRef.current = chartVisible;
|
||||
}, [chartVisible]);
|
||||
|
||||
const toggleCandleOnly = useCallback(() => {
|
||||
setCandleOnlyMode(v => !v);
|
||||
}, []);
|
||||
@@ -326,7 +359,71 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
inds: IndicatorConfig[],
|
||||
) => Promise<void>>(async () => {});
|
||||
|
||||
const syncIndicatorTrackingRefs = useCallback((inds: IndicatorConfig[]) => {
|
||||
prevIndKey.current = indKey(inds);
|
||||
prevSortedPKRef.current = sortedParamKey(inds);
|
||||
prevIndicatorsListRef.current = inds;
|
||||
}, []);
|
||||
|
||||
const restoreLogicalRange = useCallback((
|
||||
mgr: ChartManager,
|
||||
savedLR: { from: number; to: number } | null,
|
||||
) => {
|
||||
if (!savedLR) return;
|
||||
requestAnimationFrame(() => {
|
||||
mgr.applyVisibleLogicalRange(savedLR.from, savedLR.to);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const afterIndicatorPaneMutation = useCallback((mgr: ChartManager) => {
|
||||
if (candleOnlyModeRef.current) {
|
||||
mgr.applyCandleOnlyLayout(true, wrapperRef.current?.clientHeight);
|
||||
} else if (mgr.isCandleOnlyLayout()) {
|
||||
mgr.restoreFromCandleFullscreen(wrapperRef.current?.clientHeight);
|
||||
}
|
||||
applyPaneLayout(mgr);
|
||||
}, [applyPaneLayout]);
|
||||
|
||||
/** 누락된 지표만 추가 (전체 reloadIndicatorsOnly 회피) */
|
||||
const repairMissingIndicators = useCallback(async (
|
||||
mgr: ChartManager,
|
||||
inds: IndicatorConfig[],
|
||||
) => {
|
||||
const loadedIds = mgr.getLoadedIndicatorIds();
|
||||
const missing = inds.filter(i => i.hidden !== true && !loadedIds.has(i.id));
|
||||
if (missing.length === 0) return;
|
||||
indicatorSyncInFlightRef.current = true;
|
||||
try {
|
||||
await mgr.addIndicatorsBatch(missing);
|
||||
afterIndicatorPaneMutation(mgr);
|
||||
} finally {
|
||||
indicatorSyncInFlightRef.current = false;
|
||||
}
|
||||
}, [afterIndicatorPaneMutation]);
|
||||
|
||||
/** 지표만 재로드 — 커버 + 스크롤 위치 복원으로 깜빡임·떨림 완화 */
|
||||
const reloadIndicatorsWithCover = useCallback((
|
||||
mgr: ChartManager,
|
||||
inds: IndicatorConfig[],
|
||||
onComplete?: () => void,
|
||||
) => {
|
||||
const cover = createIndicatorReloadCover(containerRef.current);
|
||||
const savedLR = mgr.getVisibleLogicalRange();
|
||||
|
||||
indicatorSyncInFlightRef.current = true;
|
||||
void mgr.reloadIndicatorsOnly(inds).then(() => {
|
||||
afterIndicatorPaneMutation(mgr);
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
restoreLogicalRange(mgr, savedLR);
|
||||
fadeOutIndicatorReloadCover(cover);
|
||||
indicatorSyncInFlightRef.current = false;
|
||||
onComplete?.();
|
||||
}));
|
||||
});
|
||||
}, [applyPaneLayout, afterIndicatorPaneMutation, restoreLogicalRange]);
|
||||
|
||||
const queueFullReload = useCallback(() => {
|
||||
if (!chartVisibleRef.current) return;
|
||||
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
|
||||
recoveryTimerRef.current = window.setTimeout(() => {
|
||||
recoveryTimerRef.current = null;
|
||||
@@ -345,6 +442,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
}, [market, barsMarket]);
|
||||
|
||||
const queueIndicatorRecovery = useCallback(() => {
|
||||
if (!chartVisibleRef.current) return;
|
||||
if (reloadInFlightRef.current) return;
|
||||
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
|
||||
recoveryTimerRef.current = window.setTimeout(() => {
|
||||
@@ -365,10 +463,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
const ls = prevLogScale.current;
|
||||
|
||||
if (m.hasMainSeries()) {
|
||||
void m.reloadIndicatorsOnly(inds).then(() => {
|
||||
void repairMissingIndicators(m, inds).then(() => {
|
||||
if (m.getLoadedIndicatorCount() >= expected) {
|
||||
prevBarsKey.current = barsKey(barData, market);
|
||||
prevIndKey.current = indKey(inds);
|
||||
syncIndicatorTrackingRefs(inds);
|
||||
applyPaneLayout(m);
|
||||
return;
|
||||
}
|
||||
@@ -378,7 +476,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
}
|
||||
reloadAllRef.current?.(m, barData, ct, th, ls, inds);
|
||||
}, 120);
|
||||
}, [market, barsMarket, applyPaneLayout]);
|
||||
}, [market, barsMarket, applyPaneLayout, repairMissingIndicators, syncIndicatorTrackingRefs]);
|
||||
|
||||
/** setData 직후 캔들·볼륨 pane 레이아웃 확정 (지표 로드 중단 시에도 빈 화면 방지) */
|
||||
const commitCandleLayout = useCallback((mgr: ChartManager) => {
|
||||
@@ -474,8 +572,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
prevTheme.current = th;
|
||||
prevLogScale.current = ls;
|
||||
prevBarsKey.current = barsKey(barData, market);
|
||||
prevIndKey.current = indKey(inds);
|
||||
prevSortedPKRef.current = sortedParamKey(inds);
|
||||
syncIndicatorTrackingRefs(inds);
|
||||
lastAppliedMarketRef.current = market;
|
||||
|
||||
await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
@@ -768,33 +865,42 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
prevW = width; prevH = height;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
|
||||
const m = managerRef.current;
|
||||
if (!m) return;
|
||||
if (resizeDebounceRef.current) clearTimeout(resizeDebounceRef.current);
|
||||
resizeDebounceRef.current = window.setTimeout(() => {
|
||||
resizeDebounceRef.current = null;
|
||||
const m = managerRef.current;
|
||||
if (!m) return;
|
||||
|
||||
// 컨테이너가 처음으로 유효한 크기를 가질 때:
|
||||
// 데이터가 이미 barsRef에 있지만 차트에 아직 세팅되지 않은 경우 (예: 멀티레이아웃 초기 렌더) 재로드
|
||||
if (barsRef.current.length > 0 && !m.hasMainSeries()) {
|
||||
const bm = barsMarketRef.current;
|
||||
if (!canApplyChartBars(barsRef.current, bm, marketRef.current)) return;
|
||||
void reloadAll(
|
||||
m,
|
||||
barsRef.current,
|
||||
prevChartType.current,
|
||||
prevTheme.current,
|
||||
prevLogScale.current,
|
||||
indicatorsRef.current,
|
||||
);
|
||||
} else {
|
||||
// 컨테이너가 처음으로 유효한 크기를 가질 때:
|
||||
// 데이터가 이미 barsRef에 있지만 차트에 아직 세팅되지 않은 경우 (예: 멀티레이아웃 초기 렌더) 재로드
|
||||
if (barsRef.current.length > 0 && !m.hasMainSeries()) {
|
||||
const bm = barsMarketRef.current;
|
||||
if (!canApplyChartBars(barsRef.current, bm, marketRef.current)) return;
|
||||
void reloadAll(
|
||||
m,
|
||||
barsRef.current,
|
||||
prevChartType.current,
|
||||
prevTheme.current,
|
||||
prevLogScale.current,
|
||||
indicatorsRef.current,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const savedLR = wasHidden && m.hasMainSeries() ? m.getVisibleLogicalRange() : null;
|
||||
applyPaneLayout(m);
|
||||
// 멀티→단일 레이아웃 전환처럼 숨겨졌다가 다시 표시될 때
|
||||
// setInitialVisibleRange 를 재실행해 가시 범위(캔들 분포)를 정상화
|
||||
// 숨김→표시 전환: 줌/스크롤 위치 유지 (setInitialVisibleRange 로 초기화하면 화면이 튐)
|
||||
if (wasHidden && m.hasMainSeries()) {
|
||||
requestAnimationFrame(() => {
|
||||
const fb = m.hasIchimoku() ? 28 : 0;
|
||||
m.setInitialVisibleRange(DISPLAY_COUNT, fb);
|
||||
if (savedLR) {
|
||||
m.applyVisibleLogicalRange(savedLR.from, savedLR.to);
|
||||
} else {
|
||||
const fb = m.hasIchimoku() ? 28 : 0;
|
||||
m.setInitialVisibleRange(DISPLAY_COUNT, fb);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 80);
|
||||
});
|
||||
ro.observe(containerRef.current);
|
||||
|
||||
@@ -832,6 +938,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
reloadSafetyTimers.current.forEach(clearTimeout);
|
||||
reloadSafetyTimers.current = [];
|
||||
if (recoveryTimerRef.current) clearTimeout(recoveryTimerRef.current);
|
||||
if (resizeDebounceRef.current) clearTimeout(resizeDebounceRef.current);
|
||||
ro.disconnect();
|
||||
managerRef.current?.destroy();
|
||||
managerRef.current = null;
|
||||
@@ -851,6 +958,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
prevMarketTf.current = timeframe;
|
||||
prevBarsKey.current = '';
|
||||
prevIndKey.current = '';
|
||||
prevIndicatorsListRef.current = [];
|
||||
lastAppliedMarketRef.current = '';
|
||||
barsRef.current = [];
|
||||
reloadRetryRef.current = 0;
|
||||
@@ -869,6 +977,16 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
if (!mgr || !chartMgr || bars.length < 2) return;
|
||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||
|
||||
if (!chartVisible) {
|
||||
// 설정 등 다른 화면에서 숨김 중 — 키만 동기화해 복귀 시 불필요한 전체 재로드 방지
|
||||
prevBarsKey.current = barsKey(bars, market);
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
prevChartType.current = chartType;
|
||||
prevTheme.current = theme;
|
||||
prevLogScale.current = logScale;
|
||||
return;
|
||||
}
|
||||
|
||||
// manager 준비 전·종목 미적용 시 reloadAll 강제
|
||||
if (!mgr.hasMainSeries() || lastAppliedMarketRef.current !== market) {
|
||||
void reloadAll(mgr, bars, chartType, theme, logScale, indicators);
|
||||
@@ -896,91 +1014,105 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (barsChanged || ctChanged || indicatorsIncomplete) {
|
||||
// 데이터·차트 형식 변경 또는 지표 누락 → 전체 재로드
|
||||
if (barsChanged || ctChanged) {
|
||||
void reloadAll(mgr, bars, chartType, theme, logScale, indicators);
|
||||
} else if (indicatorsIncomplete) {
|
||||
// 누락 지표만 추가 (전체 제거·재추가로 화면이 비는 문제 방지)
|
||||
void repairMissingIndicators(mgr, indicators).then(() => {
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
prevBarsKey.current = barsKey(bars, market);
|
||||
});
|
||||
} else if (lsChanged) {
|
||||
prevLogScale.current = logScale;
|
||||
mgr.setLogScale(logScale);
|
||||
} else if (indChanged) {
|
||||
const prevList = prevIndicatorsListRef.current;
|
||||
const prevPK = prevIndKey.current.split('@@')[0] ?? '';
|
||||
const currPK = paramKey(indicators);
|
||||
const prevSK = prevIndKey.current.split('@@')[1] ?? '';
|
||||
const currSK = styleKey(indicators);
|
||||
|
||||
const needsRecalc = prevPK !== currPK;
|
||||
// 같은 지표 집합인데 순서만 바뀐 경우 (reorder-only)
|
||||
const isReorderOnly = needsRecalc
|
||||
&& sortedParamKey(indicators) === prevSortedPKRef.current
|
||||
&& sortedParamKey(indicators) === sortedParamKey(prevList)
|
||||
&& currSK === prevSK;
|
||||
|
||||
if (isReorderOnly) {
|
||||
// ── pane 순서 변경만: 메인 차트를 건드리지 않고 지표만 재배치 ──────────
|
||||
// 1) 지표 영역만 커버 (순간 숨김) → 2) 지표만 재추가 → 3) 서서히 공개
|
||||
prevSortedPKRef.current = sortedParamKey(indicators);
|
||||
prevIndKey.current = ik;
|
||||
const prevMap = new Map(prevList.map(i => [i.id, i]));
|
||||
const removedIds = prevList
|
||||
.filter(p => !indicators.some(c => c.id === p.id))
|
||||
.map(p => p.id);
|
||||
const addedConfigs = indicators.filter(c => !prevMap.has(c.id));
|
||||
const paramsChangedConfigs = indicators.filter(c => {
|
||||
const p = prevMap.get(c.id);
|
||||
if (!p) return false;
|
||||
return singleIndParamKey(p) !== singleIndParamKey(c);
|
||||
});
|
||||
|
||||
const containerEl = containerRef.current;
|
||||
// 지표 영역 위에 불투명 커버 div 삽입
|
||||
let cover: HTMLDivElement | null = null;
|
||||
if (containerEl) {
|
||||
cover = document.createElement('div');
|
||||
cover.style.cssText = [
|
||||
'position:absolute', 'inset:0', 'z-index:900',
|
||||
'pointer-events:none', 'opacity:1',
|
||||
'background:var(--bg,#131722)',
|
||||
].join(';');
|
||||
containerEl.parentElement?.appendChild(cover);
|
||||
}
|
||||
|
||||
mgr.reloadIndicatorsOnly(indicators).then(() => {
|
||||
if (candleOnlyModeRef.current) {
|
||||
mgr.applyCandleOnlyLayout(true, wrapperRef.current?.clientHeight);
|
||||
}
|
||||
applyPaneLayout(mgr);
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
if (cover) {
|
||||
cover.style.transition = 'opacity 0.25s ease';
|
||||
cover.style.opacity = '0';
|
||||
setTimeout(() => { cover?.remove(); }, 300);
|
||||
}
|
||||
}));
|
||||
});
|
||||
} else if (needsRecalc) {
|
||||
// ── 지표 추가·제거·파라미터 변경 → 지표만 재로드 (메인 캔들 유지) ──
|
||||
// reloadIndicatorsOnly 는 메인/볼륨 시리즈를 건드리지 않으므로
|
||||
// 차트 위치 초기화·깜빡임 없이 지표만 갱신된다.
|
||||
prevSortedPKRef.current = sortedParamKey(indicators);
|
||||
prevIndKey.current = ik;
|
||||
|
||||
// 현재 scroll/zoom 위치를 저장해 reload 후 복원
|
||||
const savedLR = mgr.getVisibleLogicalRange();
|
||||
|
||||
mgr.reloadIndicatorsOnly(indicators).then(() => {
|
||||
if (candleOnlyModeRef.current) {
|
||||
mgr.applyCandleOnlyLayout(true, wrapperRef.current?.clientHeight);
|
||||
} else if (mgr.isCandleOnlyLayout()) {
|
||||
mgr.restoreFromCandleFullscreen(wrapperRef.current?.clientHeight);
|
||||
}
|
||||
applyPaneLayout(mgr);
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
if (savedLR) mgr.applyVisibleLogicalRange(savedLR.from, savedLR.to);
|
||||
}));
|
||||
});
|
||||
} else if (prevSK !== currSK) {
|
||||
// ── 스타일만 변경 (색상·선폭·plotVisibility) → in-place 업데이트 ────
|
||||
// 시리즈를 제거·재생성하지 않으므로 pane 재번호 없음
|
||||
if (!needsRecalc && prevSK !== currSK) {
|
||||
for (const ind of indicators) {
|
||||
mgr.applyIndicatorStyle(ind);
|
||||
}
|
||||
prevIndKey.current = ik;
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
} else if (
|
||||
removedIds.length > 0
|
||||
&& addedConfigs.length === 0
|
||||
&& paramsChangedConfigs.length === 0
|
||||
) {
|
||||
const savedLR = mgr.getVisibleLogicalRange();
|
||||
indicatorSyncInFlightRef.current = true;
|
||||
mgr.removeIndicators(removedIds);
|
||||
afterIndicatorPaneMutation(mgr);
|
||||
restoreLogicalRange(mgr, savedLR);
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
indicatorSyncInFlightRef.current = false;
|
||||
void repairMissingIndicators(mgr, indicators);
|
||||
} else if (
|
||||
addedConfigs.length > 0
|
||||
&& removedIds.length === 0
|
||||
&& paramsChangedConfigs.length === 0
|
||||
) {
|
||||
const savedLR = mgr.getVisibleLogicalRange();
|
||||
indicatorSyncInFlightRef.current = true;
|
||||
void mgr.addIndicatorsBatch(addedConfigs).then(() => {
|
||||
afterIndicatorPaneMutation(mgr);
|
||||
restoreLogicalRange(mgr, savedLR);
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
indicatorSyncInFlightRef.current = false;
|
||||
});
|
||||
} else if (
|
||||
paramsChangedConfigs.length > 0
|
||||
&& removedIds.length === 0
|
||||
&& addedConfigs.length === 0
|
||||
) {
|
||||
const savedLR = mgr.getVisibleLogicalRange();
|
||||
indicatorSyncInFlightRef.current = true;
|
||||
void mgr.refreshIndicators(paramsChangedConfigs).then(() => {
|
||||
afterIndicatorPaneMutation(mgr);
|
||||
restoreLogicalRange(mgr, savedLR);
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
indicatorSyncInFlightRef.current = false;
|
||||
});
|
||||
} else if (isReorderOnly) {
|
||||
reloadIndicatorsWithCover(mgr, indicators, () => {
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
});
|
||||
} else {
|
||||
const change = classifyIndicatorChartChange(prevList, indicators);
|
||||
if (change.mode === 'full') {
|
||||
reloadIndicatorsWithCover(mgr, indicators, () => {
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
});
|
||||
} else {
|
||||
syncIndicatorTrackingRefs(indicators);
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators]);
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, chartVisible, reloadIndicatorsWithCover, afterIndicatorPaneMutation, restoreLogicalRange, syncIndicatorTrackingRefs, repairMissingIndicators]);
|
||||
|
||||
// 데이터는 준비됐으나 메인 시리즈가 없는 빈 차트 자동 복구 (종목 전환·레이아웃 전환 직후)
|
||||
useEffect(() => {
|
||||
if (!chartVisible) return;
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr || !chartMgr || bars.length < 2) return;
|
||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||
@@ -1004,10 +1136,11 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
);
|
||||
|
||||
return () => { timers.forEach(clearTimeout); };
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators]);
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, chartVisible]);
|
||||
|
||||
// 종목 전환 직후 reloadAll 이 중단되어 캔들만 남은 경우 자동 복구
|
||||
useEffect(() => {
|
||||
if (!chartVisible) return;
|
||||
const mgr = managerRef.current;
|
||||
if (!mgr || !chartMgr || bars.length < 2) return;
|
||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||
@@ -1018,19 +1151,22 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
|
||||
const timers = [120, 400, 900].map(delay =>
|
||||
window.setTimeout(() => {
|
||||
if (reloadInFlightRef.current) return;
|
||||
if (reloadInFlightRef.current || indicatorSyncInFlightRef.current) return;
|
||||
const m = managerRef.current;
|
||||
if (!m || !m.hasMainSeries()) return;
|
||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||
const exp = countExpectedVisibleIndicators(indicatorsRef.current);
|
||||
if (exp === 0 || m.getLoadedIndicatorCount() >= exp) return;
|
||||
prevBarsKey.current = '';
|
||||
queueIndicatorRecovery();
|
||||
void repairMissingIndicators(m, indicatorsRef.current).then(() => {
|
||||
if (m.getLoadedIndicatorCount() >= exp) return;
|
||||
prevBarsKey.current = '';
|
||||
queueIndicatorRecovery();
|
||||
});
|
||||
}, delay),
|
||||
);
|
||||
|
||||
return () => { timers.forEach(clearTimeout); };
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, queueIndicatorRecovery]);
|
||||
}, [chartMgr, bars, barsMarket, market, chartType, theme, logScale, indicators, chartVisible, queueIndicatorRecovery, repairMissingIndicators]);
|
||||
|
||||
// cursor 모드에서 드로잉 위에 있으면 pointer 커서로 변경
|
||||
const handleWrapperMouseMove = useCallback((e: React.PointerEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user