전략편집기 시간봉 선택오류 수정
This commit is contained in:
@@ -206,6 +206,10 @@ export interface ChartSlotProps {
|
||||
chartLiveReceiveHighlight?: boolean;
|
||||
/** 실시간 차트 화면 표시 여부 (설정 등 다른 메뉴에서 숨김 시 false) */
|
||||
chartVisible?: boolean;
|
||||
/** 워크스페이스·지표 설정 반영 완료 — STOMP 시작 게이트 */
|
||||
chartConfigReady?: boolean;
|
||||
/** paint 완료 알림 (멀티차트 전체 로딩용) */
|
||||
onPaintReadyChange?: (slotIndex: number, ready: boolean) => void;
|
||||
/** pane 구분선 */
|
||||
chartPaneSeparator?: ChartPaneSeparatorOptions;
|
||||
/** 크로스헤어 이동 시 OHLC·보조지표 수치 표시 */
|
||||
@@ -241,6 +245,8 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
compactMode = false,
|
||||
chartLiveReceiveHighlight = true,
|
||||
chartVisible = true,
|
||||
chartConfigReady = true,
|
||||
onPaintReadyChange,
|
||||
chartPaneSeparator,
|
||||
chartCrosshairInfoVisible = true,
|
||||
chartHoverToolbarVisible = true,
|
||||
@@ -508,17 +514,6 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
// ※ TradingChart를 재마운트(key 변경)하면 managerRef가 초기화되어 MarketSearchPanel 등
|
||||
// 다른 상태에 영향을 줄 수 있으므로, 재마운트 대신 resetPaneHeights 재시도만 수행한다.
|
||||
const slotContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
// 데이터 로딩 완료 후 차트가 여전히 비어 있으면 1회만 재마운트 (data + blank guard)
|
||||
const [reloadTick, setReloadTick] = useState(0);
|
||||
const reloadTriggeredRef = useRef(false); // 재마운트는 최대 1회
|
||||
useLayoutEffect(() => {
|
||||
reloadTriggeredRef.current = false;
|
||||
chartLiveReadyRef.current = false;
|
||||
pendingRealtimeBarRef.current = null;
|
||||
pendingSyncVisibleRef.current = null;
|
||||
pendingSyncLogicalRef.current = null;
|
||||
skipSyncApplyRef.current = true;
|
||||
}, [symbol, timeframe]);
|
||||
useEffect(() => {
|
||||
const checks = [200, 500, 1000, 2000].map(delay =>
|
||||
setTimeout(() => {
|
||||
@@ -528,21 +523,13 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
const h = el.clientHeight;
|
||||
if (h <= 0) return;
|
||||
if (mgr.hasMainSeries()) {
|
||||
// 차트 데이터 있음 → pane 높이만 재적용
|
||||
mgr.resetPaneHeights(h);
|
||||
} else if (!reloadTriggeredRef.current) {
|
||||
// 차트 비어 있고 아직 재시도 안 했음 → 1회만 재마운트
|
||||
// (bars 로딩 지연일 수 있으므로, 늦은 타이머에서만 트리거)
|
||||
if (delay >= 1000) {
|
||||
reloadTriggeredRef.current = true;
|
||||
setReloadTick(t => t + 1);
|
||||
}
|
||||
}
|
||||
}, delay)
|
||||
);
|
||||
return () => checks.forEach(clearTimeout);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [symbol, timeframe]);
|
||||
|
||||
// ── 데이터 ────────────────────────────────────────────────────────────
|
||||
const useUpbit = isUpbitMarket(symbol);
|
||||
@@ -567,12 +554,26 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
|
||||
const handleCandlesReady = useCallback(() => {
|
||||
chartLiveReadyRef.current = true;
|
||||
onPaintReadyChange?.(slotIndex, true);
|
||||
const pending = pendingRealtimeBarRef.current;
|
||||
const mgr = managerRef.current;
|
||||
if (!pending || !mgr || barsMarketRef.current !== symbolRef.current) return;
|
||||
pendingRealtimeBarRef.current = null;
|
||||
mgr.updateBar(pending);
|
||||
}, []);
|
||||
}, [slotIndex, onPaintReadyChange]);
|
||||
|
||||
const handleSlotPaintReadyChange = useCallback((ready: boolean) => {
|
||||
onPaintReadyChange?.(slotIndex, ready);
|
||||
}, [slotIndex, onPaintReadyChange]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
chartLiveReadyRef.current = false;
|
||||
pendingRealtimeBarRef.current = null;
|
||||
pendingSyncVisibleRef.current = null;
|
||||
pendingSyncLogicalRef.current = null;
|
||||
skipSyncApplyRef.current = true;
|
||||
onPaintReadyChange?.(slotIndex, false);
|
||||
}, [symbol, timeframe, slotIndex, onPaintReadyChange]);
|
||||
|
||||
const handleTickUpdate = useCallback((bar: OHLCVBar) => {
|
||||
if (barsMarketRef.current !== symbolRef.current) return;
|
||||
@@ -604,15 +605,18 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
useMemo(() => ({
|
||||
onTickUpdate: handleTickUpdate,
|
||||
onNewCandle: handleNewCandle,
|
||||
enabled: useUpbit && chartVisible,
|
||||
enabled: useUpbit && chartVisible && chartConfigReady,
|
||||
source: chartRealtimeSource,
|
||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartVisible, chartRealtimeSource]),
|
||||
}), [handleTickUpdate, handleNewCandle, useUpbit, chartVisible, chartConfigReady, chartRealtimeSource]),
|
||||
);
|
||||
|
||||
const bars = useUpbit ? upbitBars : simBars;
|
||||
const barsMarket = useUpbit ? upbitBarsMarket : symbol;
|
||||
barsMarketRef.current = barsMarket;
|
||||
|
||||
const slotDataFetched = !useUpbit || (!isLoading && bars.length >= 2 && barsMarket === symbol);
|
||||
const slotChartLoading = useUpbit && chartVisible && (!chartConfigReady || !slotDataFetched);
|
||||
|
||||
const lastKnownBar = useUpbit
|
||||
? (latestBar ?? (bars.length > 0 ? bars[bars.length - 1] : null))
|
||||
: (bars.length > 0 ? bars[bars.length - 1] : null);
|
||||
@@ -966,15 +970,6 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
className={compactMode ? 'vtd-card-chart-canvas slot-chart-wrap slot-chart-wrap--compact' : 'slot-chart-wrap'}
|
||||
style={{ flex: 1, minHeight: 0, position: 'relative', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
{useUpbit && isLoading && (
|
||||
compactMode ? (
|
||||
<div className="vtd-card-chart-loading">차트 로딩…</div>
|
||||
) : (
|
||||
<div className="chart-loading" style={{ position: 'absolute', zIndex: 10 }}>
|
||||
<div className="loading-spinner" />
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{isLoadingMore && (
|
||||
<div className="chart-history-loading">
|
||||
<div className="loading-spinner" style={{ width: 14, height: 14 }} />
|
||||
@@ -983,8 +978,9 @@ const ChartSlot = forwardRef<ChartSlotHandle, ChartSlotProps>(function ChartSlot
|
||||
)}
|
||||
|
||||
<TradingChart
|
||||
key={reloadTick}
|
||||
chartVisible={chartVisible}
|
||||
dataLoading={slotChartLoading}
|
||||
onChartPaintReadyChange={handleSlotPaintReadyChange}
|
||||
bars={bars}
|
||||
barsMarket={barsMarket}
|
||||
market={symbol}
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
decodeConditionForEditor,
|
||||
alignBuySellStartCandleTypesForSave,
|
||||
encodeConditionForSave,
|
||||
syncBuySellPrimaryStartCandleTypes,
|
||||
mergeStartMetaForLoad,
|
||||
addExtraStartSection,
|
||||
hasMultipleStartSections,
|
||||
@@ -726,36 +725,8 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
||||
}, [setCurrentRoot, setCurrentLayout, scheduleStrategyPersist]);
|
||||
|
||||
const handleStartCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
|
||||
if (startId === START_NODE_ID) {
|
||||
const synced = syncBuySellPrimaryStartCandleTypes(buyEditorState, sellEditorState, candleTypes);
|
||||
setBuyCondition(synced.buy.root);
|
||||
setBuyLayout(prev => ({
|
||||
...prev,
|
||||
startMeta: synced.buy.startMeta,
|
||||
extraStartIds: synced.buy.extraStartIds,
|
||||
extraRoots: synced.buy.extraRoots,
|
||||
startCombineOp: normalizeStartCombineOp(synced.buy.startCombineOp),
|
||||
}));
|
||||
setSellCondition(synced.sell.root);
|
||||
setSellLayout(prev => ({
|
||||
...prev,
|
||||
startMeta: synced.sell.startMeta,
|
||||
extraStartIds: synced.sell.extraStartIds,
|
||||
extraRoots: synced.sell.extraRoots,
|
||||
startCombineOp: normalizeStartCombineOp(synced.sell.startCombineOp),
|
||||
}));
|
||||
scheduleStrategyPersist();
|
||||
return;
|
||||
}
|
||||
const state = signalTab === 'buy' ? buyEditorState : sellEditorState;
|
||||
handleEditorStateChange(updateStartCandleTypes(state, startId, candleTypes));
|
||||
}, [
|
||||
buyEditorState,
|
||||
sellEditorState,
|
||||
handleEditorStateChange,
|
||||
scheduleStrategyPersist,
|
||||
signalTab,
|
||||
]);
|
||||
handleEditorStateChange(updateStartCandleTypes(currentEditorState, startId, candleTypes));
|
||||
}, [handleEditorStateChange, currentEditorState]);
|
||||
|
||||
const handleStartCombineOpChange = useCallback((op: StartCombineOp) => {
|
||||
handleEditorStateChange(updateStartCombineOp(currentEditorState, op));
|
||||
|
||||
@@ -167,6 +167,8 @@ interface TradingChartProps {
|
||||
crosshairInfoVisible?: boolean;
|
||||
/** 외부 캔들 데이터 fetch 중 (실시간 차트 초기 로딩) */
|
||||
dataLoading?: boolean;
|
||||
/** chartPaintReady 변경 알림 (워크스페이스 전체 로딩 오버레이용) */
|
||||
onChartPaintReadyChange?: (ready: boolean) => void;
|
||||
/** 마운트 시 캔들 pane 전체보기(하단 보조지표 pane 숨김) — 가상매매 카드 등 */
|
||||
defaultCandleOnly?: boolean;
|
||||
/** 캔들 pane 좌하단 전체보기/복원 버튼 (기본 true) */
|
||||
@@ -236,6 +238,7 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
showCandleOverlayControls = false,
|
||||
crosshairInfoVisible = true,
|
||||
dataLoading = false,
|
||||
onChartPaintReadyChange,
|
||||
defaultCandleOnly = false,
|
||||
showCandlePaneControls = true,
|
||||
candleOverlayToggles,
|
||||
@@ -399,6 +402,14 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
chartVisibleRef.current = chartVisible;
|
||||
}, [chartVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dataLoading) setChartPaintReady(false);
|
||||
}, [dataLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
onChartPaintReadyChange?.(chartPaintReady && !dataLoading);
|
||||
}, [chartPaintReady, dataLoading, onChartPaintReadyChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
@@ -976,12 +987,18 @@ const TradingChart: React.FC<TradingChartProps> = ({
|
||||
} while (reloadCoalesceRef.current);
|
||||
} finally {
|
||||
reloadInFlightRef.current = false;
|
||||
if (managerRef.current?.hasMainSeries()) {
|
||||
setChartPaintReady(true);
|
||||
}
|
||||
if (reloadCoalesceRef.current) {
|
||||
reloadCoalesceRef.current = false;
|
||||
queueFullReload();
|
||||
return;
|
||||
}
|
||||
const m = managerRef.current;
|
||||
if (m?.hasMainSeries()) {
|
||||
const expected = countExpectedVisibleIndicators(indicatorsRef.current);
|
||||
const loaded = m.getLoadedIndicatorCount();
|
||||
if (expected === 0 || loaded >= expected) {
|
||||
setChartPaintReady(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [normalizeChartViewport, onCandlesReady, onDataLoaded, displayTimezone, timeframe, market, queueFullReload, queueIndicatorRecovery]);
|
||||
|
||||
Reference in New Issue
Block a user