/**
 * 전략편집기 — 노드 기반 논리 트리 UI (React Flow)
 */
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { ReactFlowProvider } from '@xyflow/react';
import DraggableModalFrame from './DraggableModalFrame';
import type { Theme } from '../types/index';
import { hasRegisteredUser, loadStrategies, loadStrategy, saveStrategy, deleteStrategy, runBacktest, loadBacktestSettings, loadIndicatorSettings, type StrategyDto as ApiStrategyDto } from '../utils/backendApi';
import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicators';
import { fetchBacktestCandleBundle } from '../utils/backtestWarmup';
import { syncStrategyTimeframesFromLayoutIfNeeded } from '../utils/strategyTimeframeSync';
import type { LogicNode } from '../utils/strategyTypes';
import {
  buildStrategyEditorDefFromSettings,
  loadStratsLocal,
  saveStratsLocal,
  mergeAtRoot,
  makeNode,
  updateNode,
  CondEditor,
  genId,
  type StrategyDto,
} from '../utils/strategyEditorShared';
import { syncLogicNodeWithGlobalDef, syncLogicRootWithGlobalDef } from '../utils/strategyDefSync';
import { useIndicatorSettings } from '../hooks/useIndicatorSettings';
import { findLogicNode, getIndicatorPeriodLabel } from '../utils/strategyFlowLayout';
import {
  decodeConditionForEditor,
  alignBuySellStartCandleTypesForSave,
  encodeConditionForSave,
  mergeStartMetaForLoad,
  addExtraStartSection,
  hasMultipleStartSections,
  normalizeStartCombineOp,
  updateStartCombineOp,
  updateStartCandleTypes,
  updateBranchRoot,
  type EditorConditionState,
} from '../utils/strategyConditionSerde';
import { migrateLogicRootForEditor } from '../utils/strategyConditionNormalize';
import { persistStrategyEditorState } from '../utils/strategyTimeframeSync';
import {
  START_NODE_ID,
  defaultStartMeta,
  type StartCombineOp,
} from '../utils/strategyStartNodes';
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
import TemplatePaletteTab, { type TemplatePaletteRow } from './strategyEditor/TemplatePaletteTab';
import SidewaysFilterPaletteTab from './strategyEditor/SidewaysFilterPaletteTab';
import { buildSidewaysFilterNode, loadSidewaysPaletteItems } from '../utils/sidewaysFilterPaletteStorage';
import {
  loadPaletteItems,
  type PaletteItem,
} from '../utils/strategyPaletteStorage';
import {
  buildCompositePaletteTemplateRows,
  buildSidewaysPaletteTemplateRows,
} from '../utils/strategyEditorTemplatePaletteRows';
import {
  buildPriceExtremeBreakoutTree,
  isPriceExtremeBreakoutPairPaletteValue,
  isPriceExtremeBreakoutPairRoot,
  findPriceExtremePairRootInForest,
  patchPriceExtremePairInTree,
  setPriceExtremePairFilterLevel,
  inferPriceExtremeFilterLevel,
  type PriceExtremeFilterLevel,
} from '../utils/priceExtremeBreakoutPair';
import {
  buildInflection33Tree,
  isInflection33PaletteValue,
  isInflection33PairRoot,
  setInflection33PairFilterLevel,
  patchInflection33PairInTree,
  inferInflection33FilterLevel,
  type Inflection33FilterLevel,
} from '../utils/inflection33Strategy';
import {
  buildStableStrategyTree,
  isStableStrategyPaletteValue,
  isStableStrategyPairRoot,
  setStableStrategyPairFilterLevel,
  patchStableStrategyPairInTree,
  inferStableStrategyFilterLevel,
  type StableStrategyFilterLevel,
  type StableStrategyId,
} from '../utils/stableStrategyPairs';
import {
  buildStochOverboughtPairTree,
  findStochPairRootInForest,
  isStochOverboughtPairPaletteValue,
  isStochOverboughtPairRoot,
  patchStochPairInTree,
  setStochPairSecondary,
} from '../utils/stochOverboughtPair';
import StochPairNodeSettings from './strategyEditor/StochPairNodeSettings';
import PriceExtremePairNodeSettings from './strategyEditor/PriceExtremePairNodeSettings';
import Inflection33PairNodeSettings from './strategyEditor/Inflection33PairNodeSettings';
import StableStrategyPairNodeSettings from './strategyEditor/StableStrategyPairNodeSettings';
import {
  emptySignalFlowLayout,
  buildStrategyFlowLayoutStore,
  normalizeStrategyFlowLayout,
  readLegacyFlowLayoutFromLocalStorage,
  clearLegacyFlowLayoutFromLocalStorage,
  type SignalFlowLayoutSnapshot,
  type FlowLayoutChangePayload,
  type StrategyFlowLayoutStore,
} from '../utils/strategyEditorLayoutStorage';
import LogicExpressionPreview from './strategyEditor/LogicExpressionPreview';
import LogicExpressionNarrative from './strategyEditor/LogicExpressionNarrative';
import StrategyEditorCanvas, { type FlowLayoutSeed } from './strategyEditor/StrategyEditorCanvas';
import StrategyListEditor from './strategyEditor/StrategyListEditor';
import StartCombineOpControl from './strategyEditor/StartCombineOpControl';
import { layoutFlushRef } from './strategyEditor/strategyEditorCallbacks';
import {
  loadEditorMode,
  saveEditorMode,
  type StrategyEditorMode,
} from '../utils/strategyEditorModeStorage';
import {
  getStrategyTemplates,
  getBuiltinTemplateId,
  simpleTemplateToNode,
  type StrategyTemplateDef,
} from '../utils/strategyPresets';
import {
  addUserStrategyTemplate,
  deleteUserStrategyTemplate,
  updateUserStrategyTemplate,
  hideBuiltinTemplate,
  loadHiddenBuiltinTemplateIds,
  loadUserStrategyTemplates,
  type UserStrategyTemplate,
} from '../utils/strategyTemplateStorage';
import {
  buildStrategyExportPayload,
  buildStrategyListExportPayload,
  downloadStrategyJson,
  listImportItemToStrategyDto,
  parseStrategyImportFile,
  pickJsonFile,
  strategyDtoToListExportItem,
} from '../utils/strategyImportExport';
import PaletteChip from './strategyEditor/PaletteChip';
import PaletteDragOverlay from './strategyEditor/PaletteDragOverlay';
import { needsPointerPaletteDrag } from '../utils/paletteDragSession';
import SePanelCollapseHandle from './strategyEditor/SePanelCollapseHandle';
import {
  clampPanelSize,
  readStoredBool,
  readStoredSize,
  storeBool,
  storeSize,
  usePanelResize,
  useRightPanelResize,
} from './strategyEditor/usePanelResize';
import StrategyDescriptionModal from './strategyEditor/StrategyDescriptionModal';
import ReactDOM from 'react-dom';
import { MarketSearchPanel } from './MarketSearchPanel';
import { getKoreanName } from '../utils/marketNameCache';
import type { Timeframe } from '../types';
import {
  BACKTEST_STRATEGY_TIMEFRAME,
  buildQuickRunTimeframeSelectOptions,
  parseBacktestRunTimeframeChoice,
  resolveBacktestExecTimeframe,
  type BacktestRunTimeframeChoice,
} from '../utils/backtestRunTimeframe';
import '../styles/strategyEditor.css';
import '../styles/strategyEditorTheme.css';

const BACKTEST_FOCUS_KEY = 'backtest_focus_id';

const LEFT_PANEL_MIN = 220;
const LEFT_PANEL_MAX = 520;
const LEFT_PANEL_DEFAULT = 280;
const RIGHT_PANEL_MIN = 380;
const RIGHT_PANEL_MAX = 560;
const RIGHT_PANEL_DEFAULT = 380;
const TERMINAL_MIN = 88;
const TERMINAL_MAX = 420;
const TERMINAL_DEFAULT = 220;

function readRightPanelWidth(): number {
  return clampPanelSize(
    readStoredSize('se-right-width', RIGHT_PANEL_DEFAULT),
    RIGHT_PANEL_MIN,
    RIGHT_PANEL_MAX,
  );
}

interface Props {
  theme: Theme;
  onNavigateToBacktest?: () => void;
  /** 팝업(임베드) — 전략평가 등 */
  variant?: 'page' | 'modal';
  onClose?: () => void;
  initialStrategyId?: number | null;
  onStrategiesChanged?: () => void;
}

function toEditorState(
  root: LogicNode | null,
  layout: Pick<SignalFlowLayoutSnapshot, 'startMeta' | 'extraStartIds' | 'extraRoots' | 'startCombineOp'>,
): EditorConditionState {
  return {
    root,
    startMeta: layout.startMeta ?? defaultStartMeta(),
    extraStartIds: layout.extraStartIds ?? [],
    extraRoots: layout.extraRoots ?? {},
    startCombineOp: normalizeStartCombineOp(layout.startCombineOp),
  };
}

function normalizeTabLayoutState(snap: SignalFlowLayoutSnapshot) {
  return {
    positions: snap.positions ?? {},
    edgeHandles: snap.edgeHandles ?? {},
    startMeta: snap.startMeta ?? defaultStartMeta(),
    extraStartIds: snap.extraStartIds ?? [],
    extraRoots: snap.extraRoots ?? {},
    startCombineOp: normalizeStartCombineOp(snap.startCombineOp),
  };
}

function applyFlowLayoutFromStore(
  stored: StrategyFlowLayoutStore | null,
  setBuyLayout: React.Dispatch<React.SetStateAction<ReturnType<typeof normalizeTabLayoutState>>>,
  setSellLayout: React.Dispatch<React.SetStateAction<ReturnType<typeof normalizeTabLayoutState>>>,
  setBuyOrphans: React.Dispatch<React.SetStateAction<LogicNode[]>>,
  setSellOrphans: React.Dispatch<React.SetStateAction<LogicNode[]>>,
) {
  if (stored) {
    setBuyLayout(normalizeTabLayoutState(stored.buy));
    setSellLayout(normalizeTabLayoutState(stored.sell));
    setBuyOrphans(stored.buy.orphans ?? []);
    setSellOrphans(stored.sell.orphans ?? []);
  } else {
    const emptyBuy = emptySignalFlowLayout();
    const emptySell = emptySignalFlowLayout();
    setBuyLayout(normalizeTabLayoutState(emptyBuy));
    setSellLayout(normalizeTabLayoutState(emptySell));
    setBuyOrphans([]);
    setSellOrphans([]);
  }
}

type SaveDialogMode = 'update' | 'create' | 'saveAs';

function suggestSaveAsName(baseName: string, existing: StrategyDto[]): string {
  const trimmed = baseName.trim() || '새 전략';
  const names = new Set(existing.map(s => s.name.trim()));
  let candidate = `${trimmed} (복사)`;
  let n = 2;
  while (names.has(candidate)) {
    candidate = `${trimmed} (복사 ${n})`;
    n += 1;
  }
  return candidate;
}

function isDuplicateStrategyName(
  name: string,
  strategies: StrategyDto[],
  excludeId?: number | null,
): boolean {
  const trimmed = name.trim();
  return strategies.some(s => s.name.trim() === trimmed && s.id !== excludeId);
}

