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

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
+28 -4
View File
@@ -200,6 +200,9 @@ export interface ChartWorkspaceViewProps {
liveQuote: ReturnType<typeof import('../utils/chartLiveQuote').resolveChartLiveQuote>;
isLoading: boolean;
error: string | null;
chartConfigReady: boolean;
chartAreaLoading: boolean;
chartDataLoading: boolean;
bulkSettingsIndicators: IndicatorConfig[];
chartIndicatorsForSettings: IndicatorConfig[];
getParams: ReturnType<typeof import('../hooks/useIndicatorSettings').useIndicatorSettings>['getParams'];
@@ -298,6 +301,8 @@ export interface ChartWorkspaceViewProps {
handleSaveCustomOverlaySelection: (slot: ChartCustomOverlaySlotId, selection: ChartCustomOverlaySelection) => void;
syncCustomOverlaysToManager: (mgr: ChartManager) => void;
handleCandlesReady: () => void;
handleSingleChartPaintReadyChange: (ready: boolean) => void;
handleSlotPaintReadyChange: (slotIndex: number, ready: boolean) => void;
applyBacktestMarkersWhenReady: (payload: NonNullable<ChartWorkspaceViewProps['btMarkerPayloadRef']['current']>, attempt?: number) => void;
btRun: ReturnType<typeof import('../hooks/useBacktest').useBacktest>['run'];
refreshPaperAccount: (forMarket?: string) => Promise<void>;
@@ -339,7 +344,7 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
obWsStatus, spread, orderbookPrevClose, chartTicker, tradeFill,
paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled,
appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote,
isLoading, error, bulkSettingsIndicators, chartIndicatorsForSettings,
isLoading, error, chartConfigReady, chartAreaLoading, chartDataLoading, bulkSettingsIndicators, chartIndicatorsForSettings,
getParams, getVisualConfig, activeSlotIndicators,
setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid,
setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible,
@@ -368,7 +373,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
handleTogglePriceExtremeLabels,
handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange,
handleSaveCustomOverlaySelection, syncCustomOverlaysToManager,
handleCandlesReady, applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
handleCandlesReady, handleSingleChartPaintReadyChange, handleSlotPaintReadyChange,
applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers,
handleTimezoneChange,
} = props;
@@ -587,8 +593,15 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
gridTemplateColumns: layoutDef.cols,
gridTemplateRows: layoutDef.rows,
gridTemplateAreas: layoutDef.areas,
position: 'relative',
}}
>
{chartAreaLoading && (
<div className="chart-loading chart-loading--chart-area chart-loading--workspace" role="status" aria-live="polite">
<div className="loading-spinner" />
<span> ...</span>
</div>
)}
{/* 슬롯 0 = 메인 차트 영역 (a) */}
<div className="multi-slot-wrap" style={{ gridArea: 'a', minHeight: 0, minWidth: 0 }}>
<ChartSlot
@@ -629,6 +642,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
marketTickers={marketTickers}
compactMode
chartVisible={chartVisible}
chartConfigReady={chartConfigReady}
onPaintReadyChange={handleSlotPaintReadyChange}
/>
</div>
{/* 슬롯 1~7 */}
@@ -673,6 +688,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
marketTickers={marketTickers}
compactMode
chartVisible={chartVisible}
chartConfigReady={chartConfigReady}
onPaintReadyChange={handleSlotPaintReadyChange}
/>
</div>
))}
@@ -680,7 +697,13 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
)}
{/* ── 싱글 레이아웃 (기존) ─────────────────────────────────────── */}
<div className="tv-chart-col" style={{ display: layoutDef.count > 1 ? 'none' : undefined }}>
<div className="tv-chart-col" style={{ display: layoutDef.count > 1 ? 'none' : undefined, position: 'relative' }}>
{chartAreaLoading && (
<div className="chart-loading chart-loading--chart-area chart-loading--workspace" role="status" aria-live="polite">
<div className="loading-spinner" />
<span> ...</span>
</div>
)}
<ChartLegendBar
visibility={chartLegendOptions}
crosshairInfoVisible={chartCrosshairInfoVisible}
@@ -698,7 +721,8 @@ function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) {
<TradingChart
key={chartMountKey}
chartVisible={chartVisible}
dataLoading={useUpbit && isLoading}
dataLoading={chartDataLoading}
onChartPaintReadyChange={handleSingleChartPaintReadyChange}
bars={bars} barsMarket={barsMarket} market={symbol} timeframe={timeframe} displayTimezone={displayTimezone}
chartType={chartType} theme={theme} mode={mode}
indicators={effectiveIndicators} drawingTool={drawingTool}
+69 -11
View File
@@ -573,6 +573,13 @@ export function useChartWorkspace({
const [favoritesList, setFavoritesList] = useState<string[]>(getFavorites);
// DB 로드 완료 여부 (false → DB 로드 전, localStorage 기반 동작)
const [dbLoaded, setDbLoaded] = useState(false);
/** DB·앱설정·지표 기본값 반영 완료 — 이후 STOMP 초기 로딩 시작 (이중 페인트 방지) */
const [chartBootstrapComplete, setChartBootstrapComplete] = useState(false);
/** 싱글 레이아웃 차트 paint 완료 */
const [singleChartPaintReady, setSingleChartPaintReady] = useState(false);
/** 멀티 슬롯별 paint 완료 */
const slotPaintReadyRef = useRef<Record<number, boolean>>({});
const [slotPaintEpoch, setSlotPaintEpoch] = useState(0);
const priorityMarkets = useMemo(
() => [symbol, ...watchlist],
@@ -1111,6 +1118,7 @@ export function useChartWorkspace({
const handleCandlesReady = useCallback(() => {
chartLiveReadyRef.current = true;
setSingleChartPaintReady(true);
const pending = pendingRealtimeBarRef.current;
const mgr = managerRef.current;
if (!pending || !mgr || barsMarketRef.current !== chartContextRef.current.symbol) return;
@@ -1118,16 +1126,26 @@ export function useChartWorkspace({
mgr.updateBar(pending);
}, []);
const handleSingleChartPaintReadyChange = useCallback((ready: boolean) => {
setSingleChartPaintReady(ready);
}, []);
const handleSlotPaintReadyChange = useCallback((slotIndex: number, ready: boolean) => {
slotPaintReadyRef.current[slotIndex] = ready;
setSlotPaintEpoch(v => v + 1);
}, []);
const chartRealtimeSource = appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP';
const chartConfigReady = appSettingsLoaded && dbLoaded && chartBootstrapComplete;
const { bars: upbitBars, barsMarket: upbitBarsMarket, latestBar, wsStatus, isLoading, error } = useChartRealtimeData(
symbol,
timeframe,
useMemo(() => ({
onTickUpdate: handleTickUpdate,
onNewCandle: handleNewCandle,
enabled: chartScreenActive && useUpbit,
enabled: chartScreenActive && useUpbit && chartConfigReady,
source: chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartRealtimeSource]),
}), [handleTickUpdate, handleNewCandle, chartScreenActive, useUpbit, chartConfigReady, chartRealtimeSource]),
);
// 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
@@ -1167,8 +1185,15 @@ export function useChartWorkspace({
useLayoutEffect(() => {
chartLiveReadyRef.current = false;
pendingRealtimeBarRef.current = null;
setSingleChartPaintReady(false);
}, [symbol, timeframe]);
useEffect(() => {
slotPaintReadyRef.current = {};
setSlotPaintEpoch(v => v + 1);
setSingleChartPaintReady(false);
}, [layoutDef.count, sessionKey]);
// Upbit 초기 로딩 완료 시 stats 업데이트
useEffect(() => {
if (useUpbit && upbitBars.length > 0) {
@@ -1379,6 +1404,10 @@ export function useChartWorkspace({
});
}, [focusedIndicatorId, indicators, layoutDef.count]);
useEffect(() => {
setSingleChartPaintReady(false);
}, [effectiveIndicators]);
const handleExpandIndicator = useCallback((id: string) => {
setFocusedIndicatorId(id);
}, []);
@@ -1701,7 +1730,7 @@ export function useChartWorkspace({
const bulkSettingsIndicators = layoutDef.count > 1 ? activeSlotIndicators : indicators;
/** 설정 화면에서 DB 기본값 저장 후 차트 복귀 시 — 전역 기본값을 차트 인스턴스에 일괄 반영 */
const lastSyncedSettingsRevisionRef = useRef(indicatorSettingsRevision);
const lastSyncedSettingsRevisionRef = useRef(-1);
const syncChartsFromSavedDefaults = useCallback(() => {
const merge = (inds: IndicatorConfig[]) =>
mergeGlobalDefaultsOntoChartIndicators(inds, getParams, getVisualConfig);
@@ -1729,15 +1758,25 @@ export function useChartWorkspace({
}, [layoutDef.count, activeSlot, activeSlotIndicators, getParams, getVisualConfig]);
useLayoutEffect(() => {
if (!chartVisible) return;
if (indicatorSettingsRevision <= lastSyncedSettingsRevisionRef.current) return;
if (consumeSkipNextChartDefaultsSync()) {
lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
if (!appSettingsLoaded || !dbLoaded) {
setChartBootstrapComplete(false);
return;
}
syncChartsFromSavedDefaults();
lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
}, [chartVisible, indicatorSettingsRevision, syncChartsFromSavedDefaults]);
if (chartVisible && indicatorSettingsRevision > lastSyncedSettingsRevisionRef.current) {
if (consumeSkipNextChartDefaultsSync()) {
lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
} else {
syncChartsFromSavedDefaults();
lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
}
}
setChartBootstrapComplete(true);
}, [appSettingsLoaded, dbLoaded, chartVisible, indicatorSettingsRevision, syncChartsFromSavedDefaults]);
useEffect(() => {
setChartBootstrapComplete(false);
lastSyncedSettingsRevisionRef.current = -1;
}, [sessionKey]);
/** 보조지표 일괄 설정 적용 — 전체 지표 DB + 차트 인스턴스 병합 */
const handleBulkIndicatorApply = useCallback((result: {
@@ -1911,6 +1950,23 @@ export function useChartWorkspace({
settingsPageProps,
]);
const allSlotsPaintReady = useMemo(() => {
void slotPaintEpoch;
for (let i = 0; i < layoutDef.count; i++) {
if (!slotPaintReadyRef.current[i]) return false;
}
return layoutDef.count > 0;
}, [layoutDef.count, slotPaintEpoch]);
const chartDataFetched = !useUpbit || (!isLoading && bars.length >= 2 && barsMarket === symbol);
const chartPaintComplete = layoutDef.count > 1 ? allSlotsPaintReady : singleChartPaintReady;
const chartDataLoading = useUpbit && chartVisible && (!chartConfigReady || !chartDataFetched);
const chartAreaLoading = useUpbit && chartVisible && (
!chartConfigReady
|| !chartPaintComplete
|| (layoutDef.count === 1 && !chartDataFetched)
);
const viewProps: ChartWorkspaceViewProps = {
onOpenNotificationsPage,
onOpenAppDownload,
@@ -1946,6 +2002,7 @@ export function useChartWorkspace({
paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled,
appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote,
isLoading, error: error ?? null, bulkSettingsIndicators, chartIndicatorsForSettings,
chartConfigReady, chartAreaLoading, chartDataLoading,
getParams, getVisualConfig, activeSlotIndicators,
setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid,
setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible,
@@ -1974,7 +2031,8 @@ export function useChartWorkspace({
handleTogglePriceExtremeLabels,
handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange,
handleSaveCustomOverlaySelection, syncCustomOverlaysToManager,
handleCandlesReady, applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
handleCandlesReady, handleSingleChartPaintReadyChange, handleSlotPaintReadyChange,
applyBacktestMarkersWhenReady, btRun, refreshPaperAccount,
handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers,
goToMarketChart, handleTimezoneChange,
};