전략편집기 시간봉 선택오류 수정

This commit is contained in:
Macbook
2026-06-12 21:32:19 +09:00
parent 046bcc6379
commit 6118230471
8 changed files with 164 additions and 97 deletions
+29 -33
View File
@@ -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}
+2 -31
View File
@@ -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));
+20 -3
View File
@@ -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]);