import React, { useState, useEffect, useLayoutEffect, useCallback, useRef, useMemo } 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 { LAYOUTS, DEFAULT_SYNC, type LayoutDef, type SyncOptions } from './utils/layoutTypes';
import { generateBars, formatPrice, SYMBOLS } from './utils/dataGenerator';
import { normalizeTimezone, setDisplayTimezone, useDisplayTimezone } from './utils/timezone';
import { calcStats, type PriceStats } from './utils/calculations';
import { getKoreanName } from './utils/marketNameCache';
import { saveState, loadState, DEFAULT_MAIN_CHART_STYLE } from './utils/storage';
import { useWorkspacePersist } from './hooks/useWorkspacePersist';
import type { WatchlistItem, HoldingsItem } from './utils/backendApi';
import {
  addWatchlistItem, removeWatchlistItem, upsertHoldingsItem, removeHoldingsItem,
  loadWatchlist,
} from './utils/backendApi';
import { enrichIndicatorConfig, getIndicatorDef } from './utils/indicatorRegistry';
import { initializeIndicatorConfigForEditor } from './utils/indicatorSettingsEditor';
import {
  buildMainIndicatorConfig,
  chartIndicatorsNeedDefaultsRefresh,
  indicatorSettingsTemplateId,
  indicatorTypeFromSettingsId,
  isIndicatorSettingsTemplateId,
  mergeGlobalDefaultsOntoChartIndicators,
} from './utils/indicatorMainConfig';
import { normalizeSmaConfig, createDefaultSmaPlotVisibility } from './utils/smaConfig';
import { normalizeIchimokuConfig } from './utils/ichimokuConfig';
import { isUpbitMarket, UPBIT_MARKETS } from './utils/upbitApi';
import { getFavorites, saveFavorites, FAVORITES_CHANGED_EVENT } from './utils/marketStorage';
import { useChartRealtimeData, type WsStatus } from './hooks/useChartRealtimeData';
import type { OHLCVBar, ChartType, Theme, Timeframe, IndicatorConfig, LegendData, Drawing, ChartMode, MainChartStyle, TradeOrderFillRequest } from './types';
import type { ChartManager } from './utils/ChartManager';
import { useHistoryLoader, LOAD_MORE_TRIGGER } from './hooks/useHistoryLoader';
import { useMarketTicker } from './hooks/useMarketTicker';
import { useUpbitOrderbook } from './hooks/useUpbitOrderbook';
import { useIndicatorSettings } from './hooks/useIndicatorSettings';
import {
  duplicateIndicatorInList,
  mergeIndicators,
  reorderIndicatorGroup,
  removeIndicatorFromList,
  removeIndicatorTypeFromList,
  splitIndicatorPane,
  replaceIndicatorConfigsFromTypes,
} from './utils/indicatorPaneMerge';
import { useAppSettings } from './hooks/useAppSettings';
import { clampVirtualTargetMax } from './utils/virtualTargetLimits';
import { resolveTrendSearchAppSettings } from './utils/trendSearchAppSettings';
import MarketPanel from './components/MarketPanel';
import TopMenuBar, { type MenuPage } from './components/TopMenuBar';
import StrategyPage from './components/StrategyPage';
import StrategyEditorPage from './components/StrategyEditorPage';
import BacktestPanel from './components/BacktestPanel';
import { useBacktest } from './hooks/useBacktest';
import LiveStrategyPanel from './components/LiveStrategyPanel';
import { TradeNotificationProvider } from './contexts/TradeNotificationContext';
import { VerificationIssueNotificationProvider } from './contexts/VerificationIssueNotificationContext';
import { LiveSignalNotifier, type LiveSignalNotifierHandle } from './components/LiveSignalNotifier';
import { NotifyTopMenuBar, ChartToolbarNotify } from './components/NotifyUiBindings';
import { AppNotificationLayer } from './components/AppNotificationLayer';
import TradeNotificationListPage from './components/TradeNotificationListPage';
import BacktestSettingsModal from './components/BacktestSettingsModal';
import { BacktestResultModal } from './components/BacktestResultModal';
import { BacktestHistoryPage } from './components/BacktestHistoryPage';
import SettingsPage from './components/SettingsPage';
import PaperTradingPage from './components/PaperTradingPage';
import VirtualTradingPage from './components/VirtualTradingPage';
import TrendSearchPage from './components/TrendSearchPage';
import VerificationBoardPage from './components/VerificationBoardPage';
import DashboardPage from './components/DashboardPage';
import { loadPaperSummary, resetPaperAccount, loadActiveLiveStrategySettings, expandLiveStrategySubscriptions } from './utils/backendApi';
import ChartLegendBar from './components/ChartLegendBar';
import RightSidePanel from './components/RightSidePanel';
import {
  type BacktestSettingsDto,
  type BacktestAnalysis,
  DEFAULT_BACKTEST_SETTINGS,
  loadBacktestSettings,
  loadStrategies as loadStrategiesForLive,
  loadLiveStrategySettings,
  type LiveStrategySettingsDto,
  type BacktestSignal,
} from './utils/backendApi';
import { useVirtualLiveStrategy } from './hooks/useVirtualLiveStrategy';
import type { ChartRealtimeSource } from './hooks/useChartRealtimeData';
import { VIRTUAL_SESSION_CHANGED_EVENT } from './utils/virtualTradingStorage';
import { notifyPaperTradesChanged } from './utils/paperTradeEvents';
import { useIsMobile } from './hooks/useMediaQuery';
import MobileChartDock from './components/MobileChartDock';
import LoginModal from './components/LoginModal';
import SplashScreen from './components/SplashScreen';
import { getAuthSession, setAuthSession, clearAuthSession, type AuthSession } from './utils/auth';
import { isAppEntered, setAppEntered, clearAppEntered } from './utils/appEntry';
import { syncDocumentTheme } from './utils/documentTheme';
import { useMenuPermissions, invalidateMenuPermissionsCache } from './hooks/useMenuPermissions';
import { firstAllowedTopMenu, normalizeRole } from './utils/permissions';
import { clearAdminUnlock } from './utils/adminUnlock';
import type { LoginResponse } from './utils/backendApi';
import { invalidateAppSettingsCache } from './hooks/useAppSettings';
import { invalidateIndicatorSettingsCache } from './hooks/useIndicatorSettings';
import './App.css';
import './styles/appPopup.css';
import './styles/paperDashboard.css';
import './styles/virtualTradingDashboard.css';
import './styles/backtestDashboard.css';

let _indCounter = 0;
function newIndId() { return `ind_${++_indCounter}_${Date.now()}`; }

// ─── 연결 상태 배지 ────────────────────────────────────────────────────────
const WsBadge: React.FC<{ status: WsStatus; isUpbit: boolean }> = ({ status, isUpbit }) => {
  if (!isUpbit) return null;
  const map: Record<WsStatus, { dot: string; label: string }> = {
    connecting:   { dot: 'dot-yellow', label: '연결 중...' },
    connected:    { dot: 'dot-lime',   label: '실시간' },
    disconnected: { dot: 'dot-red',    label: '재연결 중' },
    error:        { dot: 'dot-red',    label: '오류' },
  };
  const { dot, label } = map[status];
  return <span className={`ws-badge ${dot}`}><span className="ws-dot" />{label}</span>;
};

/** 모든 .ws-dot 요소에 형광 glow 플래시 (React state 없이 직접 DOM 조작) */
let _wsFlashTimer: ReturnType<typeof setTimeout> | null = null;
function flashWsDot() {
  document.querySelectorAll('.ws-dot').forEach(el => el.classList.add('receiving'));
  if (_wsFlashTimer) clearTimeout(_wsFlashTimer);
  _wsFlashTimer = setTimeout(() => {
    document.querySelectorAll('.ws-dot').forEach(el => el.classList.remove('receiving'));
    _wsFlashTimer = null;
  }, 600);
}