export default function StrategyEditorPage({
  theme,
  onNavigateToBacktest,
  variant = 'page',
  onClose,
  initialStrategyId = null,
  onStrategiesChanged,
}: Props) {
  const { getParams, getVisualConfig, settingsRevision, settingsLoaded } = useIndicatorSettings();
  const DEF = useMemo(
    () => buildStrategyEditorDefFromSettings(getParams, getVisualConfig),
    [getParams, getVisualConfig, settingsRevision],
  );
  const settingsSyncRef = useRef(-1);
  const initialStrategyAppliedRef = useRef(false);

  const notifyStrategiesChanged = useCallback(() => {
    onStrategiesChanged?.();
  }, [onStrategiesChanged]);

  const [strategies, setStrategies] = useState<StrategyDto[]>(() => loadStratsLocal());
  const [selectedId, setSelectedId] = useState<number | null>(null);
  const [buyCondition, setBuyCondition] = useState<LogicNode | null>(null);
  const [sellCondition, setSellCondition] = useState<LogicNode | null>(null);
  const [signalTab, setSignalTab] = useState<'buy' | 'sell'>('buy');
  const [rightTab, setRightTab] = useState<'indicators' | 'templates'>('indicators');
  const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite' | 'range'>('auxiliary');
  const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary'));
  const [compositePalette, setCompositePalette] = useState<PaletteItem[]>(() => loadPaletteItems('composite'));
  const [sidewaysPalette, setSidewaysPalette] = useState(() => loadSidewaysPaletteItems());
  const [paletteSearch, setPaletteSearch] = useState('');
  const [templateSearch, setTemplateSearch] = useState('');
  const [userTemplates, setUserTemplates] = useState<UserStrategyTemplate[]>(() => loadUserStrategyTemplates());
  const [hiddenBuiltinTemplateIds, setHiddenBuiltinTemplateIds] = useState<string[]>(
    () => loadHiddenBuiltinTemplateIds(),
  );
  const [templateSaveOpen, setTemplateSaveOpen] = useState(false);
  const [templateModalMode, setTemplateModalMode] = useState<'add' | 'edit'>('add');
  const [templateEditId, setTemplateEditId] = useState<string | null>(null);
  const [templateSaveUpdateContent, setTemplateSaveUpdateContent] = useState(false);
  const [templateSaveLabel, setTemplateSaveLabel] = useState('');
  const [templateSaveDesc, setTemplateSaveDesc] = useState('');
  const [templateSaveLabelError, setTemplateSaveLabelError] = useState(false);
  const [selectedTemplateKey, setSelectedTemplateKey] = useState<string | null>(null);
  const [selectedPaletteKey, setSelectedPaletteKey] = useState<string | null>(null);
  const [stratName, setStratName] = useState('');
  const [stratDesc, setStratDesc] = useState('');
  const [isSaving, setIsSaving] = useState(false);
  const [saveOpen, setSaveOpen] = useState(false);
  const [saveMode, setSaveMode] = useState<SaveDialogMode>('create');
  const [saveDraftName, setSaveDraftName] = useState('');
  const [saveDraftDesc, setSaveDraftDesc] = useState('');
  const [saveNameError, setSaveNameError] = useState(false);
  const [saveDuplicateError, setSaveDuplicateError] = useState(false);
  const [deleteOpen, setDeleteOpen] = useState(false);
  const [descOpen, setDescOpen] = useState(false);
  const [deleteId, setDeleteId] = useState<number | null>(null);
  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
  const [buyOrphans, setBuyOrphans] = useState<LogicNode[]>([]);
  const [sellOrphans, setSellOrphans] = useState<LogicNode[]>([]);
  const [snack, setSnack] = useState<{ msg: string; ok: boolean } | null>(null);
  const [saveToast, setSaveToast] = useState(false);

  // ── 빠른 백테스팅 실행 ────────────────────────────────────────────────────
  const [qbMarket,     setQbMarket]     = useState('KRW-BTC');
  const [qbTimeframe,  setQbTimeframe]  = useState<BacktestRunTimeframeChoice>(BACKTEST_STRATEGY_TIMEFRAME);
  const [qbLoading,    setQbLoading]    = useState(false);
  const [qbError,      setQbError]      = useState<string | null>(null);
  const [qbMarketOpen, setQbMarketOpen] = useState(false);
  const [qbMarketQ,    setQbMarketQ]    = useState('');
  const qbMarketBtnRef = useRef<HTMLButtonElement>(null);

  const [leftWidth, setLeftWidth] = useState(() => readStoredSize('se-left-width', LEFT_PANEL_DEFAULT));
  const [rightWidth, setRightWidth] = useState(() => readRightPanelWidth());
  const [rightOpen, setRightOpen] = useState(() => readStoredBool('se-right-open', true));
  const [terminalHeight, setTerminalHeight] = useState(() => readStoredSize('se-terminal-height', TERMINAL_DEFAULT));
  const leftWidthRef = useRef(leftWidth);
  const rightWidthRef = useRef(rightWidth);
  const terminalHeightRef = useRef(terminalHeight);
  const [buyLayout, setBuyLayout] = useState(() => normalizeTabLayoutState(emptySignalFlowLayout()));
  const [sellLayout, setSellLayout] = useState(() => normalizeTabLayoutState(emptySignalFlowLayout()));
  const buyLayoutRef = useRef(buyLayout);
  const sellLayoutRef = useRef(sellLayout);
  const buyConditionRef = useRef(buyCondition);
  const sellConditionRef = useRef(sellCondition);
  const buyOrphansRef = useRef(buyOrphans);
  const sellOrphansRef = useRef(sellOrphans);
  const selectedIdRef = useRef(selectedId);
  const stratNameRef = useRef(stratName);
  const stratDescRef = useRef(stratDesc);
  buyLayoutRef.current = buyLayout;
  sellLayoutRef.current = sellLayout;
  buyConditionRef.current = buyCondition;
  sellConditionRef.current = sellCondition;
  buyOrphansRef.current = buyOrphans;
  sellOrphansRef.current = sellOrphans;
  selectedIdRef.current = selectedId;
  stratNameRef.current = stratName;
  stratDescRef.current = stratDesc;
  const layoutRevisionRef = useRef(0);
  const strategyAutosaveTimerRef = useRef<number | null>(null);
  const layoutPersistReadyRef = useRef(false);
  const STRATEGY_AUTOSAVE_MS = 350;
  leftWidthRef.current = leftWidth;
  rightWidthRef.current = rightWidth;
  terminalHeightRef.current = terminalHeight;

  const [layoutSeedKey, setLayoutSeedKey] = useState('draft:buy:0');
  const [editorMode, setEditorMode] = useState<StrategyEditorMode>(() => loadEditorMode());

  const onLeftSplitter = usePanelResize(
    'vertical',
    setLeftWidth,
    () => leftWidthRef.current,
    LEFT_PANEL_MIN,
    LEFT_PANEL_MAX,
    v => storeSize('se-left-width', v),
  );

  const onRightSplitter = useRightPanelResize(
    setRightWidth,
    () => rightWidthRef.current,
    RIGHT_PANEL_MIN,
    RIGHT_PANEL_MAX,
    v => storeSize('se-right-width', v),
  );

  const toggleRightPanel = useCallback(() => {
    setRightOpen(prev => {
      const next = !prev;
      storeBool('se-right-open', next);
      return next;
    });
  }, []);

  const onTerminalSplitter = usePanelResize(
    'horizontal',
    setTerminalHeight,
    () => terminalHeightRef.current,
    TERMINAL_MIN,
    TERMINAL_MAX,
    v => storeSize('se-terminal-height', v),
  );

  const bodyStyle = useMemo(() => ({
    '--se-left-width': `${leftWidth}px`,
    '--se-right-width': `${rightWidth}px`,
    '--se-terminal-height': `${terminalHeight}px`,
  }) as React.CSSProperties, [leftWidth, rightWidth, terminalHeight]);

  const showSnack = (msg: string, ok = true) => {
    setSnack({ msg, ok });
    setTimeout(() => setSnack(null), 3000);
  };

  const currentRoot = signalTab === 'buy' ? buyCondition : sellCondition;
  const setCurrentRoot = signalTab === 'buy' ? setBuyCondition : setSellCondition;
  const currentOrphans = signalTab === 'buy' ? buyOrphans : sellOrphans;
  const setCurrentOrphans = signalTab === 'buy' ? setBuyOrphans : setSellOrphans;

  const currentLayout = signalTab === 'buy' ? buyLayout : sellLayout;
  const setCurrentLayout = signalTab === 'buy' ? setBuyLayout : setSellLayout;

  const buyEditorState = useMemo(
    () => toEditorState(buyCondition, buyLayout),
    [buyCondition, buyLayout],
  );
  const sellEditorState = useMemo(
    () => toEditorState(sellCondition, sellLayout),
    [sellCondition, sellLayout],
  );
  const currentEditorState = signalTab === 'buy' ? buyEditorState : sellEditorState;
  const currentEditorStateRef = useRef(currentEditorState);
  currentEditorStateRef.current = currentEditorState;

  const selectedLogicNode = useMemo(() => {
    if (!selectedNodeId) return null;
    return findLogicNode(
      currentRoot,
      currentOrphans,
      selectedNodeId,
      currentLayout.extraRoots ?? {},
    );
  }, [selectedNodeId, currentRoot, currentOrphans, currentLayout.extraRoots]);

  const stochPairForest = useMemo(
    () => [
      currentRoot,
      ...currentOrphans,
      ...Object.values(currentLayout.extraRoots ?? {}),
    ],
    [currentRoot, currentOrphans, currentLayout.extraRoots],
  );

  const orphanTotal = buyOrphans.length + sellOrphans.length;

  const layoutStrategyKey = selectedId != null ? String(selectedId) : 'draft';

  const buildCurrentFlowLayout = useCallback(
    (): StrategyFlowLayoutStore => buildStrategyFlowLayoutStore(
      buyLayoutRef.current,
      sellLayoutRef.current,
      buyOrphansRef.current,
      sellOrphansRef.current,
    ),
    [],
  );

  const persistStrategyToDb = useCallback(async () => {
    if (selectedId == null || !stratName.trim()) return;
    const buy = toEditorState(buyConditionRef.current, buyLayoutRef.current);
    const sell = toEditorState(sellConditionRef.current, sellLayoutRef.current);
    const aligned = alignBuySellStartCandleTypesForSave(buy, sell);
    const encodedBuy = encodeConditionForSave(aligned.buy);
    const encodedSell = encodeConditionForSave(aligned.sell);
    if (!encodedBuy && !encodedSell) return;
    const flowLayout = buildCurrentFlowLayout();
    const saved = await persistStrategyEditorState(
      selectedId,
      stratName,
      stratDesc,
      aligned.buy,
      aligned.sell,
      flowLayout,
    );
    setStrategies(prev => prev.map(s => s.id === selectedId
      ? {
        ...s,
        buyCondition: encodedBuy,
        sellCondition: encodedSell,
        flowLayout,
        updatedAt: saved?.updatedAt ?? s.updatedAt,
      }
      : s));
  }, [selectedId, stratName, stratDesc, buildCurrentFlowLayout]);

  /** 전략 전환·새 전략 전 — ref 스냅샷을 지정 id 로 저장 (selectedId 변경 후 빈 DSL 덮어쓰기 방지) */
  const persistStrategySnapshot = useCallback(async (
    strategyId: number,
    name: string,
    description: string,
  ) => {
    const buy = toEditorState(buyConditionRef.current, buyLayoutRef.current);
    const sell = toEditorState(sellConditionRef.current, sellLayoutRef.current);
    const aligned = alignBuySellStartCandleTypesForSave(buy, sell);
    const encodedBuy = encodeConditionForSave(aligned.buy);
    const encodedSell = encodeConditionForSave(aligned.sell);
    if (!encodedBuy && !encodedSell) return;
    const flowLayout = buildCurrentFlowLayout();
    const saved = await persistStrategyEditorState(
      strategyId,
      name,
      description,
      aligned.buy,
      aligned.sell,
      flowLayout,
    );
    setStrategies(prev => prev.map(s => s.id === strategyId
      ? {
        ...s,
        buyCondition: encodedBuy,
        sellCondition: encodedSell,
        flowLayout,
        updatedAt: saved?.updatedAt ?? s.updatedAt,
      }
      : s));
  }, [buildCurrentFlowLayout]);

  const flushStrategyPersist = useCallback(() => {
    if (strategyAutosaveTimerRef.current != null) {
      window.clearTimeout(strategyAutosaveTimerRef.current);
      strategyAutosaveTimerRef.current = null;
    }
    if (selectedId == null || !stratName.trim()) return;
    void persistStrategyToDb().catch(err => {
      console.warn('[StrategyEditor] 전략 DB 저장 실패:', err);
    });
  }, [selectedId, stratName, persistStrategyToDb]);

  const scheduleStrategyPersist = useCallback(() => {
    if (selectedId == null || !stratName.trim()) return;
    if (strategyAutosaveTimerRef.current != null) {
      window.clearTimeout(strategyAutosaveTimerRef.current);
    }
    strategyAutosaveTimerRef.current = window.setTimeout(() => {
      strategyAutosaveTimerRef.current = null;
      void persistStrategyToDb().catch(err => {
        console.warn('[StrategyEditor] 전략 자동저장 실패:', err);
      });
    }, STRATEGY_AUTOSAVE_MS);
  }, [selectedId, stratName, persistStrategyToDb]);

  const bumpLayoutSeed = useCallback((strategyKey: string, tab: 'buy' | 'sell') => {
    layoutRevisionRef.current += 1;
    setLayoutSeedKey(`${strategyKey}:${tab}:${layoutRevisionRef.current}`);
  }, []);

  /** 보조지표 설정 변경 시 오버라이드되지 않은 조건 노드·캔버스 기준값 동기화 */
  useEffect(() => {
    if (!settingsLoaded) return;
    if (settingsRevision <= settingsSyncRef.current) return;
    settingsSyncRef.current = settingsRevision;

    const syncExtraRoots = (
      roots: Record<string, LogicNode | null>,
      signalType: 'buy' | 'sell',
    ) => {
      const next: Record<string, LogicNode | null> = {};
      for (const [key, node] of Object.entries(roots)) {
        next[key] = node ? syncLogicNodeWithGlobalDef(node, DEF, signalType) : null;
      }
      return next;
    };

    setBuyCondition(prev => (prev ? syncLogicNodeWithGlobalDef(prev, DEF, 'buy') : prev));
    setSellCondition(prev => (prev ? syncLogicNodeWithGlobalDef(prev, DEF, 'sell') : prev));
    setBuyOrphans(prev => prev.map(n => syncLogicNodeWithGlobalDef(n, DEF, 'buy')));
    setSellOrphans(prev => prev.map(n => syncLogicNodeWithGlobalDef(n, DEF, 'sell')));
    setBuyLayout(prev => ({
      ...prev,
      extraRoots: syncExtraRoots(prev.extraRoots ?? {}, 'buy'),
    }));
    setSellLayout(prev => ({
      ...prev,
      extraRoots: syncExtraRoots(prev.extraRoots ?? {}, 'sell'),
    }));
    bumpLayoutSeed(layoutStrategyKey, signalTab);
    if (selectedId != null && stratName.trim()) scheduleStrategyPersist();
  }, [settingsLoaded, settingsRevision, DEF, layoutStrategyKey, signalTab, bumpLayoutSeed, selectedId, stratName, scheduleStrategyPersist]);

  const resetFlowLayout = useCallback((strategyKey: string, tab: 'buy' | 'sell' = 'buy') => {
    applyFlowLayoutFromStore(null, setBuyLayout, setSellLayout, setBuyOrphans, setSellOrphans);
    bumpLayoutSeed(strategyKey, tab);
  }, [bumpLayoutSeed]);

  const handleLayoutChange = useCallback((snapshot: FlowLayoutChangePayload) => {
    const patch = {
      positions: snapshot.positions,
      edgeHandles: snapshot.edgeHandles,
    };
    if (snapshot.tab === 'buy') setBuyLayout(prev => ({ ...prev, ...patch }));
    else setSellLayout(prev => ({ ...prev, ...patch }));
    scheduleStrategyPersist();
  }, [scheduleStrategyPersist]);

  const handleRootChange = useCallback((root: LogicNode | null) => {
    setCurrentRoot(root);
    scheduleStrategyPersist();
  }, [setCurrentRoot, scheduleStrategyPersist]);

  const handleOrphansChange = useCallback((orphans: LogicNode[]) => {
    setCurrentOrphans(orphans);
    scheduleStrategyPersist();
  }, [setCurrentOrphans, scheduleStrategyPersist]);

  const handleStartMetaChange = useCallback((meta: Record<string, import('../utils/strategyStartNodes').StartNodeMeta>) => {
    setCurrentLayout(prev => ({ ...prev, startMeta: meta }));
    scheduleStrategyPersist();
  }, [setCurrentLayout, scheduleStrategyPersist]);

  const handleExtraStartIdsChange = useCallback((ids: string[]) => {
    setCurrentLayout(prev => ({ ...prev, extraStartIds: ids }));
    scheduleStrategyPersist();
    bumpLayoutSeed(layoutStrategyKey, signalTab);
  }, [setCurrentLayout, layoutStrategyKey, scheduleStrategyPersist, bumpLayoutSeed, signalTab]);

  const handleExtraRootsChange = useCallback((roots: Record<string, LogicNode | null>) => {
    setCurrentLayout(prev => ({ ...prev, extraRoots: roots }));
    scheduleStrategyPersist();
  }, [setCurrentLayout, scheduleStrategyPersist]);

  const applyStochPairSecondary = useCallback((orNodeId: string, secondary: string) => {
    if (currentOrphans.some(o => o.id === orNodeId)) {
      handleOrphansChange(currentOrphans.map(o => (
        o.id === orNodeId ? setStochPairSecondary(o, secondary, DEF) : o
      )));
      return;
    }
    if (currentRoot) {
      const patched = patchStochPairInTree(currentRoot, orNodeId, secondary, DEF);
      if (patched && patched !== currentRoot) {
        handleRootChange(patched);
        return;
      }
    }
    for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
      if (!branch) continue;
      const patched = patchStochPairInTree(branch, orNodeId, secondary, DEF);
      if (patched && patched !== branch) {
        handleExtraRootsChange({
          ...(currentLayout.extraRoots ?? {}),
          [startId]: patched,
        });
        return;
      }
    }
  }, [
    currentRoot, currentOrphans, currentLayout.extraRoots, DEF,
    handleOrphansChange, handleRootChange, handleExtraRootsChange,
  ]);

  const applyPriceExtremeFilterLevel = useCallback((andNodeId: string, filterLevel: PriceExtremeFilterLevel) => {
    if (currentOrphans.some(o => o.id === andNodeId)) {
      handleOrphansChange(currentOrphans.map(o => (
        o.id === andNodeId ? setPriceExtremePairFilterLevel(o, filterLevel, DEF) : o
      )));
      return;
    }
    if (currentRoot) {
      const patched = patchPriceExtremePairInTree(currentRoot, andNodeId, filterLevel, DEF);
      if (patched && patched !== currentRoot) {
        handleRootChange(patched);
        return;
      }
    }
    for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
      if (!branch) continue;
      const patched = patchPriceExtremePairInTree(branch, andNodeId, filterLevel, DEF);
      if (patched && patched !== branch) {
        handleExtraRootsChange({
          ...(currentLayout.extraRoots ?? {}),
          [startId]: patched,
        });
        return;
      }
    }
  }, [
    currentRoot, currentOrphans, currentLayout.extraRoots, DEF,
    handleOrphansChange, handleRootChange, handleExtraRootsChange,
  ]);

  const applyInflection33FilterLevel = useCallback((pairNodeId: string, filterLevel: Inflection33FilterLevel) => {
    if (currentOrphans.some(o => o.id === pairNodeId)) {
      handleOrphansChange(currentOrphans.map(o => (
        o.id === pairNodeId ? setInflection33PairFilterLevel(o, filterLevel, DEF) : o
      )));
      return;
    }
    if (currentRoot) {
      const patched = patchInflection33PairInTree(currentRoot, pairNodeId, filterLevel, DEF);
      if (patched && patched !== currentRoot) {
        handleRootChange(patched);
        return;
      }
    }
    for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
      if (!branch) continue;
      const patched = patchInflection33PairInTree(branch, pairNodeId, filterLevel, DEF);
      if (patched && patched !== branch) {
        handleExtraRootsChange({
          ...(currentLayout.extraRoots ?? {}),
          [startId]: patched,
        });
        return;
      }
    }
  }, [
    currentRoot, currentOrphans, currentLayout.extraRoots, DEF,
    handleOrphansChange, handleRootChange, handleExtraRootsChange,
  ]);

  const applyStableStrategyFilterLevel = useCallback((pairNodeId: string, filterLevel: StableStrategyFilterLevel) => {
    if (currentOrphans.some(o => o.id === pairNodeId)) {
      handleOrphansChange(currentOrphans.map(o => (
        o.id === pairNodeId ? setStableStrategyPairFilterLevel(o, filterLevel, DEF) : o
      )));
      return;
    }
    if (currentRoot) {
      const patched = patchStableStrategyPairInTree(currentRoot, pairNodeId, filterLevel, DEF);
      if (patched && patched !== currentRoot) {
        handleRootChange(patched);
        return;
      }
    }
    for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
      if (!branch) continue;
      const patched = patchStableStrategyPairInTree(branch, pairNodeId, filterLevel, DEF);
      if (patched && patched !== branch) {
        handleExtraRootsChange({
          ...(currentLayout.extraRoots ?? {}),
          [startId]: patched,
        });
        return;
      }
    }
  }, [
    currentRoot, currentOrphans, currentLayout.extraRoots, DEF,
    handleOrphansChange, handleRootChange, handleExtraRootsChange,
  ]);

  const stochPairEditForSelected = useMemo(() => {
    if (!selectedNodeId || selectedLogicNode?.type !== 'CONDITION' || !selectedLogicNode.condition) {
      return undefined;
    }
    const pairRoot = findStochPairRootInForest(stochPairForest, selectedNodeId);
    if (!pairRoot?.stochPair) return undefined;
    return {
      secondaryIndicator: pairRoot.stochPair.secondaryIndicatorType,
      stochLocked: selectedLogicNode.condition.indicatorType === 'STOCHASTIC',
      onSecondaryChange: (next: string) => applyStochPairSecondary(pairRoot.id, next),
    };
  }, [selectedNodeId, selectedLogicNode, stochPairForest, applyStochPairSecondary]);

  const handleEditorStateChange = useCallback((
    next: EditorConditionState | ((prev: EditorConditionState) => EditorConditionState),
  ) => {
    const resolved = typeof next === 'function'
      ? next(currentEditorStateRef.current)
      : next;
    currentEditorStateRef.current = resolved;
    setCurrentRoot(resolved.root);
    setCurrentLayout(prev => ({
      ...prev,
      startMeta: resolved.startMeta,
      extraStartIds: resolved.extraStartIds,
      extraRoots: resolved.extraRoots,
      startCombineOp: normalizeStartCombineOp(resolved.startCombineOp),
    }));
    scheduleStrategyPersist();
  }, [setCurrentRoot, setCurrentLayout, scheduleStrategyPersist]);

  const handleStartCandleTypesChange = useCallback((startId: string, candleTypes: string[]) => {
    handleEditorStateChange(prev => updateStartCandleTypes(prev, startId, candleTypes));
  }, [handleEditorStateChange]);

  const handleStartCombineOpChange = useCallback((op: StartCombineOp) => {
    handleEditorStateChange(prev => updateStartCombineOp(prev, op));
  }, [handleEditorStateChange]);

  const switchSignalTab = useCallback((tab: 'buy' | 'sell') => {
    if (editorMode === 'graph') layoutFlushRef.current?.();
    flushStrategyPersist();
    layoutRevisionRef.current += 1;
    setLayoutSeedKey(`${layoutStrategyKey}:${tab}:${layoutRevisionRef.current}`);
    setSignalTab(tab);
    setSelectedNodeId(null);
  }, [layoutStrategyKey, flushStrategyPersist, editorMode]);

  const handleEditorModeChange = useCallback((mode: StrategyEditorMode) => {
    if (mode === editorMode) return;
    if (editorMode === 'graph') {
      layoutFlushRef.current?.();
      flushStrategyPersist();
    }
    if (mode === 'list' && (buyOrphans.length > 0 || sellOrphans.length > 0)) {
      showSnack('목록 방식에서는 미연결(고아) 노드가 표시되지 않습니다', false);
    }
    setEditorMode(mode);
    saveEditorMode(mode);
    setSelectedNodeId(null);
  }, [editorMode, flushStrategyPersist, buyOrphans.length, sellOrphans.length]);

  const layoutSeed = useMemo((): FlowLayoutSeed => {
    const source = signalTab === 'buy' ? buyLayout : sellLayout;
    return {
      seedKey: layoutSeedKey,
      positions: source.positions,
      edgeHandles: source.edgeHandles,
    };
  }, [signalTab, layoutSeedKey, buyLayout, sellLayout]);

  const persistStrategySnapshotRef = useRef(persistStrategySnapshot);
  persistStrategySnapshotRef.current = persistStrategySnapshot;
  useEffect(() => () => {
    if (strategyAutosaveTimerRef.current != null) {
      window.clearTimeout(strategyAutosaveTimerRef.current);
      strategyAutosaveTimerRef.current = null;
    }
    const id = selectedIdRef.current;
    const name = stratNameRef.current;
    const desc = stratDescRef.current;
    if (id != null && name.trim()) {
      void persistStrategySnapshotRef.current(id, name, desc).catch(() => { /* unmount flush */ });
    }
  }, []);

  useEffect(() => {
    if (!layoutPersistReadyRef.current) {
      layoutPersistReadyRef.current = true;
      return;
    }
    scheduleStrategyPersist();
  }, [buyOrphans, sellOrphans, scheduleStrategyPersist]);

  useEffect(() => { saveStratsLocal(strategies); }, [strategies]);

  useEffect(() => {
    if (!hasRegisteredUser()) return;
    loadStrategies().then(async list => {
      if (list?.length) {
        const mapped = list.map(s => ({
          id: s.id ?? Date.now(),
          name: s.name,
          description: s.description,
          buyCondition: s.buyCondition as LogicNode | null ?? null,
          sellCondition: s.sellCondition as LogicNode | null ?? null,
          flowLayout: normalizeStrategyFlowLayout(s.flowLayout) ?? undefined,
          enabled: s.enabled ?? true,
          createdAt: s.createdAt,
          updatedAt: s.updatedAt,
        }));
        setStrategies(mapped);
        setSelectedId(prev =>
          prev != null && mapped.some(x => x.id === prev) ? prev : null,
        );
      } else {
        const local = loadStratsLocal();
        if (!local.length) return;
        const migrated: StrategyDto[] = [];
        for (const s of local) {
          try {
            const saved = await saveStrategy({
              name: s.name,
              description: s.description,
              buyCondition: s.buyCondition,
              sellCondition: s.sellCondition,
              enabled: s.enabled ?? true,
            });
            if (saved?.id) {
              migrated.push({
                id: saved.id,
                name: saved.name,
                description: saved.description,
                buyCondition: saved.buyCondition as LogicNode | null ?? null,
                sellCondition: saved.sellCondition as LogicNode | null ?? null,
                enabled: saved.enabled ?? true,
                createdAt: saved.createdAt,
                updatedAt: saved.updatedAt,
              });
            }
          } catch { /* skip */ }
        }
        if (migrated.length) setStrategies(migrated);
      }
    }).catch(() => {});
  }, []);

  const flushBeforeStrategySwitch = useCallback(() => {
    if (editorMode === 'graph') layoutFlushRef.current?.();
    if (strategyAutosaveTimerRef.current != null) {
      window.clearTimeout(strategyAutosaveTimerRef.current);
      strategyAutosaveTimerRef.current = null;
    }
    const prevId = selectedIdRef.current;
    const prevName = stratNameRef.current;
    const prevDesc = stratDescRef.current;
    if (prevId != null && prevName.trim()) {
      void persistStrategySnapshot(prevId, prevName, prevDesc).catch(() => { /* switch flush */ });
    }
  }, [editorMode, persistStrategySnapshot]);

  const handleSelectStrategy = (s: StrategyDto) => {
    if (selectedId !== s.id) {
      flushBeforeStrategySwitch();
    }
    const buyDecoded = decodeConditionForEditor(s.buyCondition ?? null);
    const sellDecoded = decodeConditionForEditor(s.sellCondition ?? null);
    let stored = normalizeStrategyFlowLayout(s.flowLayout);
    if (!stored) {
      stored = readLegacyFlowLayoutFromLocalStorage(String(s.id));
    }

    const buyMigrated = migrateLogicRootForEditor(buyDecoded.root, stored?.buy.orphans ?? [], DEF, 'buy');
    const sellMigrated = migrateLogicRootForEditor(sellDecoded.root, stored?.sell.orphans ?? [], DEF, 'sell');

    const buySynced = syncLogicRootWithGlobalDef(
      buyMigrated.root,
      buyMigrated.orphans,
      DEF,
      'buy',
    );
    const sellSynced = syncLogicRootWithGlobalDef(
      sellMigrated.root,
      sellMigrated.orphans,
      DEF,
      'sell',
    );

    setSelectedId(s.id);
    setStratName(s.name);
    setStratDesc(s.description ?? '');
    setBuyCondition(buySynced.root);
    setSellCondition(sellSynced.root);
    setBuyOrphans(buySynced.orphans);
    setSellOrphans(sellSynced.orphans);
    setBuyLayout({
      positions: stored?.buy.positions ?? {},
      edgeHandles: stored?.buy.edgeHandles ?? {},
      startMeta: mergeStartMetaForLoad(buyDecoded.startMeta, stored?.buy.startMeta),
      extraStartIds: stored?.buy.extraStartIds?.length ? stored.buy.extraStartIds : buyDecoded.extraStartIds,
      extraRoots: stored?.buy.extraRoots && Object.keys(stored.buy.extraRoots).length
        ? stored.buy.extraRoots
        : buyDecoded.extraRoots,
      startCombineOp: normalizeStartCombineOp(stored?.buy.startCombineOp ?? buyDecoded.startCombineOp),
    });
    setSellLayout({
      positions: stored?.sell.positions ?? {},
      edgeHandles: stored?.sell.edgeHandles ?? {},
      startMeta: mergeStartMetaForLoad(sellDecoded.startMeta, stored?.sell.startMeta),
      extraStartIds: stored?.sell.extraStartIds?.length ? stored.sell.extraStartIds : sellDecoded.extraStartIds,
      extraRoots: stored?.sell.extraRoots && Object.keys(stored.sell.extraRoots).length
        ? stored.sell.extraRoots
        : sellDecoded.extraRoots,
      startCombineOp: normalizeStartCombineOp(stored?.sell.startCombineOp ?? sellDecoded.startCombineOp),
    });
    setSelectedNodeId(null);
    bumpLayoutSeed(String(s.id), signalTab);

    if (!s.flowLayout && stored) {
      void (async () => {
        try {
          const aligned = alignBuySellStartCandleTypesForSave(
            toEditorState(buySynced.root, {
              startMeta: mergeStartMetaForLoad(buyDecoded.startMeta, stored.buy.startMeta),
              extraStartIds: stored.buy.extraStartIds ?? buyDecoded.extraStartIds,
              extraRoots: stored.buy.extraRoots ?? buyDecoded.extraRoots,
              startCombineOp: normalizeStartCombineOp(stored.buy.startCombineOp ?? buyDecoded.startCombineOp),
            }),
            toEditorState(sellSynced.root, {
              startMeta: mergeStartMetaForLoad(sellDecoded.startMeta, stored.sell.startMeta),
              extraStartIds: stored.sell.extraStartIds ?? sellDecoded.extraStartIds,
              extraRoots: stored.sell.extraRoots ?? sellDecoded.extraRoots,
              startCombineOp: normalizeStartCombineOp(stored.sell.startCombineOp ?? sellDecoded.startCombineOp),
            }),
          );
          await saveStrategy({
            id: s.id,
            name: s.name,
            description: s.description ?? '',
            buyCondition: encodeConditionForSave(aligned.buy),
            sellCondition: encodeConditionForSave(aligned.sell),
            flowLayout: stored,
            enabled: s.enabled ?? true,
          });
          clearLegacyFlowLayoutFromLocalStorage(String(s.id));
          setStrategies(prev => prev.map(x => x.id === s.id ? { ...x, flowLayout: stored! } : x));
        } catch {
          /* ignore */
        }
      })();
    }

    // flow layout START 분봉이 DB DSL보다 최신이면 서버에 반영 (실시간 평가는 DB 기준)
    void (async () => {
      if (!hasRegisteredUser() || s.id == null) return;
      const exists = await loadStrategy(s.id);
      if (!exists) return;
      const synced = await syncStrategyTimeframesFromLayoutIfNeeded(s.id, { ...s, flowLayout: stored ?? s.flowLayout });
      if (!synced) return;
      try {
        const fresh = await loadStrategy(s.id);
        if (!fresh) return;
        const buyD = decodeConditionForEditor((fresh.buyCondition ?? null) as LogicNode | null);
        const sellD = decodeConditionForEditor((fresh.sellCondition ?? null) as LogicNode | null);
        const buyM = migrateLogicRootForEditor(buyD.root, buySynced.orphans, DEF, 'buy');
        const sellM = migrateLogicRootForEditor(sellD.root, sellSynced.orphans, DEF, 'sell');
        setBuyCondition(syncLogicRootWithGlobalDef(buyM.root, buyM.orphans, DEF, 'buy').root);
        setSellCondition(syncLogicRootWithGlobalDef(sellM.root, sellM.orphans, DEF, 'sell').root);
      } catch {
        /* ignore */
      }
    })();
  };

  useEffect(() => {
    if (variant !== 'modal' || initialStrategyId == null || initialStrategyAppliedRef.current) return;
    if (strategies.length === 0) return;
    const found = strategies.find(s => s.id === initialStrategyId);
    if (!found) return;
    initialStrategyAppliedRef.current = true;
    handleSelectStrategy(found);
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [variant, initialStrategyId, strategies]);

  const handleNew = () => {
    flushBeforeStrategySwitch();
    setSelectedId(null);
    setStratName('');
    setStratDesc('');
    setBuyCondition(null);
    setSellCondition(null);
    setBuyOrphans([]);
    setSellOrphans([]);
    setSelectedNodeId(null);
    resetFlowLayout('draft', 'buy');
    setSignalTab('buy');
  };

  const openSaveDialog = useCallback((mode: SaveDialogMode) => {
    setSaveMode(mode);
    setSaveDraftName(mode === 'saveAs' ? suggestSaveAsName(stratName, strategies) : stratName);
    setSaveDraftDesc(stratDesc);
    setSaveNameError(false);
    setSaveDuplicateError(false);
    setSaveOpen(true);
  }, [stratName, stratDesc, strategies]);

  const handleSave = async () => {
    const name = saveDraftName.trim();
    if (!name) {
      setSaveNameError(true);
      showSnack('전략 이름을 입력하세요', false);
      return;
    }
    const asNew = saveMode === 'saveAs' || saveMode === 'create';
    const excludeId = asNew ? null : selectedId;
    if (isDuplicateStrategyName(name, strategies, excludeId)) {
      setSaveDuplicateError(true);
      showSnack('같은 이름의 전략이 이미 있습니다', false);
      return;
    }
    setSaveNameError(false);
    setSaveDuplicateError(false);
    if (editorMode === 'graph') layoutFlushRef.current?.();
    if (strategyAutosaveTimerRef.current != null) {
      window.clearTimeout(strategyAutosaveTimerRef.current);
      strategyAutosaveTimerRef.current = null;
    }
    const aligned = alignBuySellStartCandleTypesForSave(buyEditorState, sellEditorState);
    const encodedBuy = encodeConditionForSave(aligned.buy);
    const encodedSell = encodeConditionForSave(aligned.sell);
    if (!encodedBuy && !encodedSell) { showSnack('조건을 최소 1개 추가하세요', false); return; }
    setIsSaving(true);
    try {
      const flowLayout = buildCurrentFlowLayout();
      const payload: ApiStrategyDto = {
        id: asNew ? undefined : (selectedId ?? undefined),
        name,
        description: saveDraftDesc,
        buyCondition: encodedBuy,
        sellCondition: encodedSell,
        flowLayout,
        enabled: true,
      };
      const saved = await saveStrategy(payload);
      const now = new Date().toISOString();
      const dbId = saved?.id ?? (asNew ? Date.now() : selectedId ?? Date.now());
      const entry: StrategyDto = {
        id: dbId,
        name,
        description: saveDraftDesc,
        buyCondition: encodedBuy,
        sellCondition: encodedSell,
        flowLayout,
        enabled: true,
        createdAt: saved?.createdAt ?? now,
        updatedAt: saved?.updatedAt ?? now,
      };
      if (asNew) {
        setStrategies(prev => [...prev, entry]);
        setSelectedId(dbId);
      } else if (selectedId) {
        setStrategies(prev => prev.map(s => (s.id === selectedId ? { ...s, ...entry, id: dbId } : s)));
      } else {
        setStrategies(prev => [...prev, entry]);
        setSelectedId(dbId);
      }
      setStratName(name);
      setStratDesc(saveDraftDesc);
      bumpLayoutSeed(String(dbId), signalTab);
      setSaveOpen(false);
      setSaveToast(true);
      setTimeout(() => setSaveToast(false), 2500);
      if (saveMode === 'saveAs') {
        showSnack('새 전략으로 저장되었습니다');
      } else if (saveMode === 'update') {
        showSnack('전략이 수정되었습니다');
      } else {
        showSnack('전략이 저장되었습니다');
      }
      notifyStrategiesChanged();
    } catch (e) {
      showSnack(e instanceof Error ? e.message : '저장 실패', false);
    } finally {
      setIsSaving(false);
    }
  };

  const handleDeleteConfirm = async () => {
    if (!deleteId) return;
    try { await deleteStrategy(deleteId); } catch { /* local */ }
    clearLegacyFlowLayoutFromLocalStorage(String(deleteId));
    setStrategies(prev => prev.filter(s => s.id !== deleteId));
    if (selectedId === deleteId) handleNew();
    setDeleteOpen(false);
    setDeleteId(null);
    showSnack('전략이 삭제되었습니다');
    notifyStrategiesChanged();
  };

  const applyImportedFlowLayout = useCallback((layout: StrategyFlowLayoutStore | undefined, tab: 'buy' | 'sell' = signalTab) => {
    if (layout) {
      setBuyLayout({
        positions: layout.buy.positions ?? {},
        edgeHandles: layout.buy.edgeHandles ?? {},
        startMeta: layout.buy.startMeta ?? defaultStartMeta(),
        extraStartIds: layout.buy.extraStartIds ?? [],
        extraRoots: layout.buy.extraRoots ?? {},
        startCombineOp: normalizeStartCombineOp(layout.buy.startCombineOp),
      });
      setSellLayout({
        positions: layout.sell.positions ?? {},
        edgeHandles: layout.sell.edgeHandles ?? {},
        startMeta: layout.sell.startMeta ?? defaultStartMeta(),
        extraStartIds: layout.sell.extraStartIds ?? [],
        extraRoots: layout.sell.extraRoots ?? {},
        startCombineOp: normalizeStartCombineOp(layout.sell.startCombineOp),
      });
      setBuyOrphans(layout.buy.orphans ?? []);
      setSellOrphans(layout.sell.orphans ?? []);
    } else {
      resetFlowLayout('draft', tab);
    }
    bumpLayoutSeed('draft', tab);
  }, [resetFlowLayout, bumpLayoutSeed, signalTab]);

  const handleExport = useCallback(() => {
    const aligned = alignBuySellStartCandleTypesForSave(buyEditorState, sellEditorState);
    const encodedBuy = encodeConditionForSave(aligned.buy);
    const encodedSell = encodeConditionForSave(aligned.sell);
    if (!encodedBuy && !encodedSell) {
      showSnack('내보낼 조건이 없습니다', false);
      return;
    }
    if (editorMode === 'graph') layoutFlushRef.current?.();
    const payload = buildStrategyExportPayload({
      name: stratName,
      description: stratDesc,
      buyCondition: encodedBuy,
      sellCondition: encodedSell,
      flowLayout: {
        buy: { ...buyLayoutRef.current, orphans: buyOrphans },
        sell: { ...sellLayoutRef.current, orphans: sellOrphans },
      },
      editorMode,
    });
    const safeName = (stratName || '전략').replace(/[^\w\uAC00-\uD7A3-]+/g, '_');
    downloadStrategyJson(`${safeName}_${new Date().toISOString().slice(0, 10)}`, payload);
    showSnack('JSON으로 내보냈습니다');
  }, [buyEditorState, sellEditorState, stratName, stratDesc, buyOrphans, sellOrphans, editorMode]);

  const handleImport = useCallback(async () => {
    const text = await pickJsonFile();
    if (!text) return;
    try {
      const result = parseStrategyImportFile(text);
      if (result.kind === 'list') {
        showSnack('단일 전략 파일이 아닙니다. 전체 가져오기(⬆)를 사용하세요', false);
        return;
      }
      if (editorMode === 'graph') layoutFlushRef.current?.();
      const { data } = result;
      const buyDecoded = decodeConditionForEditor(data.buyCondition ?? null);
      const sellDecoded = decodeConditionForEditor(data.sellCondition ?? null);
      setBuyCondition(buyDecoded.root);
      setSellCondition(sellDecoded.root);
      setStratName(data.name ?? '가져온 전략');
      setStratDesc(data.description ?? '');
      setSelectedId(null);
      setSelectedNodeId(null);
      applyImportedFlowLayout(data.flowLayout);
      setBuyLayout(prev => ({
        ...prev,
        startCombineOp: normalizeStartCombineOp(data.flowLayout?.buy?.startCombineOp ?? buyDecoded.startCombineOp),
      }));
      setSellLayout(prev => ({
        ...prev,
        startCombineOp: normalizeStartCombineOp(data.flowLayout?.sell?.startCombineOp ?? sellDecoded.startCombineOp),
      }));
      if (data.editorMode === 'list' || data.editorMode === 'graph') {
        setEditorMode(data.editorMode);
        saveEditorMode(data.editorMode);
      }
      showSnack('전략을 가져왔습니다');
    } catch (e) {
      showSnack(e instanceof Error ? e.message : '파일 읽기 실패', false);
    }
  }, [editorMode, applyImportedFlowLayout]);

  const handleExportAll = useCallback(() => {
    if (strategies.length === 0) {
      showSnack('내보낼 전략이 없습니다', false);
      return;
    }
    const items = strategies.map(s => strategyDtoToListExportItem(
      s,
      s.flowLayout ?? null,
    ));
    downloadStrategyJson(
      `전략목록_${new Date().toISOString().slice(0, 10)}`,
      buildStrategyListExportPayload(items),
    );
    showSnack(`${strategies.length}개 전략을 내보냈습니다`);
  }, [strategies]);

  const handleImportAll = useCallback(async () => {
    const text = await pickJsonFile();
    if (!text) return;
    try {
      const result = parseStrategyImportFile(text);
      if (result.kind !== 'list') {
        showSnack('전략 목록 형식 파일이 필요합니다', false);
        return;
      }
      const items = result.data.strategies;
      if (hasRegisteredUser()) {
        const persisted: StrategyDto[] = [];
        let failed = 0;
        for (const item of items) {
          try {
            const saved = await saveStrategy({
              name: item.name,
              description: item.description,
              buyCondition: item.buyCondition,
              sellCondition: item.sellCondition,
              flowLayout: item.flowLayout,
              enabled: item.enabled ?? true,
            });
            if (saved?.id) {
              persisted.push({
                id: saved.id,
                name: saved.name,
                description: saved.description,
                buyCondition: saved.buyCondition as LogicNode | null ?? null,
                sellCondition: saved.sellCondition as LogicNode | null ?? null,
                flowLayout: normalizeStrategyFlowLayout(saved.flowLayout) ?? item.flowLayout,
                enabled: saved.enabled ?? true,
                createdAt: saved.createdAt,
                updatedAt: saved.updatedAt,
              });
            } else {
              failed += 1;
            }
          } catch {
            failed += 1;
          }
        }
        if (persisted.length === 0) {
          showSnack('전략 저장에 실패했습니다', false);
          return;
        }
        setStrategies(prev => [...prev, ...persisted]);
        showSnack(
          failed > 0
            ? `${persisted.length}개 저장, ${failed}개 실패`
            : `${persisted.length}개 전략을 가져와 저장했습니다`,
          failed === 0,
        );
        return;
      }
      const baseId = Date.now();
      const imported = items.map((item, index) => listImportItemToStrategyDto(item, baseId + index));
      setStrategies(prev => [...prev, ...imported]);
      showSnack(`${imported.length}개 전략을 가져왔습니다 (로그인 시 DB에 저장됩니다)`, false);
    } catch (e) {
      showSnack(e instanceof Error ? e.message : '파일 읽기 실패', false);
    }
  }, []);

  const handleAddStartSection = useCallback(() => {
    handleEditorStateChange(prev => addExtraStartSection(prev));
    bumpLayoutSeed(layoutStrategyKey, signalTab);
  }, [handleEditorStateChange, bumpLayoutSeed, layoutStrategyKey, signalTab]);

  const applyPalette = useCallback((type: string, value: string, _label: string, composite = false) => {
    if (type === 'start') {
      handleAddStartSection();
      return;
    }
    const newNode = makeNode(type, value, signalTab, DEF, composite ? { composite: true } : undefined);
    handleEditorStateChange(prev => {
      const root = prev.root;
      if (!root) return updateBranchRoot(prev, START_NODE_ID, newNode);
      if (type === 'operator') {
        return updateBranchRoot(prev, START_NODE_ID, mergeAtRoot(root, newNode, true));
      }
      return updateBranchRoot(prev, START_NODE_ID, mergeAtRoot(root, newNode, false));
    });
  }, [signalTab, DEF, handleEditorStateChange, handleAddStartSection]);

  const applyPaletteItem = useCallback((item: PaletteItem) => {
    const stochPair = isStochOverboughtPairPaletteValue(item.value);
    const priceExtremeBreakout = isPriceExtremeBreakoutPairPaletteValue(item.value);
    const inflection33 = isInflection33PaletteValue(item.value);
    const stableStrategy = isStableStrategyPaletteValue(item.value);
    const composite = item.kind === 'composite' && !stochPair && !priceExtremeBreakout && !inflection33 && !stableStrategy;
    const newNode = stochPair
      ? buildStochOverboughtPairTree(item.secondaryIndicator ?? 'CCI', DEF)
      : priceExtremeBreakout
        ? buildPriceExtremeBreakoutTree(item.value, DEF)
        : inflection33
          ? buildInflection33Tree(item.value, DEF)
          : stableStrategy
            ? buildStableStrategyTree(item.value, DEF)
            : makeNode('indicator', item.value, signalTab, DEF, composite ? { composite: true } : undefined);
    if (!newNode) return;
    handleEditorStateChange(prev => {
      const root = prev.root;
      if (!root) return updateBranchRoot(prev, START_NODE_ID, newNode);
      return updateBranchRoot(prev, START_NODE_ID, mergeAtRoot(root, newNode, false));
    });
  }, [signalTab, DEF, handleEditorStateChange]);

  const applySidewaysFilter = useCallback((filterId: string) => {
    const newNode = buildSidewaysFilterNode(filterId, DEF, sidewaysPalette);
    if (!newNode) return;
    const root = currentRoot;
    if (!root) setCurrentRoot(newNode);
    else setCurrentRoot(mergeAtRoot(root, newNode, false));
    scheduleStrategyPersist();
  }, [DEF, currentRoot, setCurrentRoot, scheduleStrategyPersist, sidewaysPalette]);

  const templates = useMemo(() => getStrategyTemplates(DEF), [DEF]);

  const templateRows = useMemo(() => {
    const userRows: TemplatePaletteRow[] = userTemplates.map(t => ({
      key: t.id,
      source: 'user',
      label: t.label,
      description: t.description,
      signal: t.signal,
      user: t,
    }));
    const hidden = new Set(hiddenBuiltinTemplateIds);
    const builtinRows: TemplatePaletteRow[] = templates
      .filter(t => !hidden.has(getBuiltinTemplateId(t)))
      .map(t => {
        const builtinId = getBuiltinTemplateId(t);
        return {
          key: builtinId,
          source: 'builtin',
          label: t.label,
          description: 'description' in t ? t.description : undefined,
          signal: t.signal,
          builtinId,
          builtin: t,
        };
      });
    const compositeRows: TemplatePaletteRow[] = buildCompositePaletteTemplateRows(compositePalette, hidden);
    const sidewaysRows: TemplatePaletteRow[] = buildSidewaysPaletteTemplateRows(sidewaysPalette, hidden);
    return [...userRows, ...builtinRows, ...compositeRows, ...sidewaysRows];
  }, [userTemplates, templates, hiddenBuiltinTemplateIds, compositePalette, sidewaysPalette]);

  const filteredTemplateRows = useMemo(() => {
    const q = templateSearch.trim().toLowerCase();
    if (!q) return templateRows;
    return templateRows.filter(row => {
      const signalLabel = row.signal === 'buy' ? '매수' : '매도';
      const badge = row.source === 'user' ? '내 템플릿'
        : row.source === 'composite-palette' ? '복합지표'
          : row.source === 'sideways-palette' ? '횡보지표'
            : '';
      return (
        row.label.toLowerCase().includes(q)
        || row.description?.toLowerCase().includes(q)
        || signalLabel.includes(q)
        || row.signal.includes(q)
        || badge.includes(q)
      );
    });
  }, [templateRows, templateSearch]);

  const handleTemplate = useCallback((tmpl: StrategyTemplateDef) => {
    if (editorMode === 'graph') layoutFlushRef.current?.();

    const targetTab = tmpl.signal;
    const nextRoot = tmpl.kind === 'composite' ? tmpl.build() : simpleTemplateToNode(tmpl);

    setBuyCondition(null);
    setSellCondition(null);
    setSelectedNodeId(null);

    if (targetTab === 'buy') setBuyCondition(nextRoot);
    else setSellCondition(nextRoot);

    resetFlowLayout(layoutStrategyKey, targetTab);
    if (targetTab !== signalTab) setSignalTab(targetTab);
    scheduleStrategyPersist();

    showSnack(`"${tmpl.label}" 템플릿이 적용됐습니다`);
  }, [editorMode, layoutStrategyKey, resetFlowLayout, signalTab, scheduleStrategyPersist]);

  const applyUserTemplate = useCallback((tmpl: UserStrategyTemplate) => {
    if (editorMode === 'graph') layoutFlushRef.current?.();

    const targetTab = tmpl.signal;
    const layoutFromState = {
      startMeta: tmpl.editorState.startMeta,
      extraStartIds: tmpl.editorState.extraStartIds,
      extraRoots: tmpl.editorState.extraRoots ?? {},
      startCombineOp: normalizeStartCombineOp(tmpl.editorState.startCombineOp),
      positions: {},
      edgeHandles: {},
    };

    setBuyCondition(null);
    setSellCondition(null);
    setBuyOrphans([]);
    setSellOrphans([]);
    setSelectedNodeId(null);

    if (targetTab === 'buy') {
      setBuyCondition(tmpl.editorState.root);
      setBuyLayout(prev => ({ ...prev, ...layoutFromState }));
      setBuyOrphans(tmpl.orphans);
      setSellLayout(normalizeTabLayoutState(emptySignalFlowLayout()));
    } else {
      setSellCondition(tmpl.editorState.root);
      setSellLayout(prev => ({ ...prev, ...layoutFromState }));
      setSellOrphans(tmpl.orphans);
      setBuyLayout(normalizeTabLayoutState(emptySignalFlowLayout()));
    }

    bumpLayoutSeed(layoutStrategyKey, targetTab);
    if (targetTab !== signalTab) setSignalTab(targetTab);
    scheduleStrategyPersist();
    showSnack(`"${tmpl.label}" 템플릿이 적용됐습니다`);
  }, [
    editorMode,
    layoutStrategyKey,
    bumpLayoutSeed,
    signalTab,
    scheduleStrategyPersist,
  ]);

  const handleDeleteUserTemplate = useCallback((id: string, label: string) => {
    if (!window.confirm(`"${label}" 템플릿을 목록에서 삭제할까요?`)) return;
    setUserTemplates(deleteUserStrategyTemplate(id));
    showSnack('템플릿을 삭제했습니다');
  }, []);

  const handleDeleteBuiltinTemplate = useCallback((id: string, label: string) => {
    if (!window.confirm(`"${label}" 템플릿을 목록에서 삭제할까요?`)) return;
    setHiddenBuiltinTemplateIds(hideBuiltinTemplate(id));
    showSnack('템플릿을 목록에서 제거했습니다');
  }, []);

  const openTemplateAddModal = useCallback(() => {
    const hasContent = encodeConditionForSave(currentEditorState) != null || currentOrphans.length > 0;
    if (!hasContent) {
      showSnack('저장할 조건이 없습니다', false);
      return;
    }
    setTemplateModalMode('add');
    setTemplateEditId(null);
    setTemplateSaveUpdateContent(false);
    setTemplateSaveLabel(stratName.trim() || `${signalTab === 'buy' ? '매수' : '매도'} 템플릿`);
    setTemplateSaveDesc('');
    setTemplateSaveLabelError(false);
    setTemplateSaveOpen(true);
  }, [currentEditorState, currentOrphans.length, stratName, signalTab]);

  const openTemplateEditModal = useCallback(() => {
    if (!selectedTemplateKey) {
      window.alert('수정할 템플릿을 먼저 선택해 주세요.');
      return;
    }
    const row = templateRows.find(r => r.key === selectedTemplateKey);
    if (!row) return;
    if (row.source !== 'user' || !row.user) {
      window.alert('내 템플릿만 수정할 수 있습니다.');
      return;
    }
    setTemplateModalMode('edit');
    setTemplateEditId(row.user.id);
    setTemplateSaveUpdateContent(false);
    setTemplateSaveLabel(row.user.label);
    setTemplateSaveDesc(row.user.description ?? '');
    setTemplateSaveLabelError(false);
    setTemplateSaveOpen(true);
  }, [selectedTemplateKey, templateRows]);

  const handleTemplateToolbarDelete = useCallback(() => {
    if (!selectedTemplateKey) {
      window.alert('삭제할 템플릿을 먼저 선택해 주세요.');
      return;
    }
    const row = templateRows.find(r => r.key === selectedTemplateKey);
    if (!row) return;
    if (row.source === 'user' && row.user) {
      handleDeleteUserTemplate(row.user.id, row.label);
    } else if (row.source === 'builtin' && row.builtinId) {
      handleDeleteBuiltinTemplate(row.builtinId, row.label);
    } else if (row.source === 'composite-palette' || row.source === 'sideways-palette') {
      handleDeleteBuiltinTemplate(row.key, row.label);
    }
    setSelectedTemplateKey(null);
  }, [selectedTemplateKey, templateRows, handleDeleteUserTemplate, handleDeleteBuiltinTemplate]);

  const handleTemplateApply = useCallback((row: TemplatePaletteRow) => {
    if (row.source === 'user' && row.user) {
      applyUserTemplate(row.user);
      return;
    }
    if (row.source === 'builtin' && row.builtin) {
      handleTemplate(row.builtin);
      return;
    }
    if (row.source === 'composite-palette' && row.paletteItem) {
      applyPaletteItem(row.paletteItem);
      showSnack(`"${row.label}" 조건이 추가됐습니다`);
      return;
    }
    if (row.source === 'sideways-palette' && row.sidewaysFilterId) {
      applySidewaysFilter(row.sidewaysFilterId);
      showSnack(`"${row.label}" 필터가 추가됐습니다`);
    }
  }, [applyUserTemplate, handleTemplate, applyPaletteItem, applySidewaysFilter]);

  const handleTemplateSaveConfirm = useCallback(() => {
    const label = templateSaveLabel.trim();
    if (!label) {
      setTemplateSaveLabelError(true);
      return;
    }
    if (templateModalMode === 'edit' && templateEditId) {
      const patch: Parameters<typeof updateUserStrategyTemplate>[1] = {
        label,
        description: templateSaveDesc.trim() || undefined,
      };
      if (templateSaveUpdateContent) {
        const hasContent = encodeConditionForSave(currentEditorState) != null || currentOrphans.length > 0;
        if (!hasContent) {
          showSnack('갱신할 조건이 없습니다', false);
          return;
        }
        patch.signal = signalTab;
        patch.editorState = toEditorState(currentRoot, currentLayout);
        patch.orphans = currentOrphans;
      }
      setUserTemplates(updateUserStrategyTemplate(templateEditId, patch));
      setTemplateSaveOpen(false);
      showSnack(`"${label}" 템플릿을 수정했습니다`);
      return;
    }
    const saved = addUserStrategyTemplate({
      label,
      description: templateSaveDesc.trim() || undefined,
      signal: signalTab,
      editorState: toEditorState(currentRoot, currentLayout),
      orphans: currentOrphans,
    });
    setUserTemplates(prev => [...prev, saved]);
    setSelectedTemplateKey(saved.id);
    setTemplateSaveOpen(false);
    setRightTab('templates');
    showSnack(`"${saved.label}" 템플릿을 저장했습니다`);
  }, [
    templateSaveLabel,
    templateSaveDesc,
    templateModalMode,
    templateEditId,
    templateSaveUpdateContent,
    signalTab,
    currentRoot,
    currentLayout,
    currentOrphans,
    currentEditorState,
  ]);

  const handleQuickRun = useCallback(async () => {
    if (!selectedId) {
      window.alert('백테스팅할 전략을 먼저 선택해주세요.');
      return;
    }
    setQbLoading(true);
    setQbError(null);
    try {
      const selectedStrategy = strategies.find(s => s.id === selectedId);
      const strategyTimeframe = resolveStrategyPrimaryTimeframe(
        selectedStrategy,
        { buy: buyEditorState, sell: sellEditorState },
      );
      const execTimeframe = resolveBacktestExecTimeframe(qbTimeframe, strategyTimeframe);
      const [settings, indicatorParams] = await Promise.all([
        loadBacktestSettings(),
        loadIndicatorSettings(),
      ]);
      const { bars, evaluationBarCount } = await fetchBacktestCandleBundle({
        market: qbMarket,
        timeframe: execTimeframe as import('../types').Timeframe,
        buyDsl: selectedStrategy?.buyCondition,
        sellDsl: selectedStrategy?.sellCondition,
        indicatorParams,
      });
      if (bars.length < 5) {
        throw new Error('캔들 데이터가 부족합니다. 타임프레임을 변경하거나 잠시 후 다시 시도해주세요.');
      }
      const result = await runBacktest({
        strategyId:     selectedId,
        bars,
        timeframe:      execTimeframe,
        settings,
        indicatorParams,
        symbol:         qbMarket,
        strategyName:   selectedStrategy?.name,
        evaluationBarCount,
      });
      if (!result) throw new Error('백테스팅 실행에 실패했습니다.');
      if (result.resultId) {
        try { sessionStorage.setItem(BACKTEST_FOCUS_KEY, String(result.resultId)); } catch { /* ignore */ }
      }
      onNavigateToBacktest?.();
    } catch (e) {
      setQbError(e instanceof Error ? e.message : '실행 중 오류가 발생했습니다.');
    } finally {
      setQbLoading(false);
    }
  }, [selectedId, strategies, qbMarket, qbTimeframe, buyEditorState, sellEditorState, onNavigateToBacktest]);

  const qbStrategyTimeframe = useMemo(
    () => resolveStrategyPrimaryTimeframe(
      selectedId ? strategies.find(s => s.id === selectedId) : undefined,
      { buy: buyEditorState, sell: sellEditorState },
    ),
    [selectedId, strategies, buyEditorState, sellEditorState],
  );
  const qbTimeframeOptions = useMemo(
    () => buildQuickRunTimeframeSelectOptions(qbStrategyTimeframe),
    [qbStrategyTimeframe],
  );

  const operators = [
    { type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
    { type: 'operator' as const, value: 'OR', label: 'OR', color: 'logic-or' },
    { type: 'operator' as const, value: 'NOT', label: 'NOT', color: 'logic-not' },
  ];

  const maBandItems = [
    { type: 'indicator' as const, value: 'MA', label: 'MA', desc: '이동평균', color: 'band' },
    { type: 'indicator' as const, value: 'EMA', label: 'EMA', desc: '지수이동평균', color: 'band' },
    { type: 'indicator' as const, value: 'BOLLINGER', label: 'Bollinger', desc: '볼린저밴드', color: 'band' },
    { type: 'indicator' as const, value: 'DONCHIAN', label: 'Donchian', desc: '돈치안 채널', color: 'band' },
    { type: 'indicator' as const, value: 'ICHIMOKU', label: 'Ichimoku', desc: '일목균형표', color: 'band' },
  ];

  const q = paletteSearch.trim().toLowerCase();
  const match = (label: string, desc?: string) =>
    !q || label.toLowerCase().includes(q) || (desc?.toLowerCase().includes(q) ?? false);

  const paletteKey = (type: string, id: string) => `${type}:${id}`;
  const selectPalette = (type: string, id: string) => {
    setSelectedPaletteKey(paletteKey(type, id));
  };
  const isPaletteSelected = (type: string, id: string) =>
    selectedPaletteKey === paletteKey(type, id);

  return (
    <div className={`se-page se-page--${theme}${variant === 'modal' ? ' se-page--modal' : ''}`}>
      {needsPointerPaletteDrag() ? <PaletteDragOverlay /> : null}
      {saveToast && (
        <div className="se-save-toast">전략이 DB에 성공적으로 저장되었습니다.</div>
      )}

      <header className="se-header">
        <div className="se-header-left">
          <h1 className="se-title">전략 빌더</h1>
          <span className="se-subtitle">
            Strategy Builder
            {!settingsLoaded && (
              <span className="se-subtitle-hint"> · 보조지표 설정 로드 중…</span>
            )}
          </span>
        </div>

        {/* 빠른 백테스팅 실행 툴바 */}
        <div className="se-quick-run">
          <button
            ref={qbMarketBtnRef}
            type="button"
            className="se-qr-market-btn"
            disabled={qbLoading}
            title="종목 검색"
            onClick={() => { setQbMarketQ(''); setQbMarketOpen(v => !v); }}
          >
            <span className="se-qr-market-ko">{getKoreanName(qbMarket)}</span>
            <span className="se-qr-market-sym">{qbMarket.replace('KRW-', '')}</span>
            <svg className="se-qr-market-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
              <polyline points="6 9 12 15 18 9"/>
            </svg>
          </button>
          {qbMarketOpen && ReactDOM.createPortal(
            <MarketSearchPanel
              currentMarket={qbMarket}
              query={qbMarketQ}
              onQueryChange={setQbMarketQ}
              onSelect={market => { setQbMarket(market); setQbMarketOpen(false); setQbMarketQ(''); }}
              onClose={() => { setQbMarketOpen(false); setQbMarketQ(''); }}
              anchorRect={qbMarketBtnRef.current?.getBoundingClientRect() ?? null}
              anchorPlacement="dropdown"
            />,
            document.body,
          )}
          <select
            className="se-qr-select"
            value={qbTimeframe}
            onChange={e => setQbTimeframe(parseBacktestRunTimeframeChoice(e.target.value))}
            disabled={qbLoading}
            title="백테스팅 실행 시간봉"
          >
            {qbTimeframeOptions.map(tf => (
              <option key={tf.value} value={tf.value}>{tf.label}</option>
            ))}
          </select>
          <select
            className="se-qr-select se-qr-select--strategy"
            value={selectedId ?? ''}
            onChange={e => {
              const id = e.target.value ? Number(e.target.value) : null;
              if (!id) return;
              const s = strategies.find(st => st.id === id);
              if (s) handleSelectStrategy(s);
            }}
            disabled={qbLoading}
            title="전략 선택"
          >
            <option value="">전략 선택</option>
            {strategies.map(s => (
              <option key={s.id} value={s.id}>{s.name}</option>
            ))}
          </select>
          <button
            type="button"
            className={`se-qr-run-btn${qbLoading ? ' se-qr-run-btn--loading' : ''}`}
            disabled={!selectedId || qbLoading}
            onClick={() => { void handleQuickRun(); }}
            title={selectedId ? '백테스팅 실행' : '전략을 먼저 선택하세요'}
          >
            {qbLoading ? (
              <span className="se-qr-spinner" />
            ) : (
              <svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor" aria-hidden="true">
                <path d="M3 2.5l10 5.5-10 5.5V2.5z"/>
              </svg>
            )}
          </button>
          {qbError && (
            <span className="se-qr-error" title={qbError}>⚠ {qbError}</span>
          )}
        </div>

        <div className="se-header-actions">
          {variant === 'modal' && onClose && (
            <button
              type="button"
              className="se-btn se-btn--ghost se-btn--icon se-btn--modal-close"
              title="닫기"
              aria-label="전략 편집 닫기"
              onClick={onClose}
            >
              ×
            </button>
          )}
          <button
            type="button"
            className="se-btn se-btn--ghost se-btn--icon se-btn--desc"
            title="전략 설명 — 현재 조건을 서술형으로 보기"
            aria-label="전략 설명"
            onClick={() => setDescOpen(true)}
          >
            <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden className="se-desc-icon">
              <circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" strokeWidth="1.75" />
              <path
                fill="currentColor"
                d="M11.25 10.5h1.5V17h-1.5V10.5zm0-3.25h1.5V9h-1.5V6.25z"
              />
            </svg>
          </button>
          <button
            type="button"
            className="se-btn se-btn--ghost se-btn--icon"
            title="현재 전략 JSON 내보내기"
            onClick={handleExport}
            disabled={!buyCondition && !sellCondition}
          >
            ↓
          </button>
          <button
            type="button"
            className="se-btn se-btn--ghost se-btn--icon"
            title="JSON 파일에서 전략 가져오기"
            onClick={() => { void handleImport(); }}
          >
            ↑
          </button>
          <button
            type="button"
            className="se-btn se-btn--ghost se-btn--icon"
            title="전체 전략 목록 JSON 내보내기"
            onClick={handleExportAll}
            disabled={strategies.length === 0}
          >
            ⬇
          </button>
          <button
            type="button"
            className="se-btn se-btn--ghost se-btn--icon"
            title="전체 전략 목록 JSON 가져오기"
            onClick={() => { void handleImportAll(); }}
          >
            ⬆
          </button>
        </div>
      </header>

      <div className="se-body" style={bodyStyle}>
        <aside className="se-left" style={{ width: leftWidth }}>
          <div className="se-strat-panel">
            <div className="se-strat-panel-head">
              <h2 className="se-panel-title">전략 목록</h2>
            </div>
            <button type="button" className="se-new-strat-btn" onClick={handleNew}>
              ＋ 새 전략 만들기
            </button>
            <div className="se-strat-list">
              {strategies.length === 0 ? (
                <p className="se-empty">저장된 전략이 없습니다</p>
              ) : (
                strategies.map(s => {
                  const isSel = selectedId === s.id;
                  return (
                    <div
                      key={s.id}
                      role="button"
                      tabIndex={0}
                      className={`se-strat-item${isSel ? ' se-strat-item--sel' : ''}`}
                      onClick={() => handleSelectStrategy(s)}
                      onKeyDown={e => {
                        if (e.key === 'Enter' || e.key === ' ') {
                          e.preventDefault();
                          handleSelectStrategy(s);
                        }
                      }}
                    >
                      <span className="se-strat-name" title={s.name}>{s.name}</span>
                      <div className="se-strat-item-actions">
                        <button
                          type="button"
                          className="se-strat-del"
                          title="전략 삭제"
                          onClick={e => {
                            e.stopPropagation();
                            setDeleteId(s.id);
                            setDeleteOpen(true);
                          }}
                        >
                          <svg viewBox="0 0 16 16" width="17" height="17" aria-hidden>
                            <path
                              fill="currentColor"
                              d="M5.5 2a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v.5H12a.5.5 0 0 1 0 1h-.55l-.62 8.07A1.5 1.5 0 0 1 9.83 13H6.17a1.5 1.5 0 0 1-1.49-1.43L4.05 3.5H4a.5.5 0 0 1 0-1h1.5V2zm1.5 0v.5h2V2H7zm-2.38 1.5l.58 7.53a.5.5 0 0 0 .5.47h3.66a.5.5 0 0 0 .5-.47l.58-7.53H4.62z"
                            />
                          </svg>
                        </button>
                        <span className={`se-strat-status${s.enabled ? ' se-strat-status--on' : ''}`}>
                          {s.enabled ? '활성' : '비활성'}
                        </span>
                      </div>
                    </div>
                  );
                })
              )}
            </div>
          </div>
        </aside>

        <div
          className="se-splitter se-splitter--v"
          role="separator"
          aria-orientation="vertical"
          aria-label="전략 목록 너비 조절"
          onPointerDown={onLeftSplitter}
        />

        <div className="se-main">
          <div className="se-main-row">
            <main className="se-center">
          <div className="se-center-panel">
            <div className={`se-center-work${editorMode === 'list' ? ' se-center-work--list' : ''}`}>
            <div className="se-center-head">
            <div className="se-signal-tabs">
              <button
                type="button"
                className={`se-signal-tab${signalTab === 'buy' ? ' se-signal-tab--buy-on' : ''}`}
                onClick={() => switchSignalTab('buy')}
              >
                매수 조건 (Entry)
              </button>
              <button
                type="button"
                className={`se-signal-tab${signalTab === 'sell' ? ' se-signal-tab--sell-on' : ''}`}
                onClick={() => switchSignalTab('sell')}
              >
                매도 조건 (Exit)
              </button>
            </div>
            {stratName && <span className="se-editing-name">{stratName}</span>}
            {hasMultipleStartSections(currentEditorState) && (
              <StartCombineOpControl
                value={normalizeStartCombineOp(currentEditorState.startCombineOp)}
                onChange={handleStartCombineOpChange}
              />
            )}
            <div className="se-center-head-actions">
              <button type="button" className="se-btn se-btn--ghost se-btn--sm" onClick={handleNew}>＋ 새 전략</button>
              <div className="se-editor-mode se-editor-mode--compact" role="group" aria-label="편집 방식">
                <button
                  type="button"
                  className={`se-editor-mode-btn${editorMode === 'graph' ? ' se-editor-mode-btn--on' : ''}`}
                  onClick={() => handleEditorModeChange('graph')}
                >
                  그래프 방식
                </button>
                <button
                  type="button"
                  className={`se-editor-mode-btn${editorMode === 'list' ? ' se-editor-mode-btn--on' : ''}`}
                  onClick={() => handleEditorModeChange('list')}
                >
                  목록 방식
                </button>
              </div>
              <button
                type="button"
                className="se-btn se-btn--ghost se-btn--sm"
                onClick={openTemplateAddModal}
                title="현재 탭(매수/매도) 조건을 템플릿 목록에 저장"
              >
                템플릿 저장
              </button>
              <button
                type="button"
                className="se-btn se-btn--gold se-btn--sm"
                onClick={() => openSaveDialog(selectedId ? 'update' : 'create')}
              >
                저장하기
              </button>
              <button
                type="button"
                className="se-btn se-btn--ghost se-btn--sm"
                onClick={() => openSaveDialog('saveAs')}
                title="현재 조건을 다른 이름으로 새 전략으로 저장 (기존 전략은 유지)"
              >
                새로저장
              </button>
            </div>
            </div>

          {editorMode === 'graph' ? (
            <>
              <ReactFlowProvider>
                <StrategyEditorCanvas
                  theme={theme}
                  root={currentRoot}
                  orphans={currentOrphans}
                  onOrphansChange={handleOrphansChange}
                  def={DEF}
                  signalTab={signalTab}
                  onChange={handleRootChange}
                  selectedNodeId={selectedNodeId}
                  onSelectNode={setSelectedNodeId}
                  layoutSeed={layoutSeed}
                  onLayoutChange={handleLayoutChange}
                  startMeta={currentLayout.startMeta}
                  extraStartIds={currentLayout.extraStartIds}
                  extraRoots={currentLayout.extraRoots}
                  onStartMetaChange={handleStartMetaChange}
                  onStartCandleTypesChange={handleStartCandleTypesChange}
                  onExtraStartIdsChange={handleExtraStartIdsChange}
                  onExtraRootsChange={handleExtraRootsChange}
                />
              </ReactFlowProvider>

              {selectedLogicNode?.type === 'CONDITION' && selectedLogicNode.condition && (
                <div className="se-node-config-bar">
                  <span className="se-node-config-label">
                    {stochPairEditForSelected && !stochPairEditForSelected.stochLocked
                      ? 'Stoch 과열×보조'
                      : selectedLogicNode.condition.indicatorType}
                  </span>
                  <CondEditor
                    cond={selectedLogicNode.condition}
                    signalType={signalTab}
                    def={DEF}
                    stochPairEdit={stochPairEditForSelected}
                    onChange={c => {
                      if (!selectedNodeId) return;
                      const inOrphans = currentOrphans.some(o => o.id === selectedNodeId);
                      if (inOrphans) {
                        handleOrphansChange(currentOrphans.map(o => (
                          o.id === selectedNodeId ? updateNode(o, selectedNodeId, n => ({ ...n, condition: c })) : o
                        )));
                        return;
                      }
                      if (currentRoot && findLogicNode(currentRoot, currentOrphans, selectedNodeId, currentLayout.extraRoots ?? {})) {
                        if (findLogicNode(currentRoot, [], selectedNodeId)) {
                          handleRootChange(updateNode(currentRoot, selectedNodeId, n => ({ ...n, condition: c })));
                          return;
                        }
                        for (const [startId, branch] of Object.entries(currentLayout.extraRoots ?? {})) {
                          if (branch && findLogicNode(branch, [], selectedNodeId)) {
                            handleExtraRootsChange({
                              ...(currentLayout.extraRoots ?? {}),
                              [startId]: updateNode(branch, selectedNodeId, n => ({ ...n, condition: c })),
                            });
                            return;
                          }
                        }
                      }
                    }}
                  />
                  <span className="se-sync-tip">전략 조건 전용 설정 · 차트 보조지표와 무관</span>
                </div>
              )}

              {selectedLogicNode && isStochOverboughtPairRoot(selectedLogicNode) && selectedLogicNode.stochPair && (
                <div className="se-node-config-bar se-node-config-bar--stoch-pair">
                  <span className="se-node-config-label">Stoch 과열×보조</span>
                  <StochPairNodeSettings
                    variant="inline"
                    secondaryIndicator={selectedLogicNode.stochPair.secondaryIndicatorType}
                    onChange={next => applyStochPairSecondary(selectedLogicNode.id, next)}
                    onClose={() => {}}
                  />
                </div>
              )}

              {selectedLogicNode && isPriceExtremeBreakoutPairRoot(selectedLogicNode) && selectedLogicNode.priceExtremePair && (
                <div className="se-node-config-bar se-node-config-bar--price-extreme">
                  <span className="se-node-config-label">9·20 신고가/신저가 필터</span>
                  <PriceExtremePairNodeSettings
                    variant="inline"
                    mode={selectedLogicNode.priceExtremePair.mode}
                    shortPeriod={selectedLogicNode.priceExtremePair.shortPeriod}
                    longPeriod={selectedLogicNode.priceExtremePair.longPeriod}
                    filterLevel={inferPriceExtremeFilterLevel(selectedLogicNode)}
                    onChange={next => applyPriceExtremeFilterLevel(selectedLogicNode.id, next)}
                    onClose={() => {}}
                  />
                </div>
              )}
              {selectedLogicNode && isInflection33PairRoot(selectedLogicNode) && selectedLogicNode.inflection33Pair && (
                <div className="se-node-config-bar se-node-config-bar--inflection33">
                  <span className="se-node-config-label">33변곡 필터</span>
                  <Inflection33PairNodeSettings
                    variant="inline"
                    mode={selectedLogicNode.inflection33Pair.mode}
                    period={selectedLogicNode.inflection33Pair.period}
                    filterLevel={inferInflection33FilterLevel(selectedLogicNode)}
                    onChange={next => applyInflection33FilterLevel(selectedLogicNode.id, next)}
                    onClose={() => {}}
                  />
                </div>
              )}
              {selectedLogicNode && isStableStrategyPairRoot(selectedLogicNode) && selectedLogicNode.stableStrategyPair && (
                <div className="se-node-config-bar se-node-config-bar--stable-strategy">
                  <span className="se-node-config-label">추천 전략 필터</span>
                  <StableStrategyPairNodeSettings
                    variant="inline"
                    strategyId={selectedLogicNode.stableStrategyPair.strategyId as StableStrategyId}
                    mode={selectedLogicNode.stableStrategyPair.mode}
                    filterLevel={inferStableStrategyFilterLevel(selectedLogicNode)}
                    def={DEF}
                    onChange={next => applyStableStrategyFilterLevel(selectedLogicNode.id, next)}
                    onClose={() => {}}
                  />
                </div>
              )}
            </>
          ) : (
            <StrategyListEditor
              editorState={currentEditorState}
              signalTab={signalTab}
              def={DEF}
              onEditorStateChange={handleEditorStateChange}
              onAddStart={handleAddStartSection}
              orphans={currentOrphans}
              onOrphansChange={handleOrphansChange}
            />
          )}

          </div>

          <div
            className="se-splitter se-splitter--h"
            role="separator"
            aria-orientation="horizontal"
            aria-label="Logic Expression 높이 조절"
            onPointerDown={onTerminalSplitter}
          />

          <footer className="se-terminal" style={{ height: terminalHeight }}>
            <div className="se-terminal-scroll">
              <section className="se-terminal-section">
                <div className="se-terminal-label">LOGIC EXPRESSION</div>
                <LogicExpressionPreview
                  buyCondition={buyCondition}
                  sellCondition={sellCondition}
                  buyEditorState={buyEditorState}
                  sellEditorState={sellEditorState}
                  orphanCount={orphanTotal}
                  def={DEF}
                />
              </section>
              <section className="se-terminal-section se-terminal-section--narrative">
                <div className="se-terminal-label se-terminal-label--guide">조건 설명</div>
                <LogicExpressionNarrative
                  name={stratName}
                  description={stratDesc}
                  buyCondition={buyCondition}
                  sellCondition={sellCondition}
                  buyEditorState={buyEditorState}
                  sellEditorState={sellEditorState}
                  orphanCount={orphanTotal}
                  def={DEF}
                  compact
                />
              </section>
            </div>
          </footer>
          </div>
        </main>

        {rightOpen && (
          <div
            className="se-splitter se-splitter--v"
            role="separator"
            aria-orientation="vertical"
            aria-label="우측 패널 너비 조절"
            onPointerDown={onRightSplitter}
          />
        )}

        <div className={`se-side-wrap se-side-wrap--right${rightOpen ? ' se-side-wrap--open' : ''}`}>
          <SePanelCollapseHandle
            side="right"
            open={rightOpen}
            onToggle={toggleRightPanel}
            label="지표 패널"
          />
          <aside className={`se-right se-right--collapsible${rightOpen ? ' se-right--collapsible--open' : ''}`}>
          <div className="se-palette-panel">
          <div className="se-right-tabs">
            <button type="button" className={rightTab === 'indicators' ? 'se-right-tab se-right-tab--on' : 'se-right-tab'} onClick={() => setRightTab('indicators')}>지표</button>
            <button type="button" className={rightTab === 'templates' ? 'se-right-tab se-right-tab--on' : 'se-right-tab'} onClick={() => setRightTab('templates')}>템플릿</button>
          </div>
          <div className="se-right-body">
          {rightTab === 'indicators' && (
            <>
              <input
                className="se-palette-search"
                placeholder="지표 검색 (RSI, MACD…)"
                value={paletteSearch}
                onChange={e => setPaletteSearch(e.target.value)}
              />
              <div className="se-palette-section se-palette-section--logic">
                <h3>시작 · 논리</h3>
                <div className="se-palette-grid se-palette-grid--3">
                  <PaletteChip
                    type="start"
                    value="START"
                    label="START"
                    desc="시간봉 시작점"
                    color="logic-start"
                    selected={selectedPaletteKey === 'start:START'}
                    onSelect={() => setSelectedPaletteKey('start:START')}
                    onAdd={() => applyPalette('start', 'START', 'START')}
                  />
                  {operators.map(op => (
                    <PaletteChip
                      key={op.value}
                      {...op}
                      selected={isPaletteSelected(op.type, op.value)}
                      onSelect={() => selectPalette(op.type, op.value)}
                      onAdd={() => applyPalette(op.type, op.value, op.label)}
                    />
                  ))}
                </div>
              </div>
              <div className="se-palette-section se-palette-section--band">
                <h3>밴드 · 추세</h3>
                <div className="se-palette-grid se-palette-grid--3">
                  {maBandItems.filter(i => match(i.label, i.desc)).map(item => (
                    <PaletteChip
                      key={item.value}
                      {...item}
                      period={getIndicatorPeriodLabel(item.value, DEF)}
                      selected={isPaletteSelected(item.type, item.value)}
                      onSelect={() => selectPalette(item.type, item.value)}
                      onAdd={() => applyPalette(item.type, item.value, item.label)}
                    />
                  ))}
                </div>
              </div>
              <div className="se-palette-subtabs">
                <button
                  type="button"
                  className={`se-palette-subtab se-palette-subtab--aux${indicatorSubTab === 'auxiliary' ? ' se-palette-subtab--on' : ''}`}
                  onClick={() => {
                    setIndicatorSubTab('auxiliary');
                    setSelectedPaletteKey(null);
                  }}
                >
                  보조지표
                </button>
                <button
                  type="button"
                  className={`se-palette-subtab se-palette-subtab--composite${indicatorSubTab === 'composite' ? ' se-palette-subtab--on' : ''}`}
                  onClick={() => {
                    setIndicatorSubTab('composite');
                    setSelectedPaletteKey(null);
                  }}
                >
                  복합지표
                </button>
                <button
                  type="button"
                  className={`se-palette-subtab se-palette-subtab--range${indicatorSubTab === 'range' ? ' se-palette-subtab--on' : ''}`}
                  onClick={() => {
                    setIndicatorSubTab('range');
                    setSelectedPaletteKey(null);
                  }}
                >
                  횡보필터
                </button>
              </div>
              {indicatorSubTab === 'auxiliary' ? (
                <IndicatorPaletteTab
                  kind="auxiliary"
                  items={auxiliaryPalette}
                  onItemsChange={setAuxiliaryPalette}
                  def={DEF}
                  searchQuery={paletteSearch}
                  selectedItemId={
                    selectedPaletteKey?.startsWith('auxiliary:')
                      ? selectedPaletteKey.slice('auxiliary:'.length)
                      : null
                  }
                  onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('auxiliary', id) : null)}
                  onAddToCanvas={item => {
                    selectPalette('auxiliary', item.id);
                    applyPaletteItem(item);
                  }}
                />
              ) : indicatorSubTab === 'composite' ? (
                <IndicatorPaletteTab
                  kind="composite"
                  items={compositePalette}
                  onItemsChange={setCompositePalette}
                  def={DEF}
                  searchQuery={paletteSearch}
                  selectedItemId={
                    selectedPaletteKey?.startsWith('composite:')
                      ? selectedPaletteKey.slice('composite:'.length)
                      : null
                  }
                  onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('composite', id) : null)}
                  onAddToCanvas={item => {
                    selectPalette('composite', item.id);
                    applyPaletteItem(item);
                  }}
                />
              ) : (
                <SidewaysFilterPaletteTab
                  def={DEF}
                  items={sidewaysPalette}
                  onItemsChange={setSidewaysPalette}
                  searchQuery={paletteSearch}
                  selectedItemId={
                    selectedPaletteKey?.startsWith('range:')
                      ? selectedPaletteKey.slice('range:'.length)
                      : null
                  }
                  onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('range', id) : null)}
                  onAddToCanvas={item => {
                    selectPalette('range', item.id);
                    applySidewaysFilter(item.id);
                  }}
                />
              )}
            </>
          )}
          {rightTab === 'templates' && (
            <TemplatePaletteTab
              rows={filteredTemplateRows}
              searchQuery={templateSearch}
              onSearchChange={setTemplateSearch}
              selectedKey={selectedTemplateKey}
              onSelectKey={setSelectedTemplateKey}
              onApply={handleTemplateApply}
              onAdd={openTemplateAddModal}
              onEdit={openTemplateEditModal}
              onDelete={handleTemplateToolbarDelete}
            />
          )}
          </div>
          </div>
        </aside>
        </div>
          </div>
        </div>
      </div>

      {templateSaveOpen && (
        <DraggableModalFrame
          onClose={() => { setTemplateSaveLabelError(false); setTemplateSaveOpen(false); }}
          title={templateModalMode === 'edit' ? '템플릿 수정' : '템플릿 저장'}
        >
          <div className="se-modal-body">
            <p className="se-template-save-hint">
              {templateModalMode === 'edit'
                ? <>선택한 <strong>내 템플릿</strong>의 이름·설명을 수정합니다.</>
                : <>현재 <strong>{signalTab === 'buy' ? '매수' : '매도'}</strong> 탭 조건을 템플릿 목록에 저장합니다.</>}
            </p>
            <label className="se-field-lbl">템플릿명 *</label>
            <input
              className={`se-field-inp${templateSaveLabelError ? ' se-field-inp--error' : ''}`}
              value={templateSaveLabel}
              onChange={e => { setTemplateSaveLabel(e.target.value); setTemplateSaveLabelError(false); }}
              placeholder="예: RSI 과매도 + MA 골든"
            />
            {templateSaveLabelError && (
              <p className="se-field-error" role="alert">템플릿 이름을 입력하세요</p>
            )}
            <label className="se-field-lbl">설명</label>
            <textarea
              className="se-field-ta"
              value={templateSaveDesc}
              onChange={e => setTemplateSaveDesc(e.target.value)}
              rows={2}
              placeholder="템플릿 설명 (선택)"
            />
            {templateModalMode === 'edit' && (
              <label className="se-template-save-update">
                <input
                  type="checkbox"
                  checked={templateSaveUpdateContent}
                  onChange={e => setTemplateSaveUpdateContent(e.target.checked)}
                />
                <span>현재 {signalTab === 'buy' ? '매수' : '매도'} 탭 조건으로 내용 갱신</span>
              </label>
            )}
            <div className="se-modal-actions">
              <button type="button" className="se-btn se-btn--ghost" onClick={() => setTemplateSaveOpen(false)}>취소</button>
              <button type="button" className="se-btn se-btn--gold" onClick={handleTemplateSaveConfirm}>
                {templateModalMode === 'edit' ? '수정' : '저장'}
              </button>
            </div>
          </div>
        </DraggableModalFrame>
      )}

      {saveOpen && (
        <DraggableModalFrame
          onClose={() => { setSaveNameError(false); setSaveDuplicateError(false); setSaveOpen(false); }}
          title={saveMode === 'saveAs' ? '새 이름으로 저장' : '전략 저장'}
        >
          <div className="se-modal-body">
            {saveMode === 'saveAs' && selectedId && (
              <p className="se-template-save-hint">
                현재 편집 중인 조건을 <strong>새 전략</strong>으로 저장합니다.
                기존 전략「{strategies.find(s => s.id === selectedId)?.name ?? stratName}」은 변경되지 않습니다.
              </p>
            )}
            {saveMode === 'saveAs' && !selectedId && (
              <p className="se-template-save-hint">
                현재 조건을 <strong>새 전략</strong>으로 저장합니다.
              </p>
            )}
            <label className="se-field-lbl">전략명 *</label>
            <input
              className={`se-field-inp${saveNameError || saveDuplicateError ? ' se-field-inp--error' : ''}`}
              value={saveDraftName}
              onChange={e => {
                setSaveDraftName(e.target.value);
                setSaveNameError(false);
                setSaveDuplicateError(false);
              }}
              placeholder="예: Golden_RSI_V1"
            />
            {saveNameError && (
              <p className="se-field-error" role="alert">전략 이름을 입력하세요</p>
            )}
            {saveDuplicateError && (
              <p className="se-field-error" role="alert">같은 이름의 전략이 이미 있습니다</p>
            )}
            <label className="se-field-lbl">설명</label>
            <textarea
              className="se-field-ta"
              value={saveDraftDesc}
              onChange={e => setSaveDraftDesc(e.target.value)}
              rows={2}
            />
            <div className="se-modal-actions">
              <button type="button" className="se-btn se-btn--ghost" onClick={() => setSaveOpen(false)}>취소</button>
              <button type="button" className="se-btn se-btn--gold" disabled={isSaving} onClick={handleSave}>
                {isSaving ? '저장 중…' : (saveMode === 'saveAs' ? '새로저장' : '저장')}
              </button>
            </div>
          </div>
        </DraggableModalFrame>
      )}

      {deleteOpen && (
        <DraggableModalFrame onClose={() => setDeleteOpen(false)} title="전략 삭제">
          <p>이 전략을 삭제하시겠습니까?</p>
          <div className="se-modal-actions">
            <button type="button" className="se-btn se-btn--ghost" onClick={() => setDeleteOpen(false)}>취소</button>
            <button type="button" className="se-btn se-btn--danger" onClick={handleDeleteConfirm}>삭제</button>
          </div>
        </DraggableModalFrame>
      )}

      {descOpen && (
        <StrategyDescriptionModal
          onClose={() => setDescOpen(false)}
          name={stratName}
          description={stratDesc}
          buyEditorState={buyEditorState}
          sellEditorState={sellEditorState}
          buyCondition={buyCondition}
          sellCondition={sellCondition}
          orphanCount={orphanTotal}
          def={DEF}
        />
      )}

      {snack && <div className={`se-snack${snack.ok ? '' : ' se-snack--err'}`}>{snack.msg}</div>}
    </div>
  );
}
