import '../styles/paperDashboard.css'; import '../styles/tradeRightPanel.css'; import React from 'react'; import DrawingToolbar from '../components/DrawingToolbar'; import BottomBar from '../components/BottomBar'; import TradingChart from '../components/TradingChart'; import StatsPanel from '../components/StatsPanel'; import WatchList from '../components/WatchList'; import AlertManager from '../components/AlertManager'; import ObjectTree from '../components/ObjectTree'; import IndicatorContextToolbar from '../components/IndicatorContextToolbar'; import IndicatorSettingsModal from '../components/IndicatorSettingsModal'; import IndicatorBulkSettingsModal from '../components/IndicatorBulkSettingsModal'; import MainChartSettingsModal from '../components/MainChartSettingsModal'; import DrawingContextToolbar from '../components/DrawingContextToolbar'; import DrawingSettingsModal from '../components/DrawingSettingsModal'; import FibTZSettingsModal from '../components/FibTZSettingsModal'; import ChartSlot, { type ChartSlotHandle } from '../components/ChartSlot'; import { type LayoutDef, type SyncOptions } from '../utils/layoutTypes'; import type { OHLCVBar, ChartType, Theme, Timeframe, IndicatorConfig, LegendData, Drawing, ChartMode, MainChartStyle, TradeOrderFillRequest, } from '../types'; import type { ChartManager } from '../utils/ChartManager'; import { LOAD_MORE_TRIGGER } from '../hooks/useHistoryLoader'; import type { WsStatus } from '../hooks/useChartRealtimeData'; import type { TickerData } from '../hooks/useMarketTicker'; import type { OrderbookState } from '../hooks/useUpbitOrderbook'; import type { BacktestStats } from '../utils/backendApi'; import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal'; import BacktestAnalysisReportModal from '../components/backtest/BacktestAnalysisReportModal'; import { type MenuPage } from '../components/TopMenuBar'; import BacktestPanel from '../components/BacktestPanel'; import LiveStrategyPanel from '../components/LiveStrategyPanel'; import BacktestSettingsModal from '../components/BacktestSettingsModal'; import MarketPanel from '../components/MarketPanel'; import ChartLegendBar from '../components/ChartLegendBar'; import RightSidePanel from '../components/RightSidePanel'; import ChartCustomOverlaySettingsModal from '../components/ChartCustomOverlaySettingsModal'; import MobileChartDock from '../components/MobileChartDock'; import { ChartToolbarNotify } from '../components/NotifyUiBindings'; import { buildMainIndicatorConfig, indicatorSettingsTemplateId, indicatorTypeFromSettingsId, isIndicatorSettingsTemplateId, } from '../utils/indicatorMainConfig'; import { initializeIndicatorConfigForEditor } from '../utils/indicatorSettingsEditor'; import type { PriceStats } from '../utils/calculations'; import type { ChartOverlayToggleKey, ChartOverlayVisibility } from '../utils/chartOverlayVisibility'; import type { ChartCustomOverlaySelection, ChartCustomOverlaySlotId } from '../utils/chartCustomOverlay'; import type { ChartLegendVisibility } from '../types/chartLegend'; import type { ChartPaneSeparatorOptions } from '../types/chartPaneSeparator'; import type { LiveStrategySettingsDto } from '../utils/backendApi'; import type { BacktestSettingsDto } from '../utils/backendApi'; import { addWatchlistItem, removeWatchlistItem } from '../utils/backendApi'; import { fmtListTimestamp } from '../utils/backtestUiUtils'; export interface ChartWorkspaceViewProps { onOpenNotificationsPage: () => void; symbol: string; timeframe: Timeframe; chartType: ChartType; theme: Theme; mode: ChartMode; logScale: boolean; showGrid: boolean; toolbarActiveIndicators: IndicatorConfig[]; layoutDef: LayoutDef; syncOptions: SyncOptions; activeSlot: number; activeSlotSymbol: string; activeSlotTf: Timeframe; slot0InitProps: { symbol: string; timeframe: Timeframe; indicators: IndicatorConfig[] } | null; slotInitProps: Record; syncTime: number; syncVisibleRange: { from: number; to: number } | null; syncLogicalRange: { from: number; to: number } | null; slotRefs: React.MutableRefObject<(ChartSlotHandle | null)[]>; managerRef: React.MutableRefObject; undoStack: React.MutableRefObject; redoStack: React.MutableRefObject; canUndo: boolean; canRedo: boolean; drawingTool: string; drawings: Drawing[]; drawingsLocked: boolean; drawingsVisible: boolean; indicatorsVisible: boolean; magnetMode: 'off' | 'weak' | 'strong'; snapToIndicator: boolean; keepLockedOnDelete: boolean; legend: LegendData | null; showStats: boolean; showWatch: boolean; showAlert: boolean; showObjectTree: boolean; percentScale: boolean; magnifierActive: boolean; showMarketPanel: boolean; mobileRightOpen: boolean; mobileRightTab: 'trade' | 'orderbook'; marketTickers: Map; marketInfos: import('../hooks/useMarketTicker').MarketInfo[]; marketLoading: boolean; usdRate: number; refreshAllTickers: () => Promise; setMarketSearchOpen: React.Dispatch>; formalLogin: boolean; requireFormalLogin: () => void; chartVisible: boolean; menuPage: MenuPage; isMobile: boolean; indicators: IndicatorConfig[]; effectiveIndicators: IndicatorConfig[]; mainChartStyle: MainChartStyle; showMainChartModal: boolean; showBulkIndSettings: boolean; settingsModalId: string | null; ctxToolbar: { entryId: string; screenX: number; screenY: number } | null; drawingCtxToolbar: { id: string; screenX: number; screenY: number } | null; drawingSettingsId: string | null; selectedDrawingId: string | null; focusedIndicatorId: string | null; overlayVisibility: ChartOverlayVisibility; overlayVisibilityRef: React.MutableRefObject; candleOverlayToggles: ReturnType; customOverlaySelection: ChartCustomOverlaySelection; custom1OverlaySelection: ChartCustomOverlaySelection; activeCustomOverlaySlot: ChartCustomOverlaySlotId | null; customOverlaySettingsSlot: ChartCustomOverlaySlotId | null; customOverlayActive: boolean; custom1OverlayActive: boolean; chartCandleAreaPriceLabels: boolean; chartSeriesPriceLabels: boolean; chartVolumeVisible: boolean; chartPriceExtremeLabelsVisible: boolean; chartPaneSeparator: ChartPaneSeparatorOptions; chartCrosshairInfoVisible: boolean; chartHoverToolbarVisible: boolean; chartLiveReceiveHighlight: boolean; chartRealtimeSource: 'BACKEND_STOMP' | 'UPBIT_DIRECT'; displayTimezone: string; chartTimeFormat: string; chartLegendOptions: ChartLegendVisibility; useUpbit: boolean; wsStatus: WsStatus; bars: OHLCVBar[]; barsMarket: string; barsMarketRef: React.MutableRefObject; chartMountKey: number; chartLiveReadyRef: React.MutableRefObject; pendingRealtimeBarRef: React.MutableRefObject; isSingleLoadingMore: boolean; singleLoadMoreRef: React.MutableRefObject<() => void>; btMatchesChart: boolean; btRunning: boolean; btSettings: BacktestSettingsDto; btStats: BacktestStats | null; btSignals: import('../utils/backendApi').BacktestSignal[]; btClear: () => void; btMarkerPayloadRef: React.MutableRefObject<{ symbol: string; timeframe: string; signals: import('../utils/backendApi').BacktestSignal[]; showPrice: boolean; } | null>; syncBacktestMarkersRef: React.MutableRefObject<() => void>; showBtSettings: boolean; showBtResult: boolean; showLivePanel: boolean; liveStrategies: { id: number; name: string }[]; liveStrategySettings: LiveStrategySettingsDto; monitoredMarkets: string[]; isVirtualActive: boolean; watchlist: string[]; chartBtReportModel: BacktestAnalysisReportModel | null; chartSymbol: string; chartScreenActive: boolean; orderbook: OrderbookState; obWsStatus: WsStatus; spread: { bestAsk: number; bestBid: number; spread: number; pct: number }; orderbookPrevClose: number; chartTicker: TickerData | undefined; tradeFill: TradeOrderFillRequest | null; paperCashBalance: number; paperCoinQty: number; paperTradingEnabled: boolean; paperAutoTradeEnabled: boolean; appDefaults: import('../utils/backendApi').AppSettingsDto; stats: PriceStats | null; priceMap: Record; alerts: Array<{ price: number; label: string }>; hoverBar: LegendData | OHLCVBar | null; quoteBar: OHLCVBar | null; liveQuote: ReturnType; isLoading: boolean; error: string | null; chartConfigReady: boolean; chartAreaLoading: boolean; chartDataLoading: boolean; bulkSettingsIndicators: IndicatorConfig[]; chartIndicatorsForSettings: IndicatorConfig[]; getParams: ReturnType['getParams']; getVisualConfig: ReturnType['getVisualConfig']; activeSlotIndicators: IndicatorConfig[]; setSymbol: React.Dispatch>; setLegend: React.Dispatch>; setChartType: React.Dispatch>; setMode: React.Dispatch>; setLogScale: React.Dispatch>; setShowGrid: React.Dispatch>; setDrawingTool: React.Dispatch>; setDrawings: React.Dispatch>; setDrawingsLocked: React.Dispatch>; setDrawingsVisible: React.Dispatch>; setIndicatorsVisible: React.Dispatch>; setMagnetMode: React.Dispatch>; setSnapToIndicator: React.Dispatch>; setKeepLockedOnDelete: React.Dispatch>; setShowStats: React.Dispatch>; setShowWatch: React.Dispatch>; setShowAlert: React.Dispatch>; setShowObjectTree: React.Dispatch>; setMagnifierActive: React.Dispatch>; setShowMarketPanel: React.Dispatch>; setMobileRightOpen: React.Dispatch>; setMobileRightTab: React.Dispatch>; setSyncTime: React.Dispatch>; setSyncVisibleRange: React.Dispatch>; setSyncLogicalRange: React.Dispatch>; setActiveSlotSymbol: React.Dispatch>; setCanUndo: React.Dispatch>; setCanRedo: React.Dispatch>; setCtxToolbar: React.Dispatch>; setDrawingCtxToolbar: React.Dispatch>; setDrawingSettingsId: React.Dispatch>; setSelectedDrawingId: React.Dispatch>; setShowMainChartModal: React.Dispatch>; setShowBulkIndSettings: React.Dispatch>; setSettingsModalId: React.Dispatch>; setCustomOverlaySettingsSlot: React.Dispatch>; setShowBtSettings: React.Dispatch>; setShowBtResult: React.Dispatch>; setShowLivePanel: React.Dispatch>; setWatchlist: React.Dispatch>; setAlerts: React.Dispatch>>; setIndicators: React.Dispatch>; setSyncOptions: React.Dispatch>; setPercentScale: React.Dispatch>; setBtStrategyName: React.Dispatch>; setBtReportCreatedAt: React.Dispatch>; setBtSettings: React.Dispatch>; saveAppDef: (patch: Partial) => void; applyChartIndicators: (chartIndicators: IndicatorConfig[]) => void; onThemeToggle: () => void; handleTimeframe: (tf: Timeframe) => void; handleLayoutSelect: (def: LayoutDef) => void; handleSetActiveSlot: (idx: number) => void; handleActiveSlotIndicatorsChange: (inds: IndicatorConfig[]) => void; handleActiveSlotSymbolChange: (sym: string) => void; handleVisibleRangeChange: (r: { from: number; to: number }) => void; handleLogicalRangeChange: (r: { from: number; to: number }) => void; handleSlotFullView: (slotSymbol: string, slotIdx: number) => void; applyTradeFill: (req: { market: string; price: number; side: 'buy' | 'sell' }, slotIndex?: number) => void; handleOrderbookPick: (price: number, rowType: 'ask' | 'bid') => void; handleTradeMarketSelect: (s: string) => void; pushDrawing: (newDrawings: Drawing[]) => void; handleUndo: () => void; handleRedo: () => void; handleHoverPaneLegend: (id: string, sx: number, sy: number) => void; handleLeavePaneLegend: () => void; clearHoverTimer: () => void; handleDrawingClick: (id: string | null, sx: number, sy: number) => void; handleDrawingDoubleClick: (id: string, sx: number, sy: number) => void; handleMainChartSave: (style: MainChartStyle) => void; handleAddIndicator: (type: string, fromConfig?: IndicatorConfig) => void; handleAddIndicators: (types: string[]) => void; handleRemoveByType: (type: string) => void; handleIndicatorListOrderChange: (orderedTypes: string[]) => void; handleOpenIndicatorSettings: (type: string) => void; handleIndicatorSave: (updated: IndicatorConfig) => void; handleBulkIndicatorApply: (result: { chartIndicators: IndicatorConfig[]; allConfigs: IndicatorConfig[] }) => void; handleToggleIndicatorHidden: (id: string) => void; handleDuplicateIndicator: (sourceId: string) => void; handleRemoveIndicatorById: (id: string) => void; handleScreenshot: () => void; handleReorderIndicators: (fromId: string, insertBeforeId: string | null) => void; handleMergeIndicators: (fromId: string, intoId: string) => void; handleSplitIndicatorPane: (hostId: string) => void; handleExpandIndicator: (id: string) => void; handleRestoreIndicators: () => void; handleToggleCandleOverlay: (key: ChartOverlayToggleKey) => void; handleTogglePriceExtremeLabels: () => void; handleCustomOverlayActiveChange: (active: boolean) => void; handleCustom1OverlayActiveChange: (active: boolean) => void; 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, attempt?: number) => void; btRun: ReturnType['run']; refreshPaperAccount: (forMarket?: string) => Promise; handlePaperOrderFilled: () => void; handleLiveSettingsChange: (saved: LiveStrategySettingsDto) => void; clearLiveMarkers: () => void; goToMarketChart: (market: string) => void; handleTimezoneChange: (tz: string) => void; } function ChartWorkspaceViewInner(props: ChartWorkspaceViewProps) { const { onOpenNotificationsPage, symbol, timeframe, chartType, theme, mode, logScale, showGrid, toolbarActiveIndicators, layoutDef, syncOptions, activeSlot, activeSlotSymbol, activeSlotTf, slot0InitProps, slotInitProps, syncTime, syncVisibleRange, syncLogicalRange, slotRefs, managerRef, undoStack, redoStack, canUndo, canRedo, drawingTool, drawings, drawingsLocked, drawingsVisible, indicatorsVisible, magnetMode, snapToIndicator, keepLockedOnDelete, legend, showStats, showWatch, showAlert, showObjectTree, percentScale, magnifierActive, showMarketPanel, mobileRightOpen, mobileRightTab, marketTickers, marketInfos, marketLoading, usdRate, refreshAllTickers, setMarketSearchOpen, formalLogin, requireFormalLogin, chartVisible, menuPage, isMobile, indicators, effectiveIndicators, mainChartStyle, showMainChartModal, showBulkIndSettings, settingsModalId, ctxToolbar, drawingCtxToolbar, drawingSettingsId, selectedDrawingId, focusedIndicatorId, overlayVisibilityRef, candleOverlayToggles, customOverlaySelection, custom1OverlaySelection, customOverlaySettingsSlot, customOverlayActive, custom1OverlayActive, chartCandleAreaPriceLabels, chartSeriesPriceLabels, chartVolumeVisible, chartPriceExtremeLabelsVisible, chartPaneSeparator, chartCrosshairInfoVisible, chartHoverToolbarVisible, chartLiveReceiveHighlight, chartRealtimeSource, displayTimezone, chartTimeFormat, chartLegendOptions, useUpbit, wsStatus, bars, barsMarket, barsMarketRef, chartMountKey, chartLiveReadyRef, pendingRealtimeBarRef, isSingleLoadingMore, singleLoadMoreRef, btMatchesChart, btRunning, btSettings, btStats, btSignals, btClear, btMarkerPayloadRef, syncBacktestMarkersRef, showBtSettings, showBtResult, showLivePanel, liveStrategies, liveStrategySettings, monitoredMarkets, isVirtualActive, watchlist, chartBtReportModel, chartSymbol, chartScreenActive, orderbook, obWsStatus, spread, orderbookPrevClose, chartTicker, tradeFill, paperCashBalance, paperCoinQty, paperTradingEnabled, paperAutoTradeEnabled, appDefaults, stats, priceMap, alerts, hoverBar, quoteBar, liveQuote, isLoading, error, chartConfigReady, chartAreaLoading, chartDataLoading, bulkSettingsIndicators, chartIndicatorsForSettings, getParams, getVisualConfig, activeSlotIndicators, setSymbol, setLegend, setChartType, setMode, setLogScale, setShowGrid, setDrawingTool, setDrawings, setDrawingsLocked, setDrawingsVisible, setIndicatorsVisible, setMagnetMode, setSnapToIndicator, setKeepLockedOnDelete, setShowStats, setShowWatch, setShowAlert, setShowObjectTree, setMagnifierActive, setShowMarketPanel, setMobileRightOpen, setMobileRightTab, setSyncTime, setSyncVisibleRange, setSyncLogicalRange, setActiveSlotSymbol, setCanUndo, setCanRedo, setCtxToolbar, setDrawingCtxToolbar, setDrawingSettingsId, setSelectedDrawingId, setShowMainChartModal, setShowBulkIndSettings, setSettingsModalId, setCustomOverlaySettingsSlot, setShowBtSettings, setShowBtResult, setShowLivePanel, setWatchlist, setAlerts, setIndicators, setSyncOptions, setPercentScale, setBtStrategyName, setBtReportCreatedAt, setBtSettings, saveAppDef, applyChartIndicators, onThemeToggle, handleTimeframe, handleLayoutSelect, handleSetActiveSlot, handleActiveSlotIndicatorsChange, handleActiveSlotSymbolChange, handleVisibleRangeChange, handleLogicalRangeChange, handleSlotFullView, applyTradeFill, handleOrderbookPick, handleTradeMarketSelect, pushDrawing, handleUndo, handleRedo, handleHoverPaneLegend, handleLeavePaneLegend, clearHoverTimer, handleDrawingClick, handleDrawingDoubleClick, handleMainChartSave, handleAddIndicator, handleAddIndicators, handleRemoveByType, handleIndicatorListOrderChange, handleOpenIndicatorSettings, handleIndicatorSave, handleBulkIndicatorApply, handleToggleIndicatorHidden, handleDuplicateIndicator, handleRemoveIndicatorById, handleScreenshot, handleReorderIndicators, handleMergeIndicators, handleSplitIndicatorPane, handleExpandIndicator, handleRestoreIndicators, handleToggleCandleOverlay, handleTogglePriceExtremeLabels, handleCustomOverlayActiveChange, handleCustom1OverlayActiveChange, handleSaveCustomOverlaySelection, syncCustomOverlaysToManager, handleCandlesReady, handleSingleChartPaintReadyChange, handleSlotPaintReadyChange, applyBacktestMarkersWhenReady, btRun, refreshPaperAccount, handlePaperOrderFilled, handleLiveSettingsChange, clearLiveMarkers, handleTimezoneChange, } = props; return ( <> 1 ? activeSlotTf : timeframe} chartType={chartType} theme={theme} mode={mode} logScale={logScale} showGrid={showGrid} activeIndicators={toolbarActiveIndicators} onSymbol={s => { if (layoutDef.count > 1) { // 멀티 레이아웃: 활성 슬롯의 심볼 변경 slotRefs.current[activeSlot]?.setSymbol(s); // symbol sync 활성화된 경우 모든 슬롯 일괄 변경 if (syncOptions.symbol) { slotRefs.current.slice(0, layoutDef.count).forEach(r => r?.setSymbol(s)); } } else { setSymbol(s); setLegend(null); } }} onTimeframe={handleTimeframe} onChartType={setChartType} onTheme={onThemeToggle} onMode={() => setMode(m => m === 'chart' ? 'trading' : 'chart')} onLogScale={() => setLogScale(v => !v)} onToggleGrid={() => setShowGrid(v => !v)} onFitContent={() => managerRef.current?.fitContent()} onScrollToNow={() => managerRef.current?.scrollToRealTime()} onAddIndicator={handleAddIndicator} onAddIndicators={handleAddIndicators} onIndicatorOrderChange={handleIndicatorListOrderChange} onRemoveByType={handleRemoveByType} onOpenBulkIndicatorSettings={() => setShowBulkIndSettings(true)} onOpenIndicatorSettings={handleOpenIndicatorSettings} onAutoFib={() => managerRef.current?.drawAutoFib(80)} onClearFib={() => managerRef.current?.clearFib()} onScreenshot={handleScreenshot} onToggleStats={() => setShowStats(v => !v)} onToggleWatch={() => setShowWatch(v => !v)} onToggleAlert={() => setShowAlert(v => !v)} magnetMode={magnetMode} onToggleMagnet={() => setMagnetMode(m => m === 'off' ? 'strong' : 'off')} onUndo={handleUndo} onRedo={handleRedo} canUndo={canUndo} canRedo={canRedo} onClearDrawings={() => { // 모든 드로잉 삭제 + 피보나치 삭제 + undo/redo 스택 초기화 undoStack.current = []; redoStack.current = []; setCanUndo(false); setCanRedo(false); setDrawings([]); managerRef.current?.clearFib(); managerRef.current?.clearDrawings(); }} layoutId={layoutDef.id} syncOptions={syncOptions} onLayoutSelect={handleLayoutSelect} onSyncChange={(key, val) => { setSyncOptions(prev => { const next = { ...prev, [key]: val }; saveAppDef({ syncOptions: next as unknown as Record }); return next; }); }} magnifierActive={magnifierActive} onToggleMagnifier={() => setMagnifierActive(v => !v)} marketPanelActive={showMarketPanel} onToggleMarketPanel={() => { setShowMarketPanel(v => { const next = !v; if (isMobile && next) setMobileRightOpen(false); return next; }); }} marketTickers={marketTickers} marketInfos={marketInfos} marketLoading={marketLoading} refreshAllTickers={refreshAllTickers} onMarketSearchOpenChange={setMarketSearchOpen} backtestActive={btMatchesChart} backtestRunning={btRunning} backtestRunningId={undefined} onRunBacktest={menuPage === 'chart' && layoutDef.count === 1 && formalLogin ? async (opts) => { if (opts.strategyName) setBtStrategyName(opts.strategyName); if (useUpbit && (isLoading || bars.length === 0)) return; const runSymbol = symbol; const runTf = timeframe; const r = await btRun({ ...opts, bars, timeframe: runTf, settings: btSettings, symbol: runSymbol, }); if ( r && r.symbol === runSymbol && r.timeframe === runTf && r.signals.length > 0 ) { const showPrice = appDefaults.btShowPrice ?? false; const payload = { symbol: runSymbol, timeframe: runTf, signals: r.signals, showPrice, }; btMarkerPayloadRef.current = payload; applyBacktestMarkersWhenReady(payload); setBtReportCreatedAt(fmtListTimestamp(new Date().toISOString())); } if (appDefaults.btAutoPopup) setShowBtResult(true); } : undefined} onOpenBtSettings={() => { if (!formalLogin) { requireFormalLogin(); return; } setShowBtSettings(true); }} onOpenBtResults={() => { if (!formalLogin) { requireFormalLogin(); return; } setShowBtResult(true); }} hasBtResult={btMatchesChart} onClearBtMarkers={() => { btMarkerPayloadRef.current = null; btClear(); managerRef.current?.clearBacktestMarkers(); }} onToggleLiveStrategy={() => { if (!formalLogin) { requireFormalLogin(); return; } setShowLivePanel(v => !v); }} liveStrategyActive={formalLogin && (showLivePanel || monitoredMarkets.length > 0)} showChartQuote={useUpbit} chartLiveQuote={liveQuote} chartQuoteLoading={isLoading} chartQuoteError={error} priceExtremeLabelsVisible={chartPriceExtremeLabelsVisible} onTogglePriceExtremeLabels={handleTogglePriceExtremeLabels} tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition} tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout} tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols} onTradeAlertPopupPosition={v => saveAppDef({ tradeAlertPopupPosition: v })} onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })} onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })} /> {isMobile && (showMarketPanel || mobileRightOpen) && ( { setDrawings([]); managerRef.current?.clearDrawings(); }} onUndo={() => setDrawings(prev => prev.slice(0, -1))} locked={drawingsLocked} visible={drawingsVisible} onToggleLock={() => setDrawingsLocked(v => !v)} onToggleEye={() => setDrawingsVisible(v => !v)} indicatorsVisible={indicatorsVisible} magnetMode={magnetMode} snapToIndicator={snapToIndicator} drawingCount={drawings.length} indicatorCount={indicators.length} onMagnetMode={setMagnetMode} onSnapToIndicator={setSnapToIndicator} onHideDrawings={() => setDrawingsVisible(v => !v)} onHideIndicators={() => setIndicatorsVisible(v => !v)} onHideAll={() => { setDrawingsVisible(false); setIndicatorsVisible(false); }} onClearIndicators={() => setIndicators([])} keepLockedOnDelete={keepLockedOnDelete} onToggleKeepLocked={() => setKeepLockedOnDelete(v => !v)} /> {/* ── 멀티 레이아웃: 슬롯 2개 이상일 때 별도 그리드 렌더 ─────────── */} {layoutDef.count > 1 && (
{chartAreaLoading && (
차트 로딩중...
)} {/* 슬롯 0 = 메인 차트 영역 (a) */}
{ slotRefs.current[0] = el; }} slotIndex={0} theme={theme} active={activeSlot === 0} onActivate={() => handleSetActiveSlot(0)} initialSymbol={slot0InitProps?.symbol} initialTimeframe={slot0InitProps?.timeframe} initialIndicators={slot0InitProps?.indicators} forcedSymbol={syncOptions.symbol ? symbol : undefined} forcedTf={syncOptions.interval ? timeframe : undefined} syncTime={syncOptions.crosshair ? syncTime : undefined} onCrosshairTime={t => { if (syncOptions.crosshair) setSyncTime(t); }} syncVisibleRange={syncOptions.time ? syncVisibleRange : undefined} onVisibleRangeChange={handleVisibleRangeChange} syncLogicalRange={syncOptions.dateRange ? syncLogicalRange : undefined} onLogicalRangeChange={handleLogicalRangeChange} onFullView={sym => handleSlotFullView(sym, 0)} magnifierEnabled={magnifierActive && activeSlot === 0} onMagnifierClose={() => setMagnifierActive(false)} onIndicatorsChange={activeSlot === 0 ? handleActiveSlotIndicatorsChange : undefined} onSymbolChange={activeSlot === 0 ? handleActiveSlotSymbolChange : undefined} magnetMode={magnetMode} onTradeOrderRequest={(req, idx) => applyTradeFill(req, idx)} chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartVolumeVisible={chartVolumeVisible} chartPaneSeparator={chartPaneSeparator} chartCrosshairInfoVisible={chartCrosshairInfoVisible} chartHoverToolbarVisible={chartHoverToolbarVisible} chartPriceExtremeLabelsVisible={chartPriceExtremeLabelsVisible} onTogglePriceExtremeLabels={handleTogglePriceExtremeLabels} chartLiveReceiveHighlight={chartLiveReceiveHighlight} chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'} displayTimezone={displayTimezone} marketTickers={marketTickers} marketInfos={marketInfos} marketLoading={marketLoading} refreshAllTickers={refreshAllTickers} onMarketSearchOpenChange={setMarketSearchOpen} compactMode chartVisible={chartVisible} chartConfigReady={chartConfigReady} onPaintReadyChange={handleSlotPaintReadyChange} />
{/* 슬롯 1~7 */} {(['b','c','d','e','f','g','h'] as const) .slice(0, layoutDef.count - 1) .map((area, i) => (
{ slotRefs.current[i + 1] = el; }} slotIndex={i + 1} theme={theme} active={activeSlot === i + 1} onActivate={() => handleSetActiveSlot(i + 1)} initialSymbol={slotInitProps[i + 1]?.symbol} initialTimeframe={slotInitProps[i + 1]?.timeframe} initialIndicators={slotInitProps[i + 1]?.indicators} forcedSymbol={syncOptions.symbol ? symbol : undefined} forcedTf={syncOptions.interval ? timeframe : undefined} syncTime={syncOptions.crosshair ? syncTime : undefined} onCrosshairTime={t => { if (syncOptions.crosshair) setSyncTime(t); }} syncVisibleRange={syncOptions.time ? syncVisibleRange : undefined} onVisibleRangeChange={handleVisibleRangeChange} syncLogicalRange={syncOptions.dateRange ? syncLogicalRange : undefined} onLogicalRangeChange={handleLogicalRangeChange} onFullView={sym => handleSlotFullView(sym, i + 1)} magnifierEnabled={magnifierActive && activeSlot === i + 1} onMagnifierClose={() => setMagnifierActive(false)} onIndicatorsChange={activeSlot === i + 1 ? handleActiveSlotIndicatorsChange : undefined} onSymbolChange={activeSlot === i + 1 ? handleActiveSlotSymbolChange : undefined} magnetMode={magnetMode} onTradeOrderRequest={(req, idx) => applyTradeFill(req, idx)} chartCandleAreaPriceLabels={chartCandleAreaPriceLabels} chartSeriesPriceLabels={chartSeriesPriceLabels} chartVolumeVisible={chartVolumeVisible} chartPaneSeparator={chartPaneSeparator} chartCrosshairInfoVisible={chartCrosshairInfoVisible} chartHoverToolbarVisible={chartHoverToolbarVisible} chartPriceExtremeLabelsVisible={chartPriceExtremeLabelsVisible} chartLiveReceiveHighlight={chartLiveReceiveHighlight} chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'} displayTimezone={displayTimezone} marketTickers={marketTickers} marketInfos={marketInfos} marketLoading={marketLoading} refreshAllTickers={refreshAllTickers} onMarketSearchOpenChange={setMarketSearchOpen} compactMode chartVisible={chartVisible} chartConfigReady={chartConfigReady} onPaintReadyChange={handleSlotPaintReadyChange} />
))}
)} {/* ── 싱글 레이아웃 (기존) ─────────────────────────────────────── */}
1 ? 'none' : undefined, position: 'relative' }}> {chartAreaLoading && (
차트 로딩중...
)} { const mgr = managerRef.current; mgr?.setChartOverlayVisibility(overlayVisibilityRef.current); if (mgr) syncCustomOverlaysToManager(mgr); syncBacktestMarkersRef.current(); }} onCandlesReady={handleCandlesReady} onManagerReady={mgr => { managerRef.current = mgr; const pending = pendingRealtimeBarRef.current; if (pending && chartLiveReadyRef.current && barsMarketRef.current === symbol && bars.length > 0) { pendingRealtimeBarRef.current = null; const last = bars[bars.length - 1]; if (last && pending.time === last.time) mgr.updateBar(pending); else if (!last || pending.time > last.time) void mgr.appendBar(pending); else mgr.updateBar({ ...pending, time: last.time }); } else if (chartLiveReadyRef.current) { pendingRealtimeBarRef.current = null; } mgr.setCandleAreaPriceLabelsEnabled(chartCandleAreaPriceLabels); mgr.setIndicatorAreaPriceLabelsEnabled(chartSeriesPriceLabels); const wrapper = document.querySelector('.chart-wrapper') as HTMLElement | null; mgr.setVolumeVisible(chartVolumeVisible, wrapper?.clientHeight); mgr.setPriceExtremeLabelsVisible(chartPriceExtremeLabelsVisible); mgr.setPaneSeparatorOptions(chartPaneSeparator); mgr.setDisplayTimezone(displayTimezone, timeframe, chartTimeFormat); mgr.setChartOverlayVisibility(overlayVisibilityRef.current); syncCustomOverlaysToManager(mgr); syncBacktestMarkersRef.current(); // 왼쪽 스크롤 감지 → 과거 데이터 추가 로드 mgr.subscribeVisibleLogicalRange(r => { if (r && r.from < LOAD_MORE_TRIGGER) { singleLoadMoreRef.current(); } }); }} onAddDrawing={d => pushDrawing([...drawings, d])} onSeriesClick={(entryId, point) => { if (!entryId) { setCtxToolbar(null); return; } setCtxToolbar({ entryId, screenX: point.x, screenY: point.y }); setDrawingCtxToolbar(null); // 드로잉 툴바 닫기 }} onSeriesDoubleClick={(entryId) => { setCtxToolbar(null); if (!entryId) return; if (entryId === '__main__') { setShowMainChartModal(true); } else { setSettingsModalId(entryId); } }} onHoverPaneLegend={handleHoverPaneLegend} onLeavePaneLegend={handleLeavePaneLegend} selectedDrawingId={selectedDrawingId} onDrawingClick={handleDrawingClick} onDrawingDoubleClick={handleDrawingDoubleClick} magnifierEnabled={magnifierActive} onMagnifierClose={() => setMagnifierActive(false)} onReorderIndicators={handleReorderIndicators} onMergeIndicators={handleMergeIndicators} onSplitIndicatorPane={handleSplitIndicatorPane} onExpandIndicator={handleExpandIndicator} onRemoveIndicator={handleRemoveIndicatorById} onDuplicateIndicator={handleDuplicateIndicator} focusedIndicatorId={focusedIndicatorId} onRestoreIndicators={handleRestoreIndicators} onToggleIndicatorHidden={handleToggleIndicatorHidden} onOpenIndicatorSettings={id => setSettingsModalId(id)} volumeVisible={chartVolumeVisible} candleAreaPriceLabelsEnabled={chartCandleAreaPriceLabels} indicatorAreaPriceLabelsEnabled={chartSeriesPriceLabels} paneSeparatorOptions={chartPaneSeparator} candleOverlayToggles={candleOverlayToggles} onToggleCandleOverlay={handleToggleCandleOverlay} priceExtremeLabelsVisible={chartPriceExtremeLabelsVisible} onTogglePriceExtremeLabels={handleTogglePriceExtremeLabels} customOverlayActive={customOverlayActive} onCustomOverlayActiveChange={handleCustomOverlayActiveChange} onOpenCustomOverlaySettings={() => setCustomOverlaySettingsSlot('custom')} custom1OverlayActive={custom1OverlayActive} onCustom1OverlayActiveChange={handleCustom1OverlayActiveChange} onOpenCustom1OverlaySettings={() => setCustomOverlaySettingsSlot('custom1')} showHoverToolbar={chartHoverToolbarVisible} /> {customOverlaySettingsSlot === 'custom' && ( handleSaveCustomOverlaySelection('custom', sel)} onCancel={() => setCustomOverlaySettingsSlot(null)} smaParams={indicators.find(i => i.type === 'SMA')?.params} /> )} {customOverlaySettingsSlot === 'custom1' && ( handleSaveCustomOverlaySelection('custom1', sel)} onCancel={() => setCustomOverlaySettingsSlot(null)} smaParams={indicators.find(i => i.type === 'SMA')?.params} /> )} {isSingleLoadingMore && (
과거 데이터 로딩...
)} {/* 백테스팅 결과 통계 배지 */} {btMatchesChart && layoutDef.count === 1 && ( { btMarkerPayloadRef.current = null; btClear(); managerRef.current?.clearBacktestMarkers(); }} /> )} {/* 실시간 전략 체크 패널 */} {showLivePanel && menuPage === 'chart' && layoutDef.count === 1 && ( { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }} onSettingsChange={handleLiveSettingsChange} onClose={() => setShowLivePanel(false)} /> )} {/* 백테스팅 설정 모달 */} {showBtSettings && ( setShowBtSettings(false)} onSettingsChange={s => { setBtSettings(s); setShowBtSettings(false); }} /> )} {/* 백테스팅 결과 — 투자분석 상세 레포트 (인쇄·PDF) */} setShowBtResult(false)} model={chartBtReportModel} /> {/* 시리즈 클릭/호버 컨텍스트 툴바 */} {ctxToolbar && (() => { const cfg = ctxToolbar.entryId !== '__main__' ? indicators.find(i => i.id === ctxToolbar.entryId) ?? null : null; return ( { const id = ctxToolbar.entryId; setCtxToolbar(null); if (id === '__main__') { setShowMainChartModal(true); } else { setSettingsModalId(id); } }} onToggleHidden={() => { handleToggleIndicatorHidden(ctxToolbar.entryId); }} onRemove={() => handleRemoveIndicatorById(ctxToolbar.entryId)} onClose={() => setCtxToolbar(null)} /> ); })()} {/* 드로잉 컨텍스트 툴바 */} {drawingCtxToolbar && (() => { const d = drawings.find(dr => dr.id === drawingCtxToolbar.id); if (!d) return null; return ( { setDrawingCtxToolbar(null); setDrawingSettingsId(d.id); }} onToggleVisible={() => { setDrawings(prev => prev.map(dr => dr.id === d.id ? { ...dr, visible: dr.visible !== false ? false : true } : dr ) ); setDrawingCtxToolbar(null); }} onRemove={() => { pushDrawing(drawings.filter(dr => dr.id !== d.id)); setDrawingCtxToolbar(null); setSelectedDrawingId(null); }} onClose={() => { setDrawingCtxToolbar(null); setSelectedDrawingId(null); }} /> ); })()} {/* 드로잉 설정 모달 */} {drawingSettingsId && (() => { const d = drawings.find(dr => dr.id === drawingSettingsId); if (!d) return null; const handleSaveDrawing = (updated: typeof d) => { setDrawings(prev => prev.map(dr => dr.id === updated!.id ? updated! : dr)); setDrawingSettingsId(null); setSelectedDrawingId(updated!.id); }; const handleCancelDrawing = () => { setDrawingSettingsId(null); }; if (d.type === 'fibtz') { return ( ); } return ( { setDrawings(prev => prev.map(dr => dr.id === updated.id ? updated : dr)); setDrawingSettingsId(null); setSelectedDrawingId(updated.id); }} onCancel={() => { setDrawingSettingsId(null); }} /> ); })()} {/* 메인 차트 설정 모달 */} {showMainChartModal && ( setShowMainChartModal(false)} /> )} {/* 보조지표 일괄 설정 */} {showBulkIndSettings && ( setShowBulkIndSettings(false)} /> )} {/* 인디케이터 설정 모달 */} {settingsModalId && settingsModalId !== '__main__' && (() => { let cfg: IndicatorConfig | null = null; if (isIndicatorSettingsTemplateId(settingsModalId)) { const type = indicatorTypeFromSettingsId(settingsModalId); if (!type) return null; cfg = buildMainIndicatorConfig( type, chartIndicatorsForSettings, getParams, getVisualConfig, ); } else { const onChart = chartIndicatorsForSettings.find(i => i.id === settingsModalId); if (onChart) { cfg = initializeIndicatorConfigForEditor(onChart, getParams, getVisualConfig); } } if (!cfg) return null; return ( setSettingsModalId(null)} /> ); })()}
{/* ── 우측 슬라이드 패널 (매수/매도 + 호가) ───────────────────── */} { refreshPaperAccount(symbol); handlePaperOrderFilled(); }} />
{/* ── 플로팅 패널: position:fixed로 항상 표시 (싱글/멀티 모드 공통) ─── */} {showObjectTree && ( setDrawings(prev => prev.map(d => d.id === id ? { ...d, visible: d.visible !== false ? false : true } : d) )} onRemove={id => setDrawings(prev => prev.filter(d => d.id !== id))} onClose={() => setShowObjectTree(false)} /> )} {showStats && setShowStats(false)} />} {showWatch && ( { setSymbol(s); setShowWatch(false); }} onAdd={s => { setWatchlist(prev => [...new Set([...prev, s])]); addWatchlistItem({ symbol: s }).catch(() => {}); }} onRemove={s => { setWatchlist(prev => prev.filter(x => x !== s)); removeWatchlistItem(s).catch(() => {}); }} onClose={() => setShowWatch(false)} /> )} {showAlert && ( { managerRef.current?.addAlert(price, label); setAlerts(prev => [...prev, { price, label }]); }} onRemove={price => { managerRef.current?.removeAlert(price); setAlerts(prev => prev.filter(a => Math.abs(a.price - price) > 0.0001)); }} onClose={() => setShowAlert(false)} /> )} {isMobile && ( { setShowMarketPanel(v => { const next = !v; if (next) setMobileRightOpen(false); return next; }); }} onOpenTrade={() => { setShowMarketPanel(false); setMobileRightTab('trade'); setMobileRightOpen(true); }} onOpenOrderbook={() => { setShowMarketPanel(false); setMobileRightTab('orderbook'); setMobileRightOpen(true); }} onClosePanels={() => { setShowMarketPanel(false); setMobileRightOpen(false); }} onTool={setDrawingTool} /> )} 1 ? activeSlotTf : timeframe} logScale={logScale} percentScale={percentScale} showGrid={showGrid} lastTime={bars[bars.length - 1]?.time ?? 0} displayTimezone={displayTimezone} onTimezoneChange={handleTimezoneChange} onTimeframe={handleTimeframe} onLogScale={() => setLogScale(v => !v)} onPercentScale={() => setPercentScale(v => !v)} onAutoScale={() => managerRef.current?.autoScale()} onFitContent={() => managerRef.current?.fitContent()} onToggleGrid={() => setShowGrid(v => !v)} onAddIndicators={handleAddIndicators} onAddIndicator={handleAddIndicator} /> ); } export const ChartWorkspaceView = React.memo(ChartWorkspaceViewInner);