// ─── App ───────────────────────────────────────────────────────────────────
function App() {
  const initial = loadState();

  const [sessionKey, setSessionKey] = useState(0);
  const [appEntered, setAppEnteredState] = useState(() => isAppEntered());
  const [authUser, setAuthUser] = useState<AuthSession | null>(() =>
    isAppEntered() ? getAuthSession() : null,
  );
  const [guestMode, setGuestMode] = useState(() =>
    isAppEntered() && !getAuthSession(),
  );
  const [loginOpen, setLoginOpen] = useState(false);

  // 전역 지표 파라미터 + 시각 설정 DB 동기화
  const {
    getParams: getIndicatorParams,
    saveParams: saveIndicatorParams,
    getVisualConfig: getIndicatorVisual,
    saveVisual: saveIndicatorVisual,
    saveAllIndicatorDefaults,
    settingsRevision: indicatorSettingsRevision,
  } = useIndicatorSettings(sessionKey);
  const getParams = getIndicatorParams;
  const saveParams = saveIndicatorParams;
  const getVisualConfig = getIndicatorVisual;
  const saveVisual = saveIndicatorVisual;

  // 앱 전역 기본 설정 DB 동기화 (하드코딩 대체)
  const { defaults: appDefaults, isLoaded: appSettingsLoaded, save: saveAppDef } = useAppSettings(sessionKey);
  const { permissions: menuPermissions, isLoaded: menuPermsLoaded, can: canMenu } = useMenuPermissions(sessionKey);
  const displayTimezone = useDisplayTimezone();

  // DB 로드 전에는 localStorage 기본값, 로드 후 DB 값으로 오버라이드됨
  const [symbol,      setSymbol]      = useState(initial.symbol);
  const [timeframe,   setTimeframe]   = useState<Timeframe>(initial.timeframe);
  const [chartType,   setChartType]   = useState<ChartType>(initial.chartType);
  const [theme,       setTheme]       = useState<Theme>(initial.theme);
  const [mode,        setMode]        = useState<ChartMode>('chart');
  const [logScale,    setLogScale]    = useState(false);
  const [indicators,  setIndicators]  = useState<IndicatorConfig[]>(initial.indicators ?? []);
  const [drawingTool,       setDrawingTool]       = useState('cursor');
  const [drawings,          setDrawings]          = useState<Drawing[]>([]);
  const [drawingsLocked,    setDrawingsLocked]    = useState(false);
  const [drawingsVisible,   setDrawingsVisible]   = useState(true);
  const [indicatorsVisible, setIndicatorsVisible] = useState(true);
  const [magnetMode,        setMagnetMode]        = useState<'off'|'weak'|'strong'>('off');
  const [snapToIndicator,   setSnapToIndicator]   = useState(false);
  const [keepLockedOnDelete,setKeepLockedOnDelete]= useState(false);
  const [legend,      setLegend]      = useState<LegendData | null>(null);
  const [showStats,     setShowStats]     = useState(false);
  const [showWatch,     setShowWatch]     = useState(false);
  const [showAlert,     setShowAlert]     = useState(false);
  const [showObjectTree, setShowObjectTree] = useState(false);
  const [percentScale, setPercentScale] = useState(false);
  const [showGrid,     setShowGrid]     = useState(true);
  const [magnifierActive, setMagnifierActive] = useState(false);
  const [showMarketPanel, setShowMarketPanel] = useState(false);
  const [mobileRightOpen, setMobileRightOpen] = useState(false);
  const [mobileRightTab, setMobileRightTab] = useState<'trade' | 'orderbook'>('trade');
  const [menuPage, setMenuPage] = useState<MenuPage>('chart');
  const [verificationFocusIssueId, setVerificationFocusIssueId] = useState<number | null>(null);
  const chartVisible = menuPage === 'chart';
  const isMobile = useIsMobile();

  // ── 마켓 패널 시세 데이터 ─────────────────────────────────────────────
  const { tickers: marketTickers, loading: marketLoading, usdRate } = useMarketTicker();

  // ── 레이아웃 상태 ─────────────────────────────────────────────────────
  const [layoutDef,   setLayoutDef]   = useState<LayoutDef>(() => {
    const saved = localStorage.getItem('chartLayout');
    return LAYOUTS.find(l => l.id === saved) ?? LAYOUTS[0];
  });
  const [syncOptions, setSyncOptions] = useState<SyncOptions>(() => {
    try {
      const s = localStorage.getItem('chartLayoutSync');
      return s ? JSON.parse(s) : DEFAULT_SYNC;
    } catch { return DEFAULT_SYNC; }
  });
  const [activeSlot, setActiveSlot] = useState(0);
  // 멀티 레이아웃에서 현재 활성 슬롯의 지표 목록 (IndicatorModal 표시용)
  const [activeSlotIndicators, setActiveSlotIndicators] = useState<IndicatorConfig[]>([]);
  /**
   * 단일→다중 전환 시 슬롯 0 의 마운트 초기값으로 전달할 설정.
   * React 18 배치 업데이트 덕분에 setLayoutDef 와 동일 렌더에 반영되어
   * ChartSlot 이 처음 마운트될 때 이 값이 useState 초기값으로 사용된다.
   */
  const [slot0InitProps, setSlot0InitProps] = useState<{
    symbol: string;
    timeframe: Timeframe;
    indicators: IndicatorConfig[];
  } | null>(null);
  /** 멀티슬롯용: 슬롯 1+ 초기 props (슬롯 인덱스 → 초기값 맵) */
  const [slotInitProps, setSlotInitProps] = useState<Record<number, {
    symbol?: string;
    timeframe?: Timeframe;
    indicators?: IndicatorConfig[];
    mainChartStyle?: MainChartStyle;
  }>>({});
  const [syncTime,         setSyncTime]         = useState<number>(0);
  const [syncVisibleRange, setSyncVisibleRange] = useState<{ from: number; to: number } | null>(null);
  const [syncLogicalRange, setSyncLogicalRange] = useState<{ from: number; to: number } | null>(null);
  // 멀티 레이아웃에서 활성 슬롯의 타임프레임을 BottomBar/툴바에 반영
  const [activeSlotTf,     setActiveSlotTf]     = useState<Timeframe>(timeframe);
  const [activeSlotSymbol, setActiveSlotSymbol] = useState<string>(initial.symbol);

  const handleTimezoneChange = useCallback((tz: string) => {
    const next = normalizeTimezone(tz);
    setDisplayTimezone(next);
    saveAppDef({ displayTimezone: next });
    const tf = layoutDef.count > 1 ? activeSlotTf : timeframe;
    managerRef.current?.setDisplayTimezone(next, tf);
  }, [saveAppDef, layoutDef.count, activeSlotTf, timeframe]);

  const chartSymbol = layoutDef.count > 1 ? activeSlotSymbol : symbol;
  const { orderbook, wsStatus: obWsStatus, spread } = useUpbitOrderbook(chartSymbol);
  const chartTicker = marketTickers.get(chartSymbol);
  const orderbookPrevClose = chartTicker
    ? (chartTicker.tradePrice ?? 0) - (chartTicker.changePrice ?? 0)
    : 0;

  const [tradeFill, setTradeFill] = useState<TradeOrderFillRequest | null>(null);

  const applyTradeFill = useCallback((
    req: { market: string; price: number; side: 'buy' | 'sell' },
    slotIndex?: number,
  ) => {
    setTradeFill(prev => ({
      market: req.market,
      price: req.price,
      side: req.side,
      seq: (prev?.seq ?? 0) + 1,
    }));
    if (layoutDef.count > 1) {
      const idx = slotIndex ?? activeSlot;
      setActiveSlot(idx);
      slotRefs.current[idx]?.setSymbol(req.market);
      setActiveSlotSymbol(req.market);
    } else if (req.market !== symbol) {
      setSymbol(req.market);
      setLegend(null);
    }
  }, [layoutDef.count, activeSlot, symbol]);

  const handleOrderbookPick = useCallback((price: number, rowType: 'ask' | 'bid') => {
    applyTradeFill({
      market: chartSymbol,
      price,
      // 매수 호가(bid) 클릭 → 매수 탭, 매도 호가(ask) 클릭 → 매도 탭
      side: rowType === 'bid' ? 'buy' : 'sell',
    });
  }, [applyTradeFill, chartSymbol]);

  const handleTradeMarketSelect = useCallback((s: string) => {
    if (layoutDef.count > 1) {
      slotRefs.current[activeSlot]?.setSymbol(s);
      setActiveSlotSymbol(s);
    } else {
      setSymbol(s);
      setLegend(null);
    }
  }, [layoutDef.count, activeSlot]);

  // 활성 슬롯 변경 시 해당 슬롯의 타임프레임/심볼/지표를 읽어 표시 업데이트
  const handleSetActiveSlot = useCallback((idx: number) => {
    setActiveSlot(idx);
    // symbolRef는 initialSymbol로 초기화되어 있으므로 즉시 읽어도 정확한 값 반환
    const slotImmediate = slotRefs.current[idx];
    if (slotImmediate) {
      const sym = slotImmediate.getSymbol();
      if (sym) setActiveSlotSymbol(sym);
      const tf = slotImmediate.getTimeframe();
      if (tf) setActiveSlotTf(tf);
      setActiveSlotIndicators(slotImmediate.getIndicators());
    } else {
      // 슬롯이 아직 마운트되지 않은 경우 rAF로 재시도
      requestAnimationFrame(() => {
        const slot = slotRefs.current[idx];
        if (!slot) return;
        const tf = slot.getTimeframe();
        if (tf) setActiveSlotTf(tf);
        const sym = slot.getSymbol();
        if (sym) setActiveSlotSymbol(sym);
        setActiveSlotIndicators(slot.getIndicators());
      });
    }
  }, []);

  // 멀티 모드에서 활성 슬롯 지표가 바뀌면 상태 갱신 (IndicatorModal 반영용)
  const handleActiveSlotIndicatorsChange = useCallback((inds: IndicatorConfig[]) => {
    setActiveSlotIndicators(inds);
  }, []);

  // 멀티 모드에서 활성 슬롯 심볼이 바뀌면 상태 갱신 (호가창 심볼 동기화)
  const handleActiveSlotSymbolChange = useCallback((sym: string) => {
    setActiveSlotSymbol(sym);
  }, []);

  // Toolbar 에 전달할 활성 지표 목록:
  // 멀티 레이아웃이면 활성 슬롯 지표, 싱글이면 전역 indicators
  const toolbarActiveIndicators = layoutDef.count > 1 ? activeSlotIndicators : indicators;

  /**
   * 레이아웃 전환 핸들러
   * ─ 단일 → 다중: 현재 싱글 차트의 심볼/타임프레임/지표를 슬롯 0 의 초기값으로 기록
   * ─ 다중 → 단일: 활성 슬롯의 심볼/타임프레임/지표를 싱글 차트 상태에 즉시 복원
   *                (ChartSlot 이 unmount 되기 전에 ref 에서 읽어야 함)
   */
  const handleLayoutSelect = useCallback((def: LayoutDef) => {
    if (def.count > 1 && layoutDef.count === 1) {
      // 단일 → 다중: 싱글 차트 설정을 슬롯 0 초기값으로 저장
      // setLayoutDef 와 같은 렌더에 배치되어 ChartSlot 마운트 시 반영됨
      setSlot0InitProps({
        symbol,
        timeframe,
        indicators: indicators.map(i => ({ ...i })),
      });
      setActiveSlot(0);
      setActiveSlotIndicators(indicators.map(i => ({ ...i })));
    } else if (def.count === 1 && layoutDef.count > 1) {
      // 다중 → 단일: 활성 슬롯 설정 읽기 (slot ref 는 이 시점에서만 유효)
      const slot = slotRefs.current[activeSlot];
      if (slot) {
        const slotSymbol = slot.getSymbol();
        const slotTf     = slot.getTimeframe();
        const slotInds   = slot.getIndicators().map(i => ({ ...i }));
        setSymbol(slotSymbol);
        setTimeframe(slotTf);
        setIndicators(slotInds);
        setLegend(null);
      }
      // 다음번 다중 전환 시 초기값이 남아있지 않도록 초기화
      setSlot0InitProps(null);
    }
    setLayoutDef(def);
    // 레이아웃 변경 시 DB 에도 저장
    saveAppDef({ defaultLayoutId: def.id });
  }, [layoutDef.count, activeSlot, symbol, timeframe, indicators, saveAppDef]);

  // 레이아웃/싱크 변경 시 localStorage 저장
  useEffect(() => {
    localStorage.setItem('chartLayout', layoutDef.id);
  }, [layoutDef]);
  useEffect(() => {
    localStorage.setItem('chartLayoutSync', JSON.stringify(syncOptions));
  }, [syncOptions]);

  // 다중 → 싱글 전환 시 activeSlotIndicators 초기화
  useEffect(() => {
    if (layoutDef.count <= 1) setActiveSlotIndicators([]);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [layoutDef.count]);

  // ── 시리즈 클릭/호버 컨텍스트 툴바 ────────────────────────────────────
  const [ctxToolbar, setCtxToolbar] = useState<{
    entryId: string;
    screenX: number;
    screenY: number;
  } | null>(null);
  // 호버 hide 지연 타이머 (레이블 → 툴바 이동 시 깜빡임 방지)
  const hoverHideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

  const clearHoverTimer = useCallback(() => {
    if (hoverHideTimer.current) { clearTimeout(hoverHideTimer.current); hoverHideTimer.current = null; }
  }, []);

  const handleHoverPaneLegend = useCallback((id: string, sx: number, sy: number) => {
    clearHoverTimer();
    setCtxToolbar({ entryId: id, screenX: sx, screenY: sy });
  }, [clearHoverTimer]);

  const handleLeavePaneLegend = useCallback(() => {
    clearHoverTimer();
    hoverHideTimer.current = setTimeout(() => {
      setCtxToolbar(null);
      hoverHideTimer.current = null;
    }, 180);
  }, [clearHoverTimer]);

  // ── 드로잉 컨텍스트 툴바 & 설정 모달 ────────────────────────────────────
  const [selectedDrawingId,  setSelectedDrawingId]  = useState<string | null>(null);
  const [drawingCtxToolbar,  setDrawingCtxToolbar]  = useState<{
    id: string; screenX: number; screenY: number;
  } | null>(null);
  const [drawingSettingsId,  setDrawingSettingsId]  = useState<string | null>(null);

  const handleDrawingClick = useCallback((id: string | null, sx: number, sy: number) => {
    if (!id) {
      // 빈 공간 클릭 → 선택 해제
      setSelectedDrawingId(null);
      setDrawingCtxToolbar(null);
      return;
    }
    setSelectedDrawingId(id);
    setDrawingCtxToolbar({ id, screenX: sx, screenY: sy });
    setCtxToolbar(null); // 인디케이터 툴바 닫기
  }, []);

  const handleDrawingDoubleClick = useCallback((id: string, sx: number, sy: number) => {
    setSelectedDrawingId(id);
    setDrawingCtxToolbar(null);
    setDrawingSettingsId(id);
    void sx; void sy;
  }, []);

  // ── 인디케이터 설정 모달 ───────────────────────────────────────────────
  const [settingsModalId, setSettingsModalId] = useState<string | null>(null);
  const [showBulkIndSettings, setShowBulkIndSettings] = useState(false);

  // ── 메인 차트 설정 ─────────────────────────────────────────────────────
  const [mainChartStyle, setMainChartStyle] = useState<MainChartStyle>(
    initial.mainChartStyle ?? DEFAULT_MAIN_CHART_STYLE
  );
  const [showMainChartModal, setShowMainChartModal] = useState(false);

  const handleMainChartSave = useCallback((style: MainChartStyle) => {
    setMainChartStyle(style);
    saveState({ mainChartStyle: style });
    setShowMainChartModal(false);
    // DB 에도 저장 → 다음 로드 시 하드코딩 기본값 대신 사용
    saveAppDef({ mainChartStyle: style as unknown as Record<string, string> });
  }, [saveAppDef]);

  // ── 드로잉 Undo/Redo 스택 ────────────────────────────────────────────────
  const undoStack = useRef<Drawing[][]>([]);
  const redoStack = useRef<Drawing[][]>([]);
  const [canUndo, setCanUndo] = useState(false);
  const [canRedo, setCanRedo] = useState(false);

  const pushDrawing = useCallback((newDrawings: Drawing[]) => {
    undoStack.current.push(drawings);
    redoStack.current = [];
    setDrawings(newDrawings);
    setCanUndo(true);
    setCanRedo(false);
  }, [drawings]);

  const handleUndo = useCallback(() => {
    if (undoStack.current.length === 0) return;
    redoStack.current.push(drawings);
    const prev = undoStack.current.pop()!;
    setDrawings(prev);
    setCanUndo(undoStack.current.length > 0);
    setCanRedo(true);
  }, [drawings]);

  const handleRedo = useCallback(() => {
    if (redoStack.current.length === 0) return;
    undoStack.current.push(drawings);
    const next = redoStack.current.pop()!;
    setDrawings(next);
    setCanUndo(true);
    setCanRedo(redoStack.current.length > 0);
  }, [drawings]);
  const [stats,       setStats]       = useState<PriceStats | null>(null);
  const [watchlist,   setWatchlist]   = useState<string[]>(() => {
    const base = initial.watchlist?.length
      ? initial.watchlist
      : [...UPBIT_MARKETS.slice(0, 5), ...SYMBOLS.slice(0, 4)];
    return [...new Set([...base, ...getFavorites()])];
  });
  /** 좌측 패널 관심종목(gc_favorites) — 종목 검색 드롭다운 별표·실시간 전략과 동기화 */
  const [favoritesList, setFavoritesList] = useState<string[]>(getFavorites);
  // DB 로드 완료 여부 (false → DB 로드 전, localStorage 기반 동작)
  const [dbLoaded, setDbLoaded] = useState(false);

  const enterMainApp = useCallback(() => {
    setAppEntered();
    setAppEnteredState(true);
  }, []);

  const handleLoginSuccess = useCallback((res: LoginResponse) => {
    const session: AuthSession = {
      userId: res.userId,
      username: res.username,
      displayName: res.displayName,
      role: normalizeRole(res.role),
    };
    setAuthSession(session);
    setAuthUser(session);
    setGuestMode(false);
    setDbLoaded(false);
    clearAdminUnlock();
    invalidateAppSettingsCache();
    invalidateIndicatorSettingsCache();
    invalidateMenuPermissionsCache();
    setSessionKey(k => k + 1);
    enterMainApp();
  }, [enterMainApp]);

  const handleGuestEnter = useCallback(() => {
    clearAuthSession();
    setAuthUser(null);
    setGuestMode(true);
    setDbLoaded(false);
    clearAdminUnlock();
    invalidateAppSettingsCache();
    invalidateIndicatorSettingsCache();
    invalidateMenuPermissionsCache();
    setSessionKey(k => k + 1);
    enterMainApp();
  }, [enterMainApp]);

  const handleLogout = useCallback(() => {
    clearAuthSession();
    setAuthUser(null);
    setGuestMode(false);
    setDbLoaded(false);
    clearAdminUnlock();
    invalidateAppSettingsCache();
    invalidateIndicatorSettingsCache();
    invalidateMenuPermissionsCache();
    setSessionKey(k => k + 1);
    clearAppEntered();
    setAppEnteredState(false);
    setLoginOpen(false);
  }, []);

  const guardedSetMenuPage = useCallback((page: MenuPage) => {
    if (page === 'notifications' && !canMenu('notifications')) return;
    if (page !== 'notifications' && !canMenu(page)) {
      setMenuPage(firstAllowedTopMenu(menuPermissions));
      return;
    }
    setMenuPage(page);
  }, [canMenu, menuPermissions]);

  useEffect(() => {
    if (!menuPermsLoaded) return;
    if (menuPage === 'notifications') {
      if (!canMenu('notifications')) setMenuPage(firstAllowedTopMenu(menuPermissions));
      return;
    }
    if (!canMenu(menuPage)) {
      setMenuPage(firstAllowedTopMenu(menuPermissions));
    }
  }, [menuPage, menuPermsLoaded, menuPermissions, canMenu]);

  // 앱 설정 DB 로드 완료 시: 워크스페이스 로드 전 localStorage 기본값 일부만 교체
  // layoutId 는 워크스페이스가 우선 — 로그인 직후 멀티 레이아웃으로 잠깐 전환되며 캔들이 비는 현상 방지
  useEffect(() => {
    if (!appSettingsLoaded) return;
    if (dbLoaded) return;
    if (appDefaults.mainChartStyle) setMainChartStyle(appDefaults.mainChartStyle);
    if (appDefaults.syncOptions)    setSyncOptions(appDefaults.syncOptions);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [appSettingsLoaded, dbLoaded]);

  const chartSeriesPriceLabels = appDefaults.chartSeriesPriceLabels ?? true;
  const chartVolumeVisible = appDefaults.chartVolumeVisible ?? true;
  const chartLiveReceiveHighlight = appDefaults.chartLiveReceiveHighlight ?? true;
  const chartLegendOptions = appDefaults.chartLegendOptions;
  const chartPaneSeparator = appDefaults.chartPaneSeparator;
  const paperTradingEnabled = appDefaults.paperTradingEnabled ?? true;
  const paperAutoTradeEnabled = appDefaults.paperAutoTradeEnabled ?? false;
  const [paperCashBalance, setPaperCashBalance] = useState(0);
  const [paperCoinQty, setPaperCoinQty] = useState(0);
  const [paperRefreshKey, setPaperRefreshKey] = useState(0);

  const handlePaperOrderFilled = useCallback(() => {
    setPaperRefreshKey(k => k + 1);
    notifyPaperTradesChanged();
  }, []);

  const refreshPaperAccount = useCallback(async (forMarket?: string) => {
    const summary = await loadPaperSummary();
    if (!summary) return;
    setPaperCashBalance(summary.cashBalance);
    const m = forMarket ?? symbol;
    const pos = summary.positions?.find(p => p.symbol === m);
    setPaperCoinQty(pos?.quantity ?? 0);
  }, [symbol]);

  useEffect(() => {
    if (appSettingsLoaded) refreshPaperAccount();
  }, [appSettingsLoaded, refreshPaperAccount]);

  useEffect(() => {
    if (!appSettingsLoaded || !appDefaults.fcmPushEnabled) return;
    import('./utils/fcmPush').then(({ initFcmPush }) => {
      void initFcmPush((title, body) => {
        console.info('[FCM]', title, body);
      });
    });
  }, [appSettingsLoaded, appDefaults.fcmPushEnabled]);

  useEffect(() => {
    if (appSettingsLoaded && paperTradingEnabled) refreshPaperAccount(symbol);
  }, [symbol, appSettingsLoaded, paperTradingEnabled, refreshPaperAccount]);

  const [alerts, setAlerts] = useState<Array<{ price: number; label: string }>>(
    (initial.alertPrices ?? []).map(p => ({ price: p, label: '' }))
  );

  const managerRef = useRef<ChartManager | null>(null);
  /** 차트 매니저 준비 전 도착한 STOMP 틱 (준비 후 1회 적용) */
  const pendingRealtimeBarRef = useRef<OHLCVBar | null>(null);
  /** reloadAll setData 완료 전 — 이전 종목 rawBars에 실시간 틱 혼입 방지 */
  const chartLiveReadyRef = useRef(false);
  const useUpbit   = isUpbitMarket(symbol);

  // ── 백테스팅 ───────────────────────────────────────────────────────────────
  const {
    signals: btSignals,
    stats: btStats,
    analysis: btAnalysis,
    resultSymbol: btSymbol,
    resultTimeframe: btTimeframe,
    runSeq: btRunSeq,
    running: btRunning,
    run: btRun,
    clear: btClear,
    invalidateRuns: btInvalidateRuns,
  } = useBacktest();
  const [btSettings, setBtSettings] = useState<BacktestSettingsDto>(DEFAULT_BACKTEST_SETTINGS);
  const [showBtSettings, setShowBtSettings] = useState(false);
  const [showBtResult,   setShowBtResult]   = useState(false);
  const [btStrategyName, setBtStrategyName] = useState('전략');

  /** 현재 차트와 일치하는 백테스팅 결과만 UI·마커에 반영 */
  const btMatchesChart = useMemo(
    () =>
      btSignals.length > 0 &&
      btSymbol != null &&
      btTimeframe != null &&
      btSymbol === symbol &&
      btTimeframe === timeframe,
    [btSignals.length, btSymbol, btTimeframe, symbol, timeframe],
  );

  const btMarkerPayloadRef = useRef<{
    symbol:    string;
    timeframe: string;
    signals:   BacktestSignal[];
    showPrice: boolean;
  } | null>(null);
  /** applyBacktestMarkersWhenReady rAF 재시도 시 최신 차트 종목·타임프레임 검증용 */
  const chartContextRef = useRef({ symbol, timeframe });
  chartContextRef.current = { symbol, timeframe };
  const barsMarketRef = useRef<string | null>(null);

  const syncBacktestMarkersRef = useRef<() => void>(() => {});

  /** ChartManager 메인 시리즈 준비 후 백테스트 마커 적용 (로딩·remount 직후 대응) */
  const applyBacktestMarkersWhenReady = useCallback((
    payload: NonNullable<typeof btMarkerPayloadRef.current>,
    attempt = 0,
  ) => {
    btMarkerPayloadRef.current = payload;
    const { symbol: curSym, timeframe: curTf } = chartContextRef.current;
    if (payload.symbol !== curSym || payload.timeframe !== curTf) return;

    const mgr = managerRef.current;
    if (!mgr || !mgr.hasMainSeries()) {
      if (attempt < 120) {
        requestAnimationFrame(() => applyBacktestMarkersWhenReady(payload, attempt + 1));
      }
      return;
    }
    if (payload.symbol !== chartContextRef.current.symbol
        || payload.timeframe !== chartContextRef.current.timeframe) return;
    mgr.setBacktestMarkers(payload.signals, payload.showPrice);
  }, []);

  /** reloadAll 직후 등 — payload 가 있을 때만 재적용 (없을 때 clear 하지 않음, 경쟁 방지) */
  const syncBacktestMarkers = useCallback(() => {
    const payload = btMarkerPayloadRef.current;
    if (
      payload &&
      payload.symbol === symbol &&
      payload.timeframe === timeframe &&
      payload.signals.length > 0
    ) {
      applyBacktestMarkersWhenReady(payload);
    }
  }, [symbol, timeframe, applyBacktestMarkersWhenReady]);

  syncBacktestMarkersRef.current = syncBacktestMarkers;

  // ── 실시간 전략 체크 (가상투자 세션 우선, 없으면 전역+관심종목) ─────────────
  const [showLivePanel, setShowLivePanel] = useState(false);
  const [liveStrategies, setLiveStrategies] = useState<{id:number;name:string}[]>([]);
  const [liveStrategyCandleTypes, setLiveStrategyCandleTypes] = useState<string[] | undefined>();
  const [activeLiveSettings, setActiveLiveSettings] = useState<LiveStrategySettingsDto[]>([]);
  const { session: virtualSession, isVirtualActive, monitoredMarkets: virtualMonitoredMarkets } = useVirtualLiveStrategy();

  const refreshActiveLiveSettings = useCallback(() => {
    loadActiveLiveStrategySettings()
      .then(setActiveLiveSettings)
      .catch(() => setActiveLiveSettings([]));
  }, []);

  useEffect(() => {
    refreshActiveLiveSettings();
  }, [refreshActiveLiveSettings, isVirtualActive, virtualMonitoredMarkets.join(','), virtualSession.executionType, virtualSession.positionMode]);

  useEffect(() => {
    const onVirtualChanged = () => refreshActiveLiveSettings();
    window.addEventListener(VIRTUAL_SESSION_CHANGED_EVENT, onVirtualChanged);
    return () => window.removeEventListener(VIRTUAL_SESSION_CHANGED_EVENT, onVirtualChanged);
  }, [refreshActiveLiveSettings]);

  useEffect(() => {
    if (!appSettingsLoaded) return;
    if (isVirtualActive) {
      const active = activeLiveSettings.find(s => s.market === symbol);
      setLiveStrategyCandleTypes(active?.strategyCandleTypes);
      return;
    }
    loadLiveStrategySettings(symbol)
      .then(s => setLiveStrategyCandleTypes(s.strategyCandleTypes))
      .catch(() => { /* optional */ });
  }, [symbol, appSettingsLoaded, appDefaults.liveStrategyId, isVirtualActive, activeLiveSettings]);

  const liveStrategySettings: LiveStrategySettingsDto = useMemo(() => {
    if (isVirtualActive) {
      const active = activeLiveSettings.find(s => s.market === symbol);
      return {
        market: symbol,
        strategyId: active?.strategyId ?? virtualSession.globalStrategyId,
        isLiveCheck: true,
        executionType: active?.executionType ?? virtualSession.executionType,
        positionMode: active?.positionMode ?? virtualSession.positionMode,
        strategyCandleTypes: active?.strategyCandleTypes ?? liveStrategyCandleTypes,
      };
    }
    return {
      market: symbol,
      strategyId: appDefaults.liveStrategyId ?? null,
      isLiveCheck: appDefaults.liveStrategyCheck ?? false,
      executionType: appDefaults.liveExecutionType ?? 'CANDLE_CLOSE',
      positionMode: appDefaults.livePositionMode ?? 'LONG_ONLY',
      strategyCandleTypes: liveStrategyCandleTypes,
    };
  }, [isVirtualActive, activeLiveSettings, symbol, virtualSession, appDefaults, liveStrategyCandleTypes]);

  /** STOMP 구독 대상 — 가상투자 실행 중이면 투자대상 종목, 아니면 관심종목 */
  const monitoredMarkets = useMemo(() => {
    if (isVirtualActive) return virtualMonitoredMarkets;
    if (!appDefaults.liveStrategyCheck || !appDefaults.liveStrategyId) return [];
    const set = new Set(watchlist);
    if (symbol) set.add(symbol);
    return [...set];
  }, [isVirtualActive, virtualMonitoredMarkets, watchlist, symbol, appDefaults.liveStrategyCheck, appDefaults.liveStrategyId]);

  const [marketSubscriptions, setMarketSubscriptions] = useState<{ market: string; candleType: string }[]>([]);

  const liveStompSubscriptions = useMemo(() => {
    if (monitoredMarkets.length === 0) return [];
    const filtered = activeLiveSettings.filter(s => monitoredMarkets.includes(s.market));
    if (filtered.length > 0) return expandLiveStrategySubscriptions(filtered);
    if (marketSubscriptions.length > 0) return marketSubscriptions;
    return monitoredMarkets.map(m => ({ market: m, candleType: '1m' }));
  }, [monitoredMarkets, activeLiveSettings, marketSubscriptions]);

  useEffect(() => {
    if (monitoredMarkets.length === 0) {
      setMarketSubscriptions([]);
      return;
    }
    if (isVirtualActive) {
      setMarketSubscriptions(expandLiveStrategySubscriptions(
        activeLiveSettings.filter(s => monitoredMarkets.includes(s.market)),
      ));
      return;
    }
    if (!appDefaults.liveStrategyCheck) {
      setMarketSubscriptions([]);
      return;
    }
    loadActiveLiveStrategySettings()
      .then(list => setMarketSubscriptions(expandLiveStrategySubscriptions(list)))
      .catch(() => { /* liveStompSubscriptions 폴백 */ });
  }, [isVirtualActive, appDefaults.liveStrategyCheck, appDefaults.liveStrategyId, monitoredMarkets.join(','), activeLiveSettings]);

  const handleLiveSettingsChange = useCallback((saved: LiveStrategySettingsDto) => {
    if (isVirtualActive) return;
    saveAppDef({
      liveStrategyCheck:  saved.isLiveCheck,
      liveStrategyId:     saved.strategyId ?? undefined,
      liveExecutionType:  saved.executionType,
      livePositionMode:   saved.positionMode,
    });
    if (saved.strategyCandleTypes) {
      setLiveStrategyCandleTypes(saved.strategyCandleTypes);
    }
    if (appDefaults.liveStrategyCheck && saved.isLiveCheck) {
      refreshActiveLiveSettings();
    }
  }, [saveAppDef, appDefaults.liveStrategyCheck, isVirtualActive, refreshActiveLiveSettings]);

  const prevFavoritesRef = useRef<string[]>(getFavorites());

  /** 별표 토글 → localStorage·워치리스트 동기화 */
  useEffect(() => {
    const onFavoritesChanged = (e: Event) => {
      const markets = (e as CustomEvent<string[]>).detail ?? getFavorites();
      prevFavoritesRef.current = markets;
      setFavoritesList(markets);
      setWatchlist(markets);
    };
    window.addEventListener(FAVORITES_CHANGED_EVENT, onFavoritesChanged);
    return () => window.removeEventListener(FAVORITES_CHANGED_EVENT, onFavoritesChanged);
  }, []);

  /** 기존 localStorage 관심종목 → DB 1회 이전 */
  useEffect(() => {
    if (!dbLoaded) return;
    let cancelled = false;
    (async () => {
      try {
        const items = await loadWatchlist();
        if (cancelled) return;
        const dbSyms = items.map(i => i.symbol);
        if (dbSyms.length > 0) {
          setWatchlist(dbSyms);
          setFavoritesList(dbSyms);
          saveFavorites(dbSyms, false);
          return;
        }
        const local = getFavorites();
        if (local.length === 0) return;
        for (const s of local) {
          try { await addWatchlistItem({ symbol: s }); } catch { /* ignore */ }
        }
        const refreshed = await loadWatchlist();
        if (cancelled) return;
        const syms = refreshed.map(i => i.symbol);
        setWatchlist(syms);
        setFavoritesList(syms);
        saveFavorites(syms, false);
      } catch { /* ignore */ }
    })();
    return () => { cancelled = true; };
  }, [dbLoaded]);

  const liveStrategyName = useMemo(() => {
    const sid = liveStrategySettings.strategyId;
    return sid != null ? liveStrategies.find(s => s.id === sid)?.name ?? null : null;
  }, [liveStrategies, liveStrategySettings.strategyId]);

  const liveStrategyNameByMarket = useMemo(() => {
    const map: Record<string, string> = {};
    for (const s of activeLiveSettings) {
      if (s.strategyId != null) {
        const name = liveStrategies.find(st => st.id === s.strategyId)?.name;
        if (name) map[s.market] = name;
      }
    }
    return map;
  }, [activeLiveSettings, liveStrategies]);

  const goToMarketChart = useCallback((market: string) => {
    if (layoutDef.count > 1) {
      slotRefs.current[activeSlot]?.setSymbol(market);
      if (syncOptions.symbol) {
        slotRefs.current.slice(0, layoutDef.count).forEach(r => r?.setSymbol(market));
      }
    } else {
      setSymbol(market);
      setLegend(null);
    }
  }, [layoutDef.count, activeSlot, syncOptions.symbol]);

  // 전략 목록 로드 (실시간 패널용)
  // menuPage가 chart로 복귀하거나 실시간 패널이 열릴 때마다 재로드하여
  // 전략 설정 화면에서 추가/수정된 전략이 즉시 반영되도록 한다.
  const refreshLiveStrategies = useCallback(() => {
    loadStrategiesForLive()
      .then(list => setLiveStrategies(
        list
          .filter(s => s.id != null)
          .map(s => ({ id: s.id as number, name: s.name }))
      ))
      .catch(() => {});
  }, []);

  useEffect(() => {
    refreshLiveStrategies();
  }, [menuPage, showLivePanel, refreshLiveStrategies]);

  const liveNotifierRef = useRef<LiveSignalNotifierHandle>(null);
  const clearLiveMarkers = useCallback(() => {
    liveNotifierRef.current?.clearMarkers();
    managerRef.current?.clearLiveStrategyMarkers();
  }, []);

  useEffect(() => {
    loadBacktestSettings().then(s => { if (s) setBtSettings(s); });
  }, []);

  // 단일차트 모드: 과거 데이터 추가 로드
  const { isLoadingMore: isSingleLoadingMore, loadMoreRef: singleLoadMoreRef } = useHistoryLoader({
    symbol, timeframe, isUpbit: useUpbit, managerRef,
  });

  // 개발 환경: 브라우저 콘솔에서 window.__chartMgr__.logIndicatorLastValues() 로
  // 각 지표의 실제 계산값을 확인 가능 (갱신 여부 진단용)
  useEffect(() => {
    if (import.meta.env.DEV) {
      (window as unknown as Record<string, unknown>)['__chartMgr__'] = managerRef;
    }
  }, []);

  // 멀티 레이아웃: 각 슬롯 ref 배열 (최대 8개)
  const slotRefs = useRef<(ChartSlotHandle | null)[]>(Array(8).fill(null));

  useEffect(() => {
    if (!appSettingsLoaded) return;
    const tf = layoutDef.count > 1 ? activeSlotTf : timeframe;
    managerRef.current?.setDisplayTimezone(displayTimezone, tf);
  }, [appSettingsLoaded, displayTimezone, timeframe, activeSlotTf, layoutDef.count]);

  useEffect(() => {
    if (!appSettingsLoaded) return;
    managerRef.current?.setSeriesPriceLabelsEnabled(chartSeriesPriceLabels);
    slotRefs.current.forEach(slot => slot?.setSeriesPriceLabelsEnabled(chartSeriesPriceLabels));
  }, [appSettingsLoaded, chartSeriesPriceLabels]);

  useEffect(() => {
    if (!appSettingsLoaded) return;
    const wrapper = document.querySelector('.chart-wrapper') as HTMLElement | null;
    const h = wrapper?.clientHeight;
    managerRef.current?.setVolumeVisible(chartVolumeVisible, h);
    slotRefs.current.forEach(slot => slot?.setVolumeVisible(chartVolumeVisible));
  }, [appSettingsLoaded, chartVolumeVisible]);

  useEffect(() => {
    if (!appSettingsLoaded) return;
    managerRef.current?.setPaneSeparatorOptions(chartPaneSeparator);
    slotRefs.current.forEach(slot => slot?.setPaneSeparatorOptions(chartPaneSeparator));
  }, [appSettingsLoaded, chartPaneSeparator]);

  // mainChartStyle 변경 → ChartManager 즉시 적용
  useEffect(() => {
    managerRef.current?.updateMainSeriesStyle(mainChartStyle);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mainChartStyle]);

  // 종목·타임프레임 변경 시 이전 백테스팅 결과·마커 제거 + 진행 중 run 무효화
  useEffect(() => {
    btInvalidateRuns();
    btMarkerPayloadRef.current = null;
    btClear();
    managerRef.current?.clearBacktestMarkers();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [symbol, timeframe]);

  // 백테스팅 시그널 → ref 갱신 후 ChartManager 마커 동기화
  useEffect(() => {
    if (btMatchesChart && btSignals.length > 0) {
      const payload = {
        symbol,
        timeframe,
        signals:   btSignals,
        showPrice: appDefaults.btShowPrice,
      };
      btMarkerPayloadRef.current = payload;
      applyBacktestMarkersWhenReady(payload);
    } else {
      btMarkerPayloadRef.current = null;
      managerRef.current?.clearBacktestMarkers();
    }
    let innerRaf = 0;
    const outerRaf = requestAnimationFrame(() => {
      innerRaf = requestAnimationFrame(syncBacktestMarkers);
    });
    return () => {
      cancelAnimationFrame(outerRaf);
      if (innerRaf) cancelAnimationFrame(innerRaf);
    };
  }, [btMatchesChart, btSignals, btRunSeq, symbol, timeframe, appDefaults.btShowPrice, applyBacktestMarkersWhenReady, syncBacktestMarkers]);

  // ── 시뮬레이션 데이터 (Upbit 아닐 때) ──────────────────────────────────
  const [simBars, setSimBars] = useState<OHLCVBar[]>(() =>
    !isUpbitMarket(initial.symbol) ? generateBars(initial.symbol, initial.timeframe) : []
  );
  useEffect(() => {
    if (useUpbit) return;
    const b = generateBars(symbol, timeframe);
    setSimBars(b);
    setStats(calcStats(b));
    setLegend(null);
  }, [symbol, timeframe, useUpbit]);

  // ── Upbit 실시간 데이터 ─────────────────────────────────────────────────
  // 틱 수신: 동일 캔들 업데이트 → React state 변경 없이 차트만 직접 갱신
  const handleTickUpdate = useCallback((bar: OHLCVBar) => {
    if (barsMarketRef.current !== chartContextRef.current.symbol) return;
    if (!chartLiveReadyRef.current) {
      pendingRealtimeBarRef.current = bar;
      return;
    }
    if (!managerRef.current) {
      pendingRealtimeBarRef.current = bar;
      flashWsDot();
      return;
    }
    managerRef.current.updateBar(bar);
    flashWsDot();   // 수신 중 형광 glow (DOM 직접 조작, re-render 없음)
  }, []);

  // 새 캔들: bars state를 변경하지 않고 차트에 바만 추가 + 인디케이터 last point 갱신
  const handleNewCandle = useCallback((newBar: OHLCVBar) => {
    if (barsMarketRef.current !== chartContextRef.current.symbol) return;
    if (!chartLiveReadyRef.current) {
      pendingRealtimeBarRef.current = newBar;
      return;
    }
    if (!managerRef.current) {
      pendingRealtimeBarRef.current = newBar;
      flashWsDot();
      return;
    }
    managerRef.current.appendBar(newBar);
    flashWsDot();
  }, []);

  const handleCandlesReady = useCallback(() => {
    chartLiveReadyRef.current = true;
    const pending = pendingRealtimeBarRef.current;
    const mgr = managerRef.current;
    if (!pending || !mgr || barsMarketRef.current !== chartContextRef.current.symbol) return;
    pendingRealtimeBarRef.current = null;
    mgr.updateBar(pending);
  }, []);

  const chartRealtimeSource = appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP';
  const { bars: upbitBars, barsMarket: upbitBarsMarket, latestBar, wsStatus, isLoading, error } = useChartRealtimeData(
    symbol,
    timeframe,
    useMemo(() => ({
      onTickUpdate: handleTickUpdate,
      onNewCandle:  handleNewCandle,
      enabled:      useUpbit,
      source:       chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT',
    }), [handleTickUpdate, handleNewCandle, useUpbit, chartRealtimeSource]),
  );

  // 실제로 차트에 넘길 bars (초기 200개, 실시간 수신 후에도 변경 없음)
  const bars = useUpbit ? upbitBars : simBars;
  const barsMarket = useUpbit ? upbitBarsMarket : symbol;
  barsMarketRef.current = barsMarket;

  /** 종목·타임프레임 전환 시 이전 틱 버퍼·실시간 허용 플래그 초기화 (paint 전에 실행) */
  useLayoutEffect(() => {
    chartLiveReadyRef.current = false;
    pendingRealtimeBarRef.current = null;
  }, [symbol, timeframe]);

  // Upbit 초기 로딩 완료 시 stats 업데이트
  useEffect(() => {
    if (useUpbit && upbitBars.length > 0) {
      setStats(calcStats(upbitBars));
      setLegend(null);
    }
  }, [useUpbit, upbitBars]);

  // ── Watchlist 가격 맵 ──────────────────────────────────────────────────
  const priceMap = useMemo(() => {
    const map: Record<string, { price: number; changePct: number }> = {};
    for (const sym of watchlist) {
      const b = isUpbitMarket(sym) ? [] : generateBars(sym, '1D');
      if (b.length >= 2) {
        map[sym] = { price: b[b.length-1].close, changePct: (b[b.length-1].close - b[b.length-2].close) / b[b.length-2].close * 100 };
      }
    }
    return map;
  }, [watchlist]);

  // ── 상태 저장 (localStorage) ───────────────────────────────────────────
  useEffect(() => {
    saveState({ symbol, timeframe, chartType, theme, indicators, watchlist, alertPrices: alerts.map(a => a.price) });
  }, [symbol, timeframe, chartType, theme, indicators, watchlist, alerts]);

  // body 포털(알림 팝업 등) 테마 CSS 변수 동기화
  useEffect(() => {
    syncDocumentTheme(theme);
  }, [theme]);

  // ── DB 워크스페이스 동기화 ─────────────────────────────────────────────
  const workspaceState = useMemo(() => ({
    layoutId: layoutDef.id,
    syncOptions,
    slots: [{
      slotIndex: 0,
      symbol, timeframe, chartType, theme, mode, logScale,
      indicators, drawings, drawingsLocked, drawingsVisible,
      mainChartStyle,
    }],
  }), [layoutDef.id, syncOptions, symbol, timeframe, chartType, theme,
       mode, logScale, indicators, drawings, drawingsLocked, drawingsVisible, mainChartStyle]);

  const { isLoaded: _wsLoaded } = useWorkspacePersist({
    sessionKey,
    state: workspaceState,
    onLoad: (data) => {
      try {
        if (data.layoutId) {
          const def = LAYOUTS.find(l => l.id === data.layoutId);
          if (def) setLayoutDef(def);
        }
        if (data.syncOptions) setSyncOptions(data.syncOptions as typeof syncOptions);
        const slot0 = Array.isArray(data.slots) && data.slots.length > 0
          ? (data.slots[0] as Record<string, unknown>)
          : null;
        if (slot0) {
          if (slot0.symbol)          setSymbol(slot0.symbol as string);
          if (slot0.timeframe)       setTimeframe(slot0.timeframe as Timeframe);
          if (slot0.chartType)       setChartType(slot0.chartType as ChartType);
          if (slot0.theme)           setTheme(slot0.theme as Theme);
          if (slot0.mode)            setMode(slot0.mode as ChartMode);
          if (slot0.logScale != null) setLogScale(Boolean(slot0.logScale));
          if (Array.isArray(slot0.indicators)) {
            setIndicators((slot0.indicators as IndicatorConfig[]).map(ind => {
              if (ind.type === 'SMA') return normalizeSmaConfig(ind);
              if (ind.type === 'IchimokuCloud') return normalizeIchimokuConfig(ind);
              return ind;
            }));
          }
          if (Array.isArray(slot0.drawings))    setDrawings(slot0.drawings as Drawing[]);
          if (slot0.drawingsLocked  != null) setDrawingsLocked(Boolean(slot0.drawingsLocked));
          if (slot0.drawingsVisible != null) setDrawingsVisible(Boolean(slot0.drawingsVisible));
          if (slot0.mainChartStyle)  setMainChartStyle(slot0.mainChartStyle as MainChartStyle);
        }
        // 슬롯 1+ 초기 props 수집 (멀티슬롯 전환 시 각 ChartSlot 에 전달)
        if (Array.isArray(data.slots) && data.slots.length > 1) {
          const extra: typeof slotInitProps = {};
          for (let i = 1; i < data.slots.length; i++) {
            const s = data.slots[i] as Record<string, unknown>;
            extra[i] = {
              symbol:         s.symbol   as string | undefined,
              timeframe:      s.timeframe as Timeframe | undefined,
              indicators:     Array.isArray(s.indicators)
                ? (s.indicators as IndicatorConfig[]).map(ind => {
                    if (ind.type === 'SMA') return normalizeSmaConfig(ind);
                    if (ind.type === 'IchimokuCloud') return normalizeIchimokuConfig(ind);
                    return ind;
                  })
                : undefined,
              mainChartStyle: s.mainChartStyle as MainChartStyle | undefined,
            };
          }
          setSlotInitProps(extra);
        }
      } catch (e) { console.warn('[DB load] parse error', e); }
      setDbLoaded(true);
    },
    onWatchlistLoad: (items: WatchlistItem[]) => {
      const syms = items.map(i => i.symbol);
      if (syms.length > 0) {
        setWatchlist(syms);
        setFavoritesList(syms);
        saveFavorites(syms, false);
      }
    },
    noAutoSave: !dbLoaded,
  });

  /** 로그인·로그아웃 시에만 차트 재마운트 (종목·타임프레임은 TradingChart 내부 reloadAll로 처리) */
  const chartMountKey = useMemo(() => sessionKey, [sessionKey]);

  // DB에 워크스페이스 없음(204) → appDefaults 레이아웃 적용 후 저장 허용
  useEffect(() => {
    if (!_wsLoaded || dbLoaded) return;
    setDbLoaded(true);
    const def = LAYOUTS.find(l => l.id === appDefaults.layoutId);
    if (def) setLayoutDef(def);
  }, [_wsLoaded, dbLoaded, appDefaults.layoutId]);

  // ── 키보드 단축키 ─────────────────────────────────────────────────────
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
      if (e.key === 'Escape')   {
        setDrawingTool('cursor');
        setSelectedDrawingId(null);
        setDrawingCtxToolbar(null);
      }
      if (e.key === 'f' || e.key === 'F') managerRef.current?.fitContent();
      if (e.key === 'l' || e.key === 'L') setLogScale(v => !v);
      if (e.altKey  && e.key === 's') handleScreenshot();
      if (mode === 'trading') {
        if (e.key === 'h' || e.key === 'H') setDrawingTool('hline');
        if (e.key === 'v' || e.key === 'V') setDrawingTool('vline');
        if (e.key === 't' || e.key === 'T') setDrawingTool('trendline');
        if (e.key === 'm' || e.key === 'M') setDrawingTool('measure');
        if (e.key === 'r' || e.key === 'R') setDrawingTool('rect');
        if (e.key === 'z' && !e.ctrlKey) setDrawingTool('fibtz');
        if (e.ctrlKey && e.key === 'z') { e.preventDefault(); handleUndo(); }
        if (e.ctrlKey && (e.key === 'y' || (e.shiftKey && e.key === 'Z'))) { e.preventDefault(); handleRedo(); }
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [mode]);

  useEffect(() => {
    if (mode === 'chart' && drawingTool !== 'zoom' && drawingTool !== 'magnifier') {
      setDrawingTool('cursor');
    }
  }, [mode]);

  useEffect(() => {
    if (menuPage !== 'chart') {
      setShowMarketPanel(false);
      setMobileRightOpen(false);
    }
  }, [menuPage]);

  useEffect(() => {
    if (!isMobile) setMobileRightOpen(false);
  }, [isMobile]);

  // percentScale / showGrid 변경 시 ChartManager 에 즉시 반영
  useEffect(() => { managerRef.current?.setPercentScale(percentScale); }, [percentScale]);
  useEffect(() => { managerRef.current?.setGrid(showGrid); }, [showGrid]);

  // ── 핸들러 ────────────────────────────────────────────────────────────

  /** Time sync: 한 슬롯의 가시 시간 범위가 바뀌면 나머지에 전파 */
  const handleVisibleRangeChange = useCallback((r: { from: number; to: number }) => {
    if (syncOptions.time) setSyncVisibleRange(r);
  }, [syncOptions.time]);

  /** Date range sync: 한 슬롯의 가시 논리 범위가 바뀌면 나머지에 전파 */
  const handleLogicalRangeChange = useCallback((r: { from: number; to: number }) => {
    if (syncOptions.dateRange) setSyncLogicalRange(r);
  }, [syncOptions.dateRange]);

  /**
   * 멀티차트 슬롯 전체 보기:
   * 해당 슬롯의 심볼/타임프레임/지표를 싱글 차트에 적용하고 싱글 레이아웃으로 전환한다.
   */
  const handleSlotFullView = useCallback((slotSymbol: string, slotIdx: number) => {
    const slot = slotRefs.current[slotIdx];
    if (slot) {
      setSymbol(slot.getSymbol());
      setTimeframe(slot.getTimeframe());
      setIndicators(slot.getIndicators().map(i => ({ ...i })));
    } else {
      setSymbol(slotSymbol);
    }
    setLegend(null);
    setSlot0InitProps(null);
    setLayoutDef(LAYOUTS[0]); // 싱글 레이아웃(id: '1')
  }, []);

  // ── 보조지표 단독 전체화면 모드 ────────────────────────────────────────────
  const [focusedIndicatorId, setFocusedIndicatorId] = useState<string | null>(null);

  useEffect(() => {
    setFocusedIndicatorId(null);
  }, [sessionKey]);

  /** 단독 전체화면에서 보여줄 지표 목록 (overlay + 선택된 1개) */
  const effectiveIndicators = useMemo(() => {
    if (!focusedIndicatorId || layoutDef.count > 1) return indicators;
    return indicators.filter(ind => {
      const def = getIndicatorDef(ind.type);
      return def?.overlay || def?.returnsMarkers || ind.id === focusedIndicatorId;
    });
  }, [focusedIndicatorId, indicators, layoutDef.count]);

  const handleExpandIndicator = useCallback((id: string) => {
    setFocusedIndicatorId(id);
  }, []);

  const handleRestoreIndicators = useCallback(() => {
    setFocusedIndicatorId(null);
  }, []);

  /**
   * 보조지표 순서 변경 (드래그 핸들): fromId 를 indicators 배열에서 이동.
   * overlay/markers 지표는 건드리지 않고 비오버레이 지표 위치만 변경됨.
   */
  const handleReorderIndicators = useCallback((fromId: string, insertBeforeId: string | null) => {
    setIndicators(prev => reorderIndicatorGroup(fromId, insertBeforeId, prev));
  }, []);

  const handleMergeIndicators = useCallback((fromId: string, intoId: string) => {
    setIndicators(prev => mergeIndicators(fromId, intoId, prev));
  }, []);

  const handleSplitIndicatorPane = useCallback((hostId: string) => {
    setIndicators(prev => splitIndicatorPane(hostId, prev));
  }, []);

  /**
   * 지표 추가:
   * - 싱글 레이아웃: 메인 차트에 추가
   * - 멀티 레이아웃: 활성 슬롯(ChartSlot ref)에 추가
   */
  const applyChartIndicators = useCallback((chartIndicators: IndicatorConfig[]) => {
    if (layoutDef.count > 1) {
      slotRefs.current[activeSlot]?.setIndicators(chartIndicators);
      setActiveSlotIndicators(chartIndicators.map(i => ({ ...i })));
    } else {
      setIndicators(chartIndicators);
    }
  }, [layoutDef.count, activeSlot]);

  const handleAddIndicator = useCallback((type: string, fromConfig?: IndicatorConfig) => {
    if (layoutDef.count > 1) {
      const slotRef = slotRefs.current[activeSlot];
      if (slotRef) { slotRef.addIndicator(type, fromConfig); return; }
    }
    setIndicators(prev => {
      if (prev.some(i => i.type === type)) return prev;
      const def = getIndicatorDef(type);
      if (!def) return prev;
      let cfg: IndicatorConfig;
      if (fromConfig) {
        cfg = { ...fromConfig, id: newIndId(), type: def.type };
      } else {
        const params = getParams(type, def.defaultParams);
        const { plots, hlines, cloudColors } = getVisualConfig(type, def.plots, def.hlines);
        cfg = {
          id: newIndId(),
          type: def.type,
          params,
          plots,
          hlines,
          ...(cloudColors ? { cloudColors } : {}),
        };
      }
      if (type === 'SMA') {
        cfg = normalizeSmaConfig({
          ...cfg,
          plotVisibility: cfg.plotVisibility ?? createDefaultSmaPlotVisibility(),
        });
      } else if (type === 'IchimokuCloud') {
        cfg = normalizeIchimokuConfig(cfg);
      }
      return [...prev, cfg];
    });
  }, [layoutDef.count, activeSlot, getParams, getVisualConfig]);

  /** 지표 추가 팝업 — 탭 전체 추가 (기존 보조지표 제거 후 탭 지표만 적용) */
  const handleAddIndicators = useCallback((types: string[]) => {
    if (!types.length) return;
    if (layoutDef.count > 1) {
      const slotRef = slotRefs.current[activeSlot];
      if (slotRef) {
        slotRef.addIndicators(types);
        return;
      }
    }
    setIndicators(
      replaceIndicatorConfigsFromTypes(types, getParams, getVisualConfig, newIndId),
    );
  }, [layoutDef.count, activeSlot, getParams, getVisualConfig]);

  /**
   * 지표 제거 (type 기준):
   * - 싱글: 메인 차트에서 제거
   * - 멀티: 활성 슬롯에서 제거
   */
  const handleRemoveByType = useCallback((type: string) => {
    if (layoutDef.count > 1) {
      const slotRef = slotRefs.current[activeSlot];
      if (slotRef) { slotRef.removeIndicatorByType(type); return; }
    }
    setIndicators(prev => removeIndicatorTypeFromList(prev, type));
  }, [layoutDef.count, activeSlot]);

  /**
   * 타임프레임 변경:
   * - 싱글: 메인 차트 변경
   * - 멀티: 활성 슬롯 변경 (Interval Sync 시 전체 슬롯)
   */
  // magnetMode 변경 → 싱글차트 + 멀티슬롯 전체 크로스헤어 모드 즉시 반영
  useEffect(() => {
    const enabled = magnetMode !== 'off';
    // 싱글 레이아웃 메인 차트
    managerRef.current?.setCrosshairMagnet(enabled);
    // 멀티 레이아웃 슬롯 전체
    slotRefs.current.forEach(slot => slot?.setCrosshairMagnet(enabled));
  }, [magnetMode]);

  // 테마 순환: dark → blue → light → dark, DB 에 저장
  const handleThemeToggle = useCallback(() => {
    setTheme(t => {
      const next: Theme = t === 'dark' ? 'blue' : t === 'blue' ? 'light' : 'dark';
      saveAppDef({ defaultTheme: next });
      return next;
    });
  }, [saveAppDef]);

  const handleTimeframe = useCallback((tf: Timeframe) => {
    if (layoutDef.count > 1) {
      slotRefs.current[activeSlot]?.setTimeframe(tf);
      setActiveSlotTf(tf);
      if (syncOptions.interval) {
        slotRefs.current.slice(0, layoutDef.count).forEach(r => r?.setTimeframe(tf));
      }
    } else {
      setTimeframe(tf);
    }
    // 타임프레임 변경 → DB 기본값 갱신
    saveAppDef({ defaultTimeframe: tf });
  }, [layoutDef.count, activeSlot, syncOptions.interval, saveAppDef]);

  const chartIndicatorsForSettings = layoutDef.count > 1 ? activeSlotIndicators : indicators;

  /** 지표 추가 팝업 — 항목별 설정 모달 */
  const handleOpenIndicatorSettings = useCallback((type: string) => {
    const onChart = chartIndicatorsForSettings.find(i => i.type === type);
    setSettingsModalId(onChart ? onChart.id : indicatorSettingsTemplateId(type));
    setCtxToolbar(null);
  }, [chartIndicatorsForSettings]);

  /** 지표 설정 저장 (params/plots/plotVisibility 변경) → 자동으로 재렌더링 */
  const handleIndicatorSave = useCallback((updated: IndicatorConfig) => {
    const fromTemplate =
      isIndicatorSettingsTemplateId(settingsModalId ?? '')
      || updated.id.startsWith('template_');
    saveParams(updated.type, updated.params);
    saveVisual(updated.type, updated.plots, updated.hlines, updated.cloudColors);

    const applyUpdate = (prev: IndicatorConfig[]): IndicatorConfig[] => {
      if (fromTemplate) {
        const merged = enrichIndicatorConfig(updated);
        return prev.map(ind =>
          ind.type === updated.type
            ? { ...merged, id: ind.id, hidden: ind.hidden }
            : ind,
        );
      }
      return prev.map(ind => (ind.id === updated.id ? updated : ind));
    };

    if (layoutDef.count > 1) {
      applyChartIndicators(applyUpdate(activeSlotIndicators));
    } else {
      setIndicators(applyUpdate);
    }
    setSettingsModalId(null);
    setCtxToolbar(null);
  }, [
    saveParams,
    saveVisual,
    settingsModalId,
    layoutDef.count,
    activeSlotIndicators,
    applyChartIndicators,
  ]);

  const bulkSettingsIndicators = layoutDef.count > 1 ? activeSlotIndicators : indicators;

  /** 설정 화면에서 DB 기본값 저장 후 차트 복귀 시 — 전역 기본값을 차트 인스턴스에 일괄 반영 */
  const lastSyncedSettingsRevisionRef = useRef(indicatorSettingsRevision);
  const syncChartsFromSavedDefaults = useCallback(() => {
    const merge = (inds: IndicatorConfig[]) =>
      mergeGlobalDefaultsOntoChartIndicators(inds, getParams, getVisualConfig);

    if (layoutDef.count > 1) {
      slotRefs.current.slice(0, layoutDef.count).forEach(slot => {
        if (!slot) return;
        const current = slot.getIndicators();
        const next = merge(current);
        if (chartIndicatorsNeedDefaultsRefresh(current, next)) {
          slot.setIndicators(next);
        }
      });
      const activeInds = slotRefs.current[activeSlot]?.getIndicators() ?? activeSlotIndicators;
      const mergedActive = merge(activeInds);
      if (chartIndicatorsNeedDefaultsRefresh(activeInds, mergedActive)) {
        setActiveSlotIndicators(mergedActive.map(i => ({ ...i })));
      }
    } else {
      setIndicators(prev => {
        const next = merge(prev);
        return chartIndicatorsNeedDefaultsRefresh(prev, next) ? next : prev;
      });
    }
  }, [layoutDef.count, activeSlot, activeSlotIndicators, getParams, getVisualConfig]);

  useLayoutEffect(() => {
    if (!chartVisible) return;
    if (indicatorSettingsRevision <= lastSyncedSettingsRevisionRef.current) return;
    syncChartsFromSavedDefaults();
    lastSyncedSettingsRevisionRef.current = indicatorSettingsRevision;
  }, [chartVisible, indicatorSettingsRevision, syncChartsFromSavedDefaults]);

  /** 보조지표 일괄 설정 적용 — 전체 지표 DB + 차트 인스턴스 병합 */
  const handleBulkIndicatorApply = useCallback((result: {
    chartIndicators: IndicatorConfig[];
    allConfigs: IndicatorConfig[];
  }) => {
    const { chartIndicators, allConfigs } = result;
    applyChartIndicators(chartIndicators);
    allConfigs.forEach(ind => {
      saveParams(ind.type, ind.params);
      saveVisual(ind.type, ind.plots, ind.hlines, ind.cloudColors);
    });
    setShowBulkIndSettings(false);
  }, [applyChartIndicators, saveParams, saveVisual]);

  /** 지표 숨김/표시 토글 */
  const handleToggleIndicatorHidden = useCallback((id: string) => {
    setIndicators(prev => prev.map(ind =>
      ind.id === id ? { ...ind, hidden: !ind.hidden } : ind
    ));
  }, []);

  /** 지표 ID로 삭제 */
  const handleDuplicateIndicator = useCallback((sourceId: string) => {
    if (layoutDef.count > 1) {
      slotRefs.current[activeSlot]?.duplicateIndicator(sourceId);
      return;
    }
    setIndicators(prev => duplicateIndicatorInList(sourceId, prev, newIndId) ?? prev);
  }, [layoutDef.count, activeSlot]);

  const handleRemoveIndicatorById = useCallback((id: string) => {
    setIndicators(prev => removeIndicatorFromList(prev, id));
    setFocusedIndicatorId(prev => prev === id ? null : prev);
    setCtxToolbar(null);
    setSettingsModalId(null);
  }, []);

  const handleScreenshot = () => {
    const url = managerRef.current?.screenshot();
    if (!url) return;
    const a = document.createElement('a');
    a.href = url;
    a.download = `chart_${symbol}_${timeframe}_${Date.now()}.png`;
    a.click();
  };

  const handleFullscreen = () => {
    if (!document.fullscreenElement) document.documentElement.requestFullscreen().catch(() => {});
    else document.exitFullscreen().catch(() => {});
  };

  // 실시간 모드: latestBar 우선, 없으면 마지막 bars 사용
  const lastKnownBar = useUpbit
    ? (latestBar ?? (bars.length > 0 ? bars[bars.length - 1] : null))
    : (bars.length > 0 ? bars[bars.length - 1] : null);
  const currentBar = legend ?? lastKnownBar;
  const isUp = currentBar ? currentBar.close >= currentBar.open : true;
  const prevClose = bars.length > 1 ? bars[bars.length - 2].close : currentBar?.open ?? 0;
  const dailyChange    = currentBar ? currentBar.close - prevClose : 0;
  const dailyChangePct = prevClose > 0 ? dailyChange / prevClose * 100 : 0;

  const openNotificationsPage = useCallback(() => guardedSetMenuPage('notifications'), [guardedSetMenuPage]);

  if (!appEntered) {
    return (
      <SplashScreen
        onLoginSuccess={handleLoginSuccess}
        onGuest={handleGuestEnter}
      />
    );
  }

  return (
    <TradeNotificationProvider
      popupEnabled={appDefaults.tradeAlertPopup ?? true}
      soundEnabled={appDefaults.tradeAlertSoundEnabled ?? true}
      soundId={appDefaults.tradeAlertSound ?? 'bell'}
    >
    <VerificationIssueNotificationProvider
      enabled={canMenu('verification-board')}
      notifyEnabled={appDefaults.verificationIssueNotify}
    >
    <div className={`app ${theme} mode-${mode}${isMobile ? ' app--mobile' : ''}`}>
      <LiveSignalNotifier
        ref={liveNotifierRef}
        markets={monitoredMarkets}
        marketSubscriptions={liveStompSubscriptions}
        activeMarket={symbol}
        enabled={monitoredMarkets.length > 0}
        popupEnabled={appDefaults.tradeAlertPopup ?? true}
        strategyId={liveStrategySettings.strategyId ?? undefined}
        executionType={liveStrategySettings.executionType ?? 'CANDLE_CLOSE'}
        strategyName={liveStrategyName}
        strategyNameByMarket={liveStrategyNameByMarket}
        executionTypeByMarket={Object.fromEntries(
          activeLiveSettings.map(s => [s.market, s.executionType ?? 'CANDLE_CLOSE']),
        )}
        chartMarkersActive={menuPage === 'chart'}
        onMarkersChange={markers => {
          managerRef.current?.setLiveStrategyMarkers(markers, appDefaults.btShowPrice);
        }}
      />

      {/* ── 최상단 글로벌 메뉴바 ─────────────────────────────────────── */}
      <NotifyTopMenuBar
        activePage={menuPage}
        theme={theme}
        onPage={guardedSetMenuPage}
        onTheme={handleThemeToggle}
        authUser={authUser}
        guestMode={guestMode}
        menuPermissions={menuPermissions}
        onLoginClick={() => setLoginOpen(true)}
        onLogout={handleLogout}
        tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
        tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
        tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
        onTradeAlertPopupPosition={v => saveAppDef({ tradeAlertPopupPosition: v })}
        onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
        onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
      />

      <LoginModal
        open={loginOpen}
        onClose={() => setLoginOpen(false)}
        onSuccess={handleLoginSuccess}
      />

      {/* ── 투자전략 화면 ──────────────────────────────────────────────── */}
      {menuPage === 'dashboard' && (
        <DashboardPage theme={theme} onGoChart={m => { goToMarketChart(m); setMenuPage('chart'); }} />
      )}

      {menuPage === 'strategy' && (
        <StrategyPage theme={theme} activeIndicators={indicators} />
      )}

      {menuPage === 'strategy-editor' && (
        <StrategyEditorPage theme={theme} />
      )}

      {/* ── 백테스팅 이력 화면 ─────────────────────────────────────────── */}
      {menuPage === 'backtest' && (
        <BacktestHistoryPage theme={theme} />
      )}

      {menuPage === 'paper' && (
        <PaperTradingPage
          theme={theme}
          tickers={marketTickers}
          refreshKey={paperRefreshKey}
          defaultMarket={symbol}
          paperTradingEnabled={paperTradingEnabled}
          paperAutoTradeEnabled={paperAutoTradeEnabled}
          onPaperOrderFilled={handlePaperOrderFilled}
          onGoChart={m => {
            goToMarketChart(m);
            setMenuPage('chart');
          }}
        />
      )}

      {menuPage === 'virtual' && (
        <VirtualTradingPage
          theme={theme}
          tickers={marketTickers}
          defaultMarket={symbol}
          paperTradingEnabled={paperTradingEnabled}
          paperAutoTradeEnabled={paperAutoTradeEnabled}
          paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
          onPaperAutoTradeEnabled={v => saveAppDef({ paperAutoTradeEnabled: v })}
          onPaperOrderFilled={handlePaperOrderFilled}
        />
      )}

      {menuPage === 'trend-search' && (
        <TrendSearchPage
          theme={theme}
          chartRealtimeSource={(appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP') as ChartRealtimeSource}
          chartSeriesPriceLabels={appDefaults.chartSeriesPriceLabels}
          tickers={marketTickers}
        />
      )}

      {menuPage === 'verification-board' && (
        <VerificationBoardPage
          theme={theme}
          focusIssueId={verificationFocusIssueId}
          onFocusIssueConsumed={() => setVerificationFocusIssueId(null)}
        />
      )}

      {/* ── 설정 화면 ──────────────────────────────────────────────────── */}
      {menuPage === 'notifications' && (
        <TradeNotificationListPage
          theme={theme}
          tickers={marketTickers}
          defaultMarket={symbol}
          paperTradingEnabled={paperTradingEnabled}
          paperAutoTradeEnabled={paperAutoTradeEnabled}
          onPaperOrderFilled={handlePaperOrderFilled}
          onGoToChart={market => {
            goToMarketChart(market);
            setMenuPage('chart');
          }}
        />
      )}

      {menuPage === 'settings' && canMenu('settings') && (
        <SettingsPage
          theme={theme}
          menuPermissions={menuPermissions}
          magnetMode={magnetMode}
          onMagnetMode={m => setMagnetMode(m)}
          btAutoPopup={appDefaults.btAutoPopup}
          onBtAutoPopup={v => saveAppDef({ btAutoPopup: v })}
          btShowPrice={appDefaults.btShowPrice}
          onBtShowPrice={v => saveAppDef({ btShowPrice: v })}
          liveMarket={symbol}
          liveStrategies={liveStrategies}
          onClearLiveMarkers={() => { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }}
          tradeAlertPopup={appDefaults.tradeAlertPopup}
          onTradeAlertPopup={v => saveAppDef({ tradeAlertPopup: v })}
          tradeAlertSoundEnabled={appDefaults.tradeAlertSoundEnabled}
          onTradeAlertSoundEnabled={v => saveAppDef({ tradeAlertSoundEnabled: v })}
          tradeAlertSound={appDefaults.tradeAlertSound}
          onTradeAlertSound={v => saveAppDef({ tradeAlertSound: v })}
          tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
          onTradeAlertPopupPosition={v => saveAppDef({ tradeAlertPopupPosition: v })}
          tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
          onTradeAlertPopupLayout={v => saveAppDef({ tradeAlertPopupLayout: v })}
          tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
          onTradeAlertPopupGridCols={v => saveAppDef({ tradeAlertPopupGridCols: v })}
          liveSettings={liveStrategySettings}
          onLiveSettingsChange={handleLiveSettingsChange}
          watchlistCount={watchlist.length}
          monitoredMarkets={monitoredMarkets}
          virtualDriven={isVirtualActive}
          getIndicatorParams={getIndicatorParams}
          saveIndicatorParams={saveIndicatorParams}
          getIndicatorVisual={getIndicatorVisual}
          saveIndicatorVisual={saveIndicatorVisual}
          saveAllIndicatorDefaults={saveAllIndicatorDefaults}
          indicatorSettingsRevision={indicatorSettingsRevision}
          chartIndicators={bulkSettingsIndicators}
          onAddIndicatorToChart={handleAddIndicator}
          onRemoveIndicatorFromChart={handleRemoveByType}
          chartSeriesPriceLabels={chartSeriesPriceLabels}
          onChartSeriesPriceLabels={v => {
            saveAppDef({ chartSeriesPriceLabels: v });
            managerRef.current?.setSeriesPriceLabelsEnabled(v);
            slotRefs.current.forEach(slot => slot?.setSeriesPriceLabelsEnabled(v));
          }}
          chartVolumeVisible={chartVolumeVisible}
          onChartVolumeVisible={v => {
            saveAppDef({ chartVolumeVisible: v });
            const wrapper = document.querySelector('.chart-wrapper') as HTMLElement | null;
            managerRef.current?.setVolumeVisible(v, wrapper?.clientHeight);
            slotRefs.current.forEach(slot => slot?.setVolumeVisible(v));
          }}
          chartLiveReceiveHighlight={chartLiveReceiveHighlight}
          onChartLiveReceiveHighlight={v => saveAppDef({ chartLiveReceiveHighlight: v })}
          chartLegendOptions={chartLegendOptions}
          onChartLegendOptionsChange={patch => {
            saveAppDef({
              chartLegendOptions: { ...chartLegendOptions, ...patch } as unknown as Record<string, boolean>,
            });
          }}
          chartPaneSeparator={chartPaneSeparator}
          onChartPaneSeparatorChange={opts => {
            saveAppDef({ chartPaneSeparator: opts });
            managerRef.current?.setPaneSeparatorOptions(opts);
            slotRefs.current.forEach(slot => slot?.setPaneSeparatorOptions(opts));
          }}
          paperTradingEnabled={paperTradingEnabled}
          onPaperTradingEnabled={v => saveAppDef({ paperTradingEnabled: v })}
          paperInitialCapital={appDefaults.paperInitialCapital}
          onPaperInitialCapital={v => saveAppDef({ paperInitialCapital: v })}
          paperFeeRatePct={appDefaults.paperFeeRatePct}
          onPaperFeeRatePct={v => saveAppDef({ paperFeeRatePct: v })}
          paperSlippagePct={appDefaults.paperSlippagePct}
          onPaperSlippagePct={v => saveAppDef({ paperSlippagePct: v })}
          paperMinOrderKrw={appDefaults.paperMinOrderKrw}
          onPaperMinOrderKrw={v => saveAppDef({ paperMinOrderKrw: v })}
          paperAutoTradeEnabled={appDefaults.paperAutoTradeEnabled}
          onPaperAutoTradeEnabled={v => saveAppDef({ paperAutoTradeEnabled: v })}
          paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
          onPaperAutoTradeBudgetPct={v => saveAppDef({ paperAutoTradeBudgetPct: v })}
          virtualTargetMaxCount={appDefaults.virtualTargetMaxCount}
          onVirtualTargetMaxCount={v => saveAppDef({ virtualTargetMaxCount: clampVirtualTargetMax(v) })}
          trendSearchSettings={appDefaults.trendSearchSettings}
          onTrendSearchSettingsChange={s => saveAppDef({
            trendSearchSettings: resolveTrendSearchAppSettings(s),
          })}
          onPaperAccountReset={async () => {
            await resetPaperAccount();
            await refreshPaperAccount();
          }}
          tradingMode={appDefaults.tradingMode}
          onTradingMode={v => saveAppDef({ tradingMode: v })}
          liveAutoTradeEnabled={appDefaults.liveAutoTradeEnabled}
          onLiveAutoTradeEnabled={v => saveAppDef({ liveAutoTradeEnabled: v })}
          hasUpbitKeys={appDefaults.hasUpbitKeys}
          upbitAccessKeyMasked={appDefaults.upbitAccessKeyMasked}
          onUpbitKeys={(access, secret) => {
            const patch: Parameters<typeof saveAppDef>[0] = {};
            const a = access.trim();
            const s = secret.trim();
            const isMasked = (v: string) => v.startsWith('····') || v.startsWith('****');
            if (a && !isMasked(a)) patch.upbitAccessKey = a;
            if (s && !isMasked(s)) patch.upbitSecretKey = s;
            if (Object.keys(patch).length) saveAppDef(patch);
          }}
          chartRealtimeSource={appDefaults.chartRealtimeSource}
          onChartRealtimeSource={v => saveAppDef({ chartRealtimeSource: v })}
          liveAutoTradeBudgetPct={appDefaults.liveAutoTradeBudgetPct}
          onLiveAutoTradeBudgetPct={v => saveAppDef({ liveAutoTradeBudgetPct: v })}
          fcmPushEnabled={appDefaults.fcmPushEnabled}
          onFcmPushEnabled={v => saveAppDef({ fcmPushEnabled: v })}
          displayTimezone={displayTimezone}
          onDisplayTimezoneChange={handleTimezoneChange}
          verificationIssueNotify={appDefaults.verificationIssueNotify}
          onVerificationIssueNotify={v => saveAppDef({ verificationIssueNotify: v })}
          onFcmTest={async () => {
            const { sendFcmTest } = await import('./utils/backendApi');
            const r = await sendFcmTest();
            window.alert(r?.available ? 'FCM 테스트 요청을 보냈습니다.' : 'FCM 서버가 비활성 상태입니다.');
          }}
        />
      )}

      {/* ── 실시간 차트 화면 ────────────────────────────────────────────── */}
      <div style={{ display: menuPage === 'chart' ? 'contents' : 'none' }}>

      <ChartToolbarNotify
        onOpenNotifyList={openNotificationsPage}
        symbol={symbol} timeframe={layoutDef.count > 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={handleThemeToggle}
        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}
        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')}
        onFullscreen={handleFullscreen}
        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<string, boolean> });
            return next;
          });
        }}
        magnifierActive={magnifierActive}
        onToggleMagnifier={() => setMagnifierActive(v => !v)}
        marketPanelActive={showMarketPanel}
        onToggleMarketPanel={() => {
          setShowMarketPanel(v => {
            const next = !v;
            if (isMobile && next) setMobileRightOpen(false);
            return next;
          });
        }}
        backtestActive={btMatchesChart}
        backtestRunning={btRunning}
        backtestRunningId={undefined}
        onRunBacktest={menuPage === 'chart' && layoutDef.count === 1
          ? 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;
                const payload = {
                  symbol:    runSymbol,
                  timeframe: runTf,
                  signals:   r.signals,
                  showPrice,
                };
                btMarkerPayloadRef.current = payload;
                applyBacktestMarkersWhenReady(payload);
              }
              if (appDefaults.btAutoPopup) setShowBtResult(true);
            }
          : undefined}
        onOpenBtSettings={() => setShowBtSettings(true)}
        onOpenBtResults={() => setShowBtResult(true)}
        hasBtResult={btMatchesChart}
        onClearBtMarkers={() => {
          btMarkerPayloadRef.current = null;
          btClear();
          managerRef.current?.clearBacktestMarkers();
        }}
        onToggleLiveStrategy={() => setShowLivePanel(v => !v)}
        liveStrategyActive={showLivePanel || monitoredMarkets.length > 0}
      />

      {/* 업비트 연결 상태 바 */}
      {useUpbit && (
        <div className={`upbit-bar ${wsStatus}`}>
          <WsBadge status={wsStatus} isUpbit={useUpbit} />
          <span className="upbit-market">{getKoreanName(symbol)} <span style={{opacity:0.5, fontSize:'11px'}}>{symbol}</span></span>
          {isLoading && <span className="upbit-loading">데이터 로딩 중...</span>}
          {error && <span className="upbit-error">⚠ {error}</span>}
          {!isLoading && !error && currentBar && (
            <>
              <span className="upbit-price" style={{ color: isUp ? 'var(--up)' : 'var(--down)' }}>
                {currentBar.close.toLocaleString()} KRW
              </span>
              <span className={`upbit-change ${isUp ? 'up' : 'down'}`}>
                {isUp ? '▲' : '▼'} {Math.abs(dailyChangePct).toFixed(2)}%
              </span>
            </>
          )}
          <span className="upbit-hint">💡 업비트 마켓: KRW-BTC, KRW-ETH, KRW-XRP ...</span>
        </div>
      )}

      {isMobile && (showMarketPanel || mobileRightOpen) && (
        <button
          type="button"
          className="mobile-panel-backdrop"
          aria-label="패널 닫기"
          onClick={() => { setShowMarketPanel(false); setMobileRightOpen(false); }}
        />
      )}

      <div className="tv-main">
        {/* ── 좌측 마켓 패널 ─────────────────────────────────────────── */}
        <MarketPanel
          open={showMarketPanel}
          tickers={marketTickers}
          loading={marketLoading}
          usdRate={usdRate}
          activeSymbol={layoutDef.count > 1 ? activeSlotSymbol : symbol}
          onSymbolSelect={s => {
            if (layoutDef.count > 1) {
              // 다중차트: 현재 활성 슬롯에만 적용, 호가창 심볼도 즉시 갱신
              slotRefs.current[activeSlot]?.setSymbol(s);
              setActiveSlotSymbol(s);
            } else {
              setSymbol(s);
              setLegend(null);
            }
          }}
        />

        {/* 마켓 패널 접기/펼치기 핸들 – tv-main flex 자식으로 항상 표시 */}
        <button
          className={`mp-panel-handle ${showMarketPanel ? 'mp-panel-handle--open' : ''}`}
          onClick={() => setShowMarketPanel(v => !v)}
          title={showMarketPanel ? '마켓 패널 닫기' : '마켓 패널 열기'}
        >
          <svg width="8" height="14" viewBox="0 0 8 14" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
            {showMarketPanel
              ? <polyline points="6,1 2,7 6,13" />
              : <polyline points="2,1 6,7 2,13" />}
          </svg>
        </button>

        <DrawingToolbar
          activeTool={drawingTool} mode={mode}
          onTool={setDrawingTool}
          onClearAll={() => { 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 && (
          <div
            className="multi-chart-grid"
            style={{
              gridTemplateColumns: layoutDef.cols,
              gridTemplateRows:    layoutDef.rows,
              gridTemplateAreas:   layoutDef.areas,
            }}
          >
            {/* 슬롯 0 = 메인 차트 영역 (a) */}
            <div className="multi-slot-wrap" style={{ gridArea: 'a', minHeight: 0, minWidth: 0 }}>
              <ChartSlot
                ref={el => { 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)}
                chartSeriesPriceLabels={chartSeriesPriceLabels}
                chartVolumeVisible={chartVolumeVisible}
                chartPaneSeparator={chartPaneSeparator}
                chartLiveReceiveHighlight={chartLiveReceiveHighlight}
                chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'}
                displayTimezone={displayTimezone}
                compactMode
                chartVisible={chartVisible}
              />
            </div>
            {/* 슬롯 1~7 */}
            {(['b','c','d','e','f','g','h'] as const)
              .slice(0, layoutDef.count - 1)
              .map((area, i) => (
                <div key={area} className="multi-slot-wrap" style={{ gridArea: area, minHeight: 0, minWidth: 0 }}>
                  <ChartSlot
                    ref={el => { 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)}
                    chartSeriesPriceLabels={chartSeriesPriceLabels}
                    chartVolumeVisible={chartVolumeVisible}
                    chartPaneSeparator={chartPaneSeparator}
                    chartLiveReceiveHighlight={chartLiveReceiveHighlight}
                    chartRealtimeSource={chartRealtimeSource as 'BACKEND_STOMP' | 'UPBIT_DIRECT'}
                    displayTimezone={displayTimezone}
                    compactMode
                    chartVisible={chartVisible}
                  />
                </div>
              ))}
          </div>
        )}

        {/* ── 싱글 레이아웃 (기존) ─────────────────────────────────────── */}
        <div className="tv-chart-col" style={{ display: layoutDef.count > 1 ? 'none' : undefined }}>
          <ChartLegendBar
            visibility={chartLegendOptions}
            symbol={symbol}
            timeframe={timeframe}
            useUpbit={useUpbit}
            wsStatus={wsStatus}
            mode={mode}
            currentBar={currentBar}
            isUp={isUp}
            dailyChange={dailyChange}
            dailyChangePct={dailyChangePct}
            indicators={indicators}
            legend={legend}
          />

          {/* 로딩 오버레이 */}
          {useUpbit && isLoading && (
            <div className="chart-loading">
              <div className="loading-spinner" />
              <span>업비트 데이터 로딩 중...</span>
            </div>
          )}
          {isSingleLoadingMore && (
            <div className="chart-history-loading">
              <div className="loading-spinner" style={{ width: 14, height: 14 }} />
              <span>과거 데이터 로딩...</span>
            </div>
          )}

          <TradingChart
            key={chartMountKey}
            chartVisible={chartVisible}
            bars={bars} barsMarket={barsMarket} market={symbol} timeframe={timeframe} displayTimezone={displayTimezone}
            chartType={chartType} theme={theme} mode={mode}
            indicators={effectiveIndicators} drawingTool={drawingTool}
            drawings={drawings} logScale={logScale}
            drawingsLocked={drawingsLocked}
            drawingsVisible={drawingsVisible}
            onCrosshair={setLegend}
            onTradeOrderRequest={applyTradeFill}
            onDataLoaded={() => 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 {
                pendingRealtimeBarRef.current = null;
              }
              mgr.setSeriesPriceLabelsEnabled(chartSeriesPriceLabels);
              const wrapper = document.querySelector('.chart-wrapper') as HTMLElement | null;
              mgr.setVolumeVisible(chartVolumeVisible, wrapper?.clientHeight);
              mgr.setPaneSeparatorOptions(chartPaneSeparator);
              mgr.setDisplayTimezone(displayTimezone, timeframe);
              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}
            volumeVisible={chartVolumeVisible}
            paneSeparatorOptions={chartPaneSeparator}
          />

          {/* 백테스팅 결과 통계 배지 */}
          {btMatchesChart && layoutDef.count === 1 && (
            <BacktestPanel
              stats={btStats}
              signalCount={btSignals.length}
              onClear={() => {
                btMarkerPayloadRef.current = null;
                btClear();
                managerRef.current?.clearBacktestMarkers();
              }}
            />
          )}

          {/* 실시간 전략 체크 패널 */}
          {showLivePanel && menuPage === 'chart' && layoutDef.count === 1 && (
            <LiveStrategyPanel
              theme={theme}
              market={symbol}
              strategies={liveStrategies}
              settings={liveStrategySettings}
              watchlistCount={isVirtualActive ? monitoredMarkets.length : watchlist.length}
              monitoredMarkets={monitoredMarkets}
              virtualDriven={isVirtualActive}
              onClearMarkers={() => { clearLiveMarkers(); managerRef.current?.clearLiveStrategyMarkers(); }}
              onSettingsChange={handleLiveSettingsChange}
              onClose={() => setShowLivePanel(false)}
            />
          )}

          {/* 백테스팅 설정 모달 */}
          {showBtSettings && (
            <BacktestSettingsModal
              onClose={() => setShowBtSettings(false)}
              onSettingsChange={s => { setBtSettings(s); setShowBtSettings(false); }}
            />
          )}

          {/* 백테스팅 결과 모달 */}
          {showBtResult && (
            <BacktestResultModal
              analysis={btAnalysis ?? null}
              stats={btStats ?? null}
              signals={btSignals}
              strategyName={btStrategyName}
              symbol={symbol}
              timeframe={timeframe}
              barCount={bars.length}
              onClose={() => setShowBtResult(false)}
            />
          )}

          {/* 시리즈 클릭/호버 컨텍스트 툴바 */}
          {ctxToolbar && (() => {
            const cfg = ctxToolbar.entryId !== '__main__'
              ? indicators.find(i => i.id === ctxToolbar.entryId) ?? null
              : null;
            return (
              <IndicatorContextToolbar
                indicatorId={ctxToolbar.entryId}
                config={cfg}
                chartType={chartType}
                screenX={ctxToolbar.screenX}
                screenY={ctxToolbar.screenY}
                onMouseEnterToolbar={clearHoverTimer}
                onMouseLeaveToolbar={handleLeavePaneLegend}
                onSettings={() => {
                  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 (
              <DrawingContextToolbar
                drawing={d}
                screenX={drawingCtxToolbar.screenX}
                screenY={drawingCtxToolbar.screenY}
                onSettings={() => {
                  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 (
                <FibTZSettingsModal
                  drawing={d}
                  onSave={handleSaveDrawing}
                  onCancel={handleCancelDrawing}
                />
              );
            }
            return (
              <DrawingSettingsModal
                drawing={d}
                onSave={updated => {
                  setDrawings(prev => prev.map(dr => dr.id === updated.id ? updated : dr));
                  setDrawingSettingsId(null);
                  setSelectedDrawingId(updated.id);
                }}
                onCancel={() => {
                  setDrawingSettingsId(null);
                }}
              />
            );
          })()}

          {/* 메인 차트 설정 모달 */}
          {showMainChartModal && (
            <MainChartSettingsModal
              style={mainChartStyle}
              onSave={handleMainChartSave}
              onCancel={() => setShowMainChartModal(false)}
            />
          )}

          {/* 보조지표 일괄 설정 */}
          {showBulkIndSettings && (
            <IndicatorBulkSettingsModal
              open={showBulkIndSettings}
              indicators={bulkSettingsIndicators}
              chartMarket={symbol}
              getParams={getParams}
              getVisualConfig={getVisualConfig}
              onApply={handleBulkIndicatorApply}
              onLiveChartChange={applyChartIndicators}
              onClose={() => 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 (
              <IndicatorSettingsModal
                config={cfg}
                chartMarket={symbol}
                onSave={handleIndicatorSave}
                onCancel={() => setSettingsModalId(null)}
              />
            );
          })()}

        </div>

        {/* ── 우측 슬라이드 패널 (매수/매도 + 호가) ───────────────────── */}
        <RightSidePanel
          open={isMobile ? mobileRightOpen : undefined}
          onOpenChange={isMobile ? setMobileRightOpen : undefined}
          activeTab={isMobile ? mobileRightTab : undefined}
          onTabChange={isMobile ? setMobileRightTab : undefined}
          market={chartSymbol}
          tradePrice={chartTicker?.tradePrice ?? null}
          asks={orderbook.asks}
          bids={orderbook.bids}
          totalAskSize={orderbook.totalAskSize}
          totalBidSize={orderbook.totalBidSize}
          wsStatus={obWsStatus}
          bestAsk={spread.bestAsk}
          bestBid={spread.bestBid}
          spread={spread.spread}
          spreadPct={spread.pct}
          prevClose={orderbookPrevClose}
          tickerInfo={chartTicker ? {
            tradePrice: chartTicker.tradePrice,
            changeRate: chartTicker.changeRate,
            changePrice: chartTicker.changePrice,
            accTradePrice24: chartTicker.accTradePrice24,
            accTradeVolume24: chartTicker.accTradeVolume24,
            highPrice: chartTicker.highPrice,
            lowPrice: chartTicker.lowPrice,
            openingPrice: chartTicker.openingPrice,
          } : undefined}
          fillRequest={tradeFill}
          onOrderbookPick={handleOrderbookPick}
          onMarketSelect={handleTradeMarketSelect}
          availableKrw={paperCashBalance}
          availableCoinQty={paperCoinQty}
          paperTradingEnabled={paperTradingEnabled}
          paperAutoTradeEnabled={paperAutoTradeEnabled}
          onPaperOrderFilled={() => {
            refreshPaperAccount(symbol);
            handlePaperOrderFilled();
          }}
        />

      </div>

      {/* ── 플로팅 패널: position:fixed로 항상 표시 (싱글/멀티 모드 공통) ─── */}
      {showObjectTree && (
        <ObjectTree
          drawings={drawings}
          onToggleVisible={id => 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 && <StatsPanel stats={stats} symbol={symbol} onClose={() => setShowStats(false)} />}
      {showWatch && (
        <WatchList
          watchlist={watchlist} currentSymbol={symbol} priceMap={priceMap}
          onSelect={s => { 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 && (
        <AlertManager
          alerts={alerts} currentPrice={currentBar?.close ?? 0}
          onAdd={(price, label) => { 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 && (
        <MobileChartDock
          marketOpen={showMarketPanel}
          rightOpen={mobileRightOpen}
          rightTab={mobileRightTab}
          drawingTool={drawingTool}
          onToggleMarket={() => {
            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}
        />
      )}

      <BottomBar
        timeframe={layoutDef.count > 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}
      />

      </div>{/* chart-screen */}

      <AppNotificationLayer
        menuPage={menuPage}
        onPage={guardedSetMenuPage}
        onGoToChart={goToMarketChart}
        onGoToVerificationIssue={issueId => {
          setVerificationFocusIssueId(issueId);
          guardedSetMenuPage('verification-board');
        }}
        tradingMode={appDefaults.tradingMode}
        hasUpbitKeys={appDefaults.hasUpbitKeys}
        paperTradingEnabled={paperTradingEnabled}
        liveAutoTradeBudgetPct={appDefaults.liveAutoTradeBudgetPct}
        paperAutoTradeBudgetPct={appDefaults.paperAutoTradeBudgetPct}
        tradeAlertPopupPosition={appDefaults.tradeAlertPopupPosition}
        tradeAlertPopupLayout={appDefaults.tradeAlertPopupLayout}
        tradeAlertPopupGridCols={appDefaults.tradeAlertPopupGridCols}
        onOrderDone={() => {
          refreshPaperAccount(symbol);
          handlePaperOrderFilled();
        }}
      />

    </div>
    </VerificationIssueNotificationProvider>
    </TradeNotificationProvider>
  );
}

export default App;
