가상매매 화면오류 수정
This commit is contained in:
@@ -178,6 +178,10 @@ interface TradingChartProps {
|
|||||||
custom1OverlayActive?: boolean;
|
custom1OverlayActive?: boolean;
|
||||||
onCustom1OverlayActiveChange?: (active: boolean) => void;
|
onCustom1OverlayActiveChange?: (active: boolean) => void;
|
||||||
onOpenCustom1OverlaySettings?: () => void;
|
onOpenCustom1OverlaySettings?: () => void;
|
||||||
|
/** 미니 차트 — plot 너비에 맞는 초기 x축 캔들 수 (미지정 시 DISPLAY_COUNT) */
|
||||||
|
resolveInitialDisplayCount?: (plotWidth: number) => number;
|
||||||
|
/** true — reload·layout 복구 타이머의 반복 viewport 재조정 생략 (미니 카드 깜빡임 방지) */
|
||||||
|
skipViewportRecovery?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TradingChart: React.FC<TradingChartProps> = ({
|
const TradingChart: React.FC<TradingChartProps> = ({
|
||||||
@@ -231,6 +235,8 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
custom1OverlayActive,
|
custom1OverlayActive,
|
||||||
onCustom1OverlayActiveChange,
|
onCustom1OverlayActiveChange,
|
||||||
onOpenCustom1OverlaySettings,
|
onOpenCustom1OverlaySettings,
|
||||||
|
resolveInitialDisplayCount,
|
||||||
|
skipViewportRecovery = false,
|
||||||
}) => {
|
}) => {
|
||||||
const effectiveShowChartRightToolbar = showChartRightToolbar ?? showPaneLegend;
|
const effectiveShowChartRightToolbar = showChartRightToolbar ?? showPaneLegend;
|
||||||
const showOverlayControlsOnly = !!(
|
const showOverlayControlsOnly = !!(
|
||||||
@@ -316,6 +322,17 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
const recoveryTimerRef = useRef<number | null>(null);
|
const recoveryTimerRef = useRef<number | null>(null);
|
||||||
const resizeDebounceRef = useRef<number | null>(null);
|
const resizeDebounceRef = useRef<number | null>(null);
|
||||||
const chartVisibleRef = useRef(chartVisible);
|
const chartVisibleRef = useRef(chartVisible);
|
||||||
|
const resolveInitialDisplayCountRef = useRef(resolveInitialDisplayCount);
|
||||||
|
const skipViewportRecoveryRef = useRef(skipViewportRecovery);
|
||||||
|
const lastViewportFitRef = useRef<{ count: number; width: number } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resolveInitialDisplayCountRef.current = resolveInitialDisplayCount;
|
||||||
|
}, [resolveInitialDisplayCount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
skipViewportRecoveryRef.current = skipViewportRecovery;
|
||||||
|
}, [skipViewportRecovery]);
|
||||||
|
|
||||||
const chartTimeFormat = useChartTimeFormat();
|
const chartTimeFormat = useChartTimeFormat();
|
||||||
const [chartMgr, setChartMgr] = useState<ChartManager | null>(null);
|
const [chartMgr, setChartMgr] = useState<ChartManager | null>(null);
|
||||||
@@ -548,15 +565,31 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
userDraggedRatioRef.current = null;
|
userDraggedRatioRef.current = null;
|
||||||
}, [paneAreaRatio]);
|
}, [paneAreaRatio]);
|
||||||
|
|
||||||
|
const getInitialDisplayCount = useCallback((): number => {
|
||||||
|
const plotW = containerRef.current?.clientWidth ?? wrapperRef.current?.clientWidth ?? 0;
|
||||||
|
const resolver = resolveInitialDisplayCountRef.current;
|
||||||
|
return resolver ? resolver(plotW) : DISPLAY_COUNT;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const applyInitialVisibleRange = useCallback((mgr: ChartManager) => {
|
||||||
|
const plotW = containerRef.current?.clientWidth ?? wrapperRef.current?.clientWidth ?? 0;
|
||||||
|
if (plotW <= 0) return;
|
||||||
|
const count = getInitialDisplayCount();
|
||||||
|
const prev = lastViewportFitRef.current;
|
||||||
|
if (prev && prev.count === count && Math.abs(prev.width - plotW) < 4) return;
|
||||||
|
lastViewportFitRef.current = { count, width: plotW };
|
||||||
|
const fb = mgr.hasIchimoku() ? 28 : 0;
|
||||||
|
mgr.setInitialVisibleRange(count, fb);
|
||||||
|
}, [getInitialDisplayCount]);
|
||||||
|
|
||||||
/** pane 레이아웃 + 가격·시간축 정상화 (느린 네트워크·레이아웃 지연 후 복구용) */
|
/** pane 레이아웃 + 가격·시간축 정상화 (느린 네트워크·레이아웃 지연 후 복구용) */
|
||||||
const normalizeChartViewport = useCallback((mgr: ChartManager) => {
|
const normalizeChartViewport = useCallback((mgr: ChartManager) => {
|
||||||
applyPaneLayout(mgr);
|
applyPaneLayout(mgr);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
try { mgr.autoScale(); } catch { /* ok */ }
|
try { mgr.autoScale(); } catch { /* ok */ }
|
||||||
const fb = mgr.hasIchimoku() ? 28 : 0;
|
applyInitialVisibleRange(mgr);
|
||||||
mgr.setInitialVisibleRange(DISPLAY_COUNT, fb);
|
|
||||||
});
|
});
|
||||||
}, [applyPaneLayout]);
|
}, [applyPaneLayout, applyInitialVisibleRange]);
|
||||||
|
|
||||||
/** 캔들 확대 보기 → 원복 직후 가격·시간축·pane 비율 정상화 */
|
/** 캔들 확대 보기 → 원복 직후 가격·시간축·pane 비율 정상화 */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -573,13 +606,12 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
const wrapperH = wrapperRef.current?.clientHeight ?? 0;
|
const wrapperH = wrapperRef.current?.clientHeight ?? 0;
|
||||||
m.restoreFromCandleFullscreen(wrapperH);
|
m.restoreFromCandleFullscreen(wrapperH);
|
||||||
applyPaneLayout(m);
|
applyPaneLayout(m);
|
||||||
const futureBars = m.hasIchimoku() ? 28 : 0;
|
applyInitialVisibleRange(m);
|
||||||
m.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
|
|
||||||
};
|
};
|
||||||
requestAnimationFrame(() => requestAnimationFrame(runRestore));
|
requestAnimationFrame(() => requestAnimationFrame(runRestore));
|
||||||
[200, 500, 1000].forEach(delay => setTimeout(runRestore, delay));
|
[200, 500, 1000].forEach(delay => setTimeout(runRestore, delay));
|
||||||
}
|
}
|
||||||
}, [candleOnlyMode, chartMgr, applyPaneLayout]);
|
}, [candleOnlyMode, chartMgr, applyPaneLayout, applyInitialVisibleRange]);
|
||||||
|
|
||||||
// ── 전체 재로드 (데이터 + 인디케이터) ─────────────────────────────────────
|
// ── 전체 재로드 (데이터 + 인디케이터) ─────────────────────────────────────
|
||||||
const reloadSafetyTimers = useRef<ReturnType<typeof setTimeout>[]>([]);
|
const reloadSafetyTimers = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||||
@@ -778,11 +810,10 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
applyPaneLayout(mgr);
|
applyPaneLayout(mgr);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const futureBars = mgr.hasIchimoku() ? 28 : 0;
|
applyInitialVisibleRange(mgr);
|
||||||
mgr.setInitialVisibleRange(DISPLAY_COUNT, futureBars);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [applyPaneLayout]);
|
}, [applyPaneLayout, applyInitialVisibleRange]);
|
||||||
|
|
||||||
const reloadAll = useCallback(async (
|
const reloadAll = useCallback(async (
|
||||||
mgr: ChartManager,
|
mgr: ChartManager,
|
||||||
@@ -887,6 +918,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
queueIndicatorRecovery();
|
queueIndicatorRecovery();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (skipViewportRecoveryRef.current) return;
|
||||||
const w = wrapperRef.current;
|
const w = wrapperRef.current;
|
||||||
if (!w || w.clientHeight < MIN_VALID_LAYOUT_HEIGHT) return;
|
if (!w || w.clientHeight < MIN_VALID_LAYOUT_HEIGHT) return;
|
||||||
normalizeChartViewport(m);
|
normalizeChartViewport(m);
|
||||||
@@ -1233,8 +1265,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
if (savedLR) {
|
if (savedLR) {
|
||||||
m.applyVisibleLogicalRange(savedLR.from, savedLR.to);
|
m.applyVisibleLogicalRange(savedLR.from, savedLR.to);
|
||||||
} else {
|
} else {
|
||||||
const fb = m.hasIchimoku() ? 28 : 0;
|
applyInitialVisibleRange(m);
|
||||||
m.setInitialVisibleRange(DISPLAY_COUNT, fb);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1528,10 +1559,11 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
const layoutRecoveryAttemptedRef = useRef(false);
|
const layoutRecoveryAttemptedRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
layoutRecoveryAttemptedRef.current = false;
|
layoutRecoveryAttemptedRef.current = false;
|
||||||
|
lastViewportFitRef.current = null;
|
||||||
}, [market, timeframe]);
|
}, [market, timeframe]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartVisible) return;
|
if (!chartVisible || skipViewportRecovery) return;
|
||||||
const mgr = managerRef.current;
|
const mgr = managerRef.current;
|
||||||
if (!mgr || !chartMgr || bars.length < 2) return;
|
if (!mgr || !chartMgr || bars.length < 2) return;
|
||||||
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
if (!canApplyChartBars(bars, barsMarket, market)) return;
|
||||||
@@ -1561,7 +1593,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return () => { timers.forEach(clearTimeout); };
|
return () => { timers.forEach(clearTimeout); };
|
||||||
}, [chartMgr, bars.length, barsMarket, market, chartVisible, normalizeChartViewport]);
|
}, [chartMgr, bars.length, barsMarket, market, chartVisible, normalizeChartViewport, skipViewportRecovery]);
|
||||||
|
|
||||||
// 데이터는 준비됐으나 메인 시리즈가 없는 빈 차트 자동 복구 (종목 전환·레이아웃 전환 직후)
|
// 데이터는 준비됐으나 메인 시리즈가 없는 빈 차트 자동 복구 (종목 전환·레이아웃 전환 직후)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -22,7 +22,15 @@ import {
|
|||||||
resolveStrategyTimeframes,
|
resolveStrategyTimeframes,
|
||||||
} from '../../utils/strategyToChartIndicators';
|
} from '../../utils/strategyToChartIndicators';
|
||||||
import { timeframeToCandleType } from '../../utils/chartCandleType';
|
import { timeframeToCandleType } from '../../utils/chartCandleType';
|
||||||
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
|
import { DISPLAY_COUNT } from '../../hooks/useUpbitData';
|
||||||
|
|
||||||
|
/** 미니 카드 폭에 맞춰 표시할 캔들 수 (좌우 빈 공간·x축 라벨 겹침 방지) */
|
||||||
|
const COMPACT_MIN_BAR_PX = 5;
|
||||||
|
|
||||||
|
function resolveCompactDisplayCount(width: number): number {
|
||||||
|
if (width <= 0) return DISPLAY_COUNT;
|
||||||
|
return Math.max(40, Math.min(DISPLAY_COUNT, Math.floor(width / COMPACT_MIN_BAR_PX)));
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
market: string;
|
market: string;
|
||||||
@@ -81,6 +89,11 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
[indicators, timeframe],
|
[indicators, timeframe],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const resolveInitialDisplayCount = useCallback(
|
||||||
|
(plotWidth: number) => resolveCompactDisplayCount(plotWidth),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const managerRef = useRef<ChartManager | null>(null);
|
const managerRef = useRef<ChartManager | null>(null);
|
||||||
const canvasWrapRef = useRef<HTMLDivElement | null>(null);
|
const canvasWrapRef = useRef<HTMLDivElement | null>(null);
|
||||||
const [chartReloadTick, setChartReloadTick] = useState(0);
|
const [chartReloadTick, setChartReloadTick] = useState(0);
|
||||||
@@ -106,9 +119,10 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
chartLiveReadyRef.current = true;
|
chartLiveReadyRef.current = true;
|
||||||
const pending = pendingBarRef.current;
|
const pending = pendingBarRef.current;
|
||||||
const mgr = managerRef.current;
|
const mgr = managerRef.current;
|
||||||
if (!pending || !mgr || barsMarketRef.current !== marketRef.current) return;
|
if (pending && mgr && barsMarketRef.current === marketRef.current) {
|
||||||
pendingBarRef.current = null;
|
pendingBarRef.current = null;
|
||||||
mgr.updateBar(pending);
|
mgr.updateBar(pending);
|
||||||
|
}
|
||||||
syncChartLayout();
|
syncChartLayout();
|
||||||
}, [syncChartLayout]);
|
}, [syncChartLayout]);
|
||||||
|
|
||||||
@@ -155,7 +169,6 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
|
|
||||||
const barsReady = bars.length >= 2 && barsMarket === market;
|
const barsReady = bars.length >= 2 && barsMarket === market;
|
||||||
|
|
||||||
|
|
||||||
const requestChartReload = useCallback(() => {
|
const requestChartReload = useCallback(() => {
|
||||||
chartReloadTriggeredRef.current = true;
|
chartReloadTriggeredRef.current = true;
|
||||||
setChartReloadTick(t => t + 1);
|
setChartReloadTick(t => t + 1);
|
||||||
@@ -171,36 +184,39 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
const wrap = canvasWrapRef.current;
|
const wrap = canvasWrapRef.current;
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
|
|
||||||
const timers = [200, 600, 1200].map(delay =>
|
let resizeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
const scheduleLayoutSync = () => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(() => syncChartLayout(), 120);
|
||||||
|
};
|
||||||
|
|
||||||
|
const timers = [600].map(delay =>
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!canvasWrapRef.current) return;
|
if (!canvasWrapRef.current) return;
|
||||||
if (managerRef.current?.hasMainSeries()) {
|
if (managerRef.current?.hasMainSeries()) {
|
||||||
syncChartLayout();
|
syncChartLayout();
|
||||||
} else if (!chartReloadTriggeredRef.current && delay >= 600 && barsReady) {
|
} else if (!chartReloadTriggeredRef.current && barsReady) {
|
||||||
requestChartReload();
|
requestChartReload();
|
||||||
}
|
}
|
||||||
}, delay),
|
}, delay),
|
||||||
);
|
);
|
||||||
|
|
||||||
const ro = new ResizeObserver(() => syncChartLayout());
|
const ro = new ResizeObserver(() => scheduleLayoutSync());
|
||||||
ro.observe(wrap);
|
ro.observe(wrap);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
timers.forEach(clearTimeout);
|
timers.forEach(clearTimeout);
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
};
|
};
|
||||||
}, [market, timeframe, barsReady, syncChartLayout, requestChartReload]);
|
}, [market, timeframe, barsReady, syncChartLayout, requestChartReload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!barsReady) return;
|
if (!barsReady) return;
|
||||||
if (managerRef.current?.hasMainSeries()) {
|
if (!managerRef.current?.hasMainSeries() && !chartReloadTriggeredRef.current) {
|
||||||
syncChartLayout();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!chartReloadTriggeredRef.current) {
|
|
||||||
requestChartReload();
|
requestChartReload();
|
||||||
}
|
}
|
||||||
}, [barsReady, syncChartLayout, requestChartReload]);
|
}, [barsReady, requestChartReload]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!streamActive) return;
|
if (!streamActive) return;
|
||||||
@@ -242,7 +258,7 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
<div className="vtd-card-chart-loading">캔들 데이터 수집 중…</div>
|
<div className="vtd-card-chart-loading">캔들 데이터 수집 중…</div>
|
||||||
)}
|
)}
|
||||||
<TradingChart
|
<TradingChart
|
||||||
key={`${market}-${timeframe}-${chartReloadTick}`}
|
key={`${market}-${timeframe}-${chartReloadTick}-${effectiveIndicators.map(i => i.id).join(',')}`}
|
||||||
chartVisible={chartStreamEnabled}
|
chartVisible={chartStreamEnabled}
|
||||||
bars={bars}
|
bars={bars}
|
||||||
barsMarket={barsMarket}
|
barsMarket={barsMarket}
|
||||||
@@ -261,6 +277,8 @@ const VirtualTargetCardChart: React.FC<Props> = ({
|
|||||||
onManagerReady={mgr => { managerRef.current = mgr; }}
|
onManagerReady={mgr => { managerRef.current = mgr; }}
|
||||||
onAddDrawing={noop as (d: Drawing) => void}
|
onAddDrawing={noop as (d: Drawing) => void}
|
||||||
onCandlesReady={handleCandlesReady}
|
onCandlesReady={handleCandlesReady}
|
||||||
|
resolveInitialDisplayCount={resolveInitialDisplayCount}
|
||||||
|
skipViewportRecovery
|
||||||
magnifierEnabled={false}
|
magnifierEnabled={false}
|
||||||
volumeVisible={false}
|
volumeVisible={false}
|
||||||
paneLayoutClamp
|
paneLayoutClamp
|
||||||
|
|||||||
Reference in New Issue
Block a user