/**
 * 멀티차트 대시보드 - 개별 차트 카드
 * 투자분석(CandlestickTestPage)과 동일한 CandlestickChart + useMarketDataWebSocket + setInterval 갱신 방식 사용
 */
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { Box, Typography, IconButton, CircularProgress, Tooltip, FormControl, Select, MenuItem, InputLabel, Button, Menu, ListItemIcon, ListItemText, Checkbox, Divider } from '@mui/material';
import { Add as AddIcon, Close as CloseIcon, Visibility as VisibilityIcon, Fullscreen as FullscreenIcon, FullscreenExit as FullscreenExitIcon, BarChart as BarChartIcon, KeyboardArrowDown, DragIndicator as DragIndicatorIcon } from '@mui/icons-material';
import { useDroppable, useDraggable } from '@dnd-kit/core';
import type { StockInfo } from '../../stores/useDashboardStore';
import CandlestickChart, { type ChartHandle, type AlertMarker } from '../CandlestickChart';
import type { Time } from 'lightweight-charts';
import { pickMarkerUnixSecondsForAlert } from '../../utils/alertSignalInference';
import { CandleData, type IndicatorData } from '../../services/backtestApi';
import { useMultiChartMarketData } from '../../contexts/MultiChartMarketDataContext';
import { useIndicatorSettings } from '../../contexts/IndicatorSettingsContext';
import { useIndicatorVisibility } from '../../contexts/IndicatorVisibilityContext';
import { useChartColors } from '../../contexts/ChartColorContext';
import { alignIndicatorRowsToCandleTimes, buildUpbitAnalyzePayload, normalizeAnalyzeIndicatorRow } from '../../utils/investmentChart/analyzePayload';
import axios from 'axios';
import { API_BASE_URL } from '../../services/apiConfig';

/** CandlestickChart ichimokuSettings 형식 */
interface IchimokuSettings {
  tenkan: boolean;
  kijun: boolean;
  chikou: boolean;
  senkouA: boolean;
  senkouB: boolean;
  tenkanPeriod: number;
  kijunPeriod: number;
  senkouBPeriod: number;
}

/** CandlestickChart overlaySettingsOverride (볼린저 등) */
interface OverlaySettingsOverride {
  bollingerUpper?: boolean;
  bollingerMiddle?: boolean;
  bollingerLower?: boolean;
  bollingerPeriod?: number;
  bollingerStdDev?: number;
}

/** 보조지표 체크박스 메뉴 (투자분석과 동일) */
const INDICATOR_MENU_ITEMS: { key: string; label: string }[] = [
  { key: 'volume', label: '거래량' },
  { key: 'newPsychological', label: '신심리도' },
  { key: 'investPsychological', label: '투자심리도' },
  { key: 'macd', label: 'MACD' },
  { key: 'cci', label: 'CCI' },
  { key: 'adx', label: 'ADX' },
  { key: 'bwi', label: '볼린저밴드 %B' },
  { key: 'dmi', label: 'DMI' },
  { key: 'obv', label: 'OBV' },
  { key: 'rsi', label: 'RSI' },
  { key: 'stochastic', label: 'Stochastic Slow' },
  { key: 'williamsR', label: 'Williams %R' },
  { key: 'trix', label: 'TRIX' },
  { key: 'volumeOsc', label: 'Volume OSC' },
  { key: 'vr', label: 'VR' },
  { key: 'disparity', label: '이격도' },
];

/** 시간봉 옵션 (MultiChartDashboard와 동일) */
const TIMEFRAME_OPTIONS = [
  { value: '1m', label: '1분' },
  { value: '3m', label: '3분' },
  { value: '5m', label: '5분' },
  { value: '10m', label: '10분' },
  { value: '15m', label: '15분' },
  { value: '30m', label: '30분' },
  { value: '1h', label: '1시간' },
  { value: '4h', label: '4시간' },
  { value: '1d', label: '일봉' },
];

interface ChartItemProps {
  id: string;
  stock: StockInfo | null;
  gridIndex: number;
  timeframe: string;
  refreshTrigger?: number;
  onAddClick: () => void;
  onRemove: () => void;
  onDetailClick?: (stock: StockInfo, slotTimeframe?: string) => void;
  onChartFullscreen?: () => void;
  isChartFullscreen?: boolean;
  isOver?: boolean;
  isDragging?: boolean;
  /** 전체화면 모드에서 더 큰 높이 사용 (기본 CHART_HEIGHT 대신) */
  fullscreenHeight?: number;
  /** 슬롯별 시간봉 변경 콜백 */
  onTimeframeChange?: (slotId: string, tf: string) => void;
  /** 슬롯별 보조지표 설정 (전체화면 전환 시 유지) */
  indicatorSettings?: Record<string, boolean>;
  /** 보조지표 설정 변경 콜백 */
  onIndicatorSettingsChange?: (settings: Record<string, boolean>) => void;
  /** 슬롯별 보조지표 표시 순서 */
  indicatorOrder?: string[];
  /** 보조지표 순서 변경 콜백 */
  onIndicatorOrderChange?: (order: string[]) => void;
  /** 슬롯별 캔들 영역 높이 비율 (캔들·보조지표 분할 대비, 예: 0.6 = 캔들 60%) */
  candleHeightRatio?: number;
  /** 캔들 높이 비율 변경 콜백 */
  onCandleHeightRatioChange?: (ratio: number) => void;
  /** true면 캔들/보조지표 높이를 candleHeightRatio로 나눔(알림목록). false면 멀티차트처럼 영역을 자동으로 꽉 채움 */
  preferCandleHeightRatio?: boolean;
  /** 드래그·비율의 하한 (예: 0.6 = 캔들이 분할 대비 최소 60%). 미지정 시 픽셀 최소 높이만 반영 */
  minCandleHeightRatio?: number;
  /** 이동평균선 설정 */
  maLines?: Array<{ id: string; enabled: boolean; period: number; color: string; width: number; type: 'SMA' | 'EMA' }>;
  /** 일목균형표 설정 */
  ichimokuSettings?: IchimokuSettings;
  /** 볼린저 등 오버레이 설정 (context 대신 사용) */
  overlaySettingsOverride?: OverlaySettingsOverride;
  /** 알림종목 모드에서 최신 알림 종목 여부 (타이틀바 파란 그라데이션 표시) */
  isLatestAlert?: boolean;
  /** 선택된 차트 여부 (분홍색 아웃라인 표시) */
  isSelected?: boolean;
  /** true면 시간봉 드롭다운 대신 고정 라벨만 표시 (알림목록 등) */
  lockTimeframe?: boolean;
  /** true면 타이틀바를 드래그해 다른 차트 칸과 위치 교환 (DndContext 상위 필요) */
  enableTitleBarReorder?: boolean;
  /** 알림 팝업 → 알림목록 진입 시, 알림 발생 봉에 매수/매도 마커 */
  navAlertMarker?: { signal: 'BUY' | 'SELL'; timeIso: string } | null;
}

/** @dnd-kit 드래그 id — 멀티차트·알림목록 handleDragEnd와 접두사 일치 */
export const chartTitleDragId = (slotId: string) => `chart-title-${slotId}`;

const CHART_HEIGHT = 180;

/** 화면 표시 캔들 수 (투자분석과 동일) */
const CHART_DISPLAY_COUNT = 200;
/** 보조지표/이동평균/일목 워밍업 캔들 수 (MA200, EMA200, 일목 52기 등 대응) */
const INDICATOR_WARMUP_COUNT = 200;
/** 이 값 미만이면 데이터 부족으로 판단하고 백그라운드 보충 요청 */
const MIN_ACCEPTABLE_CANDLES = 80;

/** timestamp(ms) → CandlestickChart time 형식 (yyyy-MM-ddTHH:mm:ss) */
const timestampToTime = (ts: number): string => {
  return new Date(ts).toISOString().slice(0, 19).replace('Z', '');
};

export const ChartItem: React.FC<ChartItemProps> = ({
  id,
  stock,
  gridIndex,
  timeframe,
  refreshTrigger = 0,
  onAddClick,
  onRemove,
  onDetailClick,
  onChartFullscreen,
  isChartFullscreen = false,
  isOver = false,
  isDragging = false,
  fullscreenHeight,
  onTimeframeChange,
  indicatorSettings: indicatorSettingsProp,
  onIndicatorSettingsChange,
  indicatorOrder,
  onIndicatorOrderChange,
  candleHeightRatio = 0.6,
  onCandleHeightRatioChange,
  preferCandleHeightRatio = false,
  minCandleHeightRatio,
  maLines,
  ichimokuSettings,
  overlaySettingsOverride,
  isLatestAlert = false,
  isSelected = false,
  lockTimeframe = false,
  enableTitleBarReorder = false,
  navAlertMarker = null,
}) => {
  const [chartData, setChartData] = useState<CandleData[]>([]);
  const [maCalculationData, setMaCalculationData] = useState<CandleData[]>([]);
  const [allIndicatorData, setAllIndicatorData] = useState<IndicatorData[]>([]);
  const [loading, setLoading] = useState(false);
  const [loadError, setLoadError] = useState<string | null>(null);
  const [retryKey, setRetryKey] = useState(0);
  const [chartContainerHeight, setChartContainerHeight] = useState(250);
  const [indicatorMenuAnchor, setIndicatorMenuAnchor] = useState<null | HTMLElement>(null);
  const defaultIndicatorSettings = Object.fromEntries(INDICATOR_MENU_ITEMS.map((i) => [i.key, false]));
  const [localIndicatorSettings, setLocalIndicatorSettings] = useState<Record<string, boolean>>(defaultIndicatorSettings);
  const indicatorSettings = indicatorSettingsProp ?? localIndicatorSettings;
  const [dataReceivedBlink, setDataReceivedBlink] = useState(false); // ✅ 데이터 수신 깜빡임
  const setIndicatorSettings = useCallback(
    (updater: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => {
      const next = typeof updater === 'function' ? updater(indicatorSettings) : updater;
      if (onIndicatorSettingsChange) {
        onIndicatorSettingsChange(next);
      } else {
        setLocalIndicatorSettings(next);
      }
    },
    [indicatorSettings, onIndicatorSettingsChange]
  );
  const [indicatorData, setIndicatorData] = useState<IndicatorData[]>([]);
  const [indicatorLoading, setIndicatorLoading] = useState(false);
  const hasLoadedIndicatorsRef = useRef(false); // 초기 로딩 vs WebSocket 갱신 구분용
  const backgroundRefetchInProgressRef = useRef(false); // 데이터 부족 시 백그라운드 보충 중복 방지
  const lastIndicatorConfigRef = useRef<{ stock: string; timeframe: string } | null>(null); // stock/timeframe 변경 시 로딩 재표시
  const chartRef = useRef<ChartHandle>(null);

  const navAlertMarkers = useMemo((): AlertMarker[] => {
    if (!navAlertMarker || chartData.length === 0) return [];
    const sec = pickMarkerUnixSecondsForAlert(chartData, navAlertMarker.timeIso);
    if (sec == null) return [];
    const isBuy = navAlertMarker.signal === 'BUY';
    return [
      {
        time: sec as Time,
        position: isBuy ? 'belowBar' : 'aboveBar',
        color: isBuy ? '#26a69a' : '#ef5350',
        shape: isBuy ? 'arrowUp' : 'arrowDown',
        text: isBuy ? '매수' : '매도',
      },
    ];
  }, [navAlertMarker, chartData]);
  const chartDataRef = useRef<CandleData[]>([]);
  const chartContainerRef = useRef<HTMLDivElement | null>(null);
  const chartFillRef = useRef<HTMLDivElement | null>(null);
  const [chartFillHeight, setChartFillHeight] = useState<number | null>(null);

  const { setNodeRef, isOver: droppableIsOver } = useDroppable({ id });
  const {
    attributes: titleDragAttributes,
    listeners: titleDragListeners,
    setNodeRef: setTitleDragRef,
    isDragging: isTitleBarDragging,
  } = useDraggable({
    id: chartTitleDragId(id),
    disabled: !enableTitleBarReorder || !stock,
    data: { type: 'chart-reorder', slotId: id, gridIndex, stock },
  });
  const { indicatorSettings: investmentAnalysisIndicatorSettings, getLatestSettings, settingsVersion } = useIndicatorSettings();
  const { settings: indicatorVisibilitySettings } = useIndicatorVisibility();
  const { colors: investmentAnalysisColorSettings, colorVersion } = useChartColors();

  const hasAnyIndicator = Object.values(indicatorSettings).some(Boolean);

  // 차트 컨테이너 높이 측정 → 초기 로딩 시 오버플로우 방지 (마운트 직후 + ResizeObserver)
  const setChartContainerRef = useCallback((el: HTMLDivElement | null) => {
    (chartContainerRef as React.MutableRefObject<HTMLDivElement | null>).current = el;
    if (el?.clientHeight && el.clientHeight > 0) {
      setChartContainerHeight(el.clientHeight);
    }
  }, []);
  useEffect(() => {
    const el = chartContainerRef.current;
    if (!el) return;
    const ro = new ResizeObserver((entries) => {
      for (const e of entries) {
        const h = e.contentRect.height;
        if (h > 0) setChartContainerHeight(h);
      }
    });
    ro.observe(el);
    if (el.clientHeight > 0) setChartContainerHeight(el.clientHeight);
    return () => ro.disconnect();
  }, [hasAnyIndicator]);

  const setChartFillRef = useCallback((el: HTMLDivElement | null) => {
    chartFillRef.current = el;
  }, []);

  // 보조지표 없을 때: 채울 영역을 측정하여 차트가 100% fit되도록
  // ✅ chartData.length 의존성: 로딩→차트 표시 전환 시에도 effect 재실행하여 ResizeObserver 설정
  useEffect(() => {
    if (hasAnyIndicator) {
      setChartFillHeight(null);
      return;
    }
    const el = chartFillRef.current;
    if (!el) return;
    const ro = new ResizeObserver((entries) => {
      for (const e of entries) {
        const h = e.contentRect.height;
        if (h > 0) setChartFillHeight(h);
      }
    });
    ro.observe(el);
    if (el.clientHeight > 0) setChartFillHeight(el.clientHeight);
    return () => {
      ro.disconnect();
      setChartFillHeight(null);
    };
  }, [hasAnyIndicator, stock?.symbol, chartData.length]);

  // ✅ MultiChartMarketDataContext 사용 (subscribe_multi로 단일 연결)
  const multiChartCtx = useMultiChartMarketData();
  const [wsCandles, setWsCandles] = useState<Array<{ timestamp: number; open: number; high: number; low: number; close: number; volume?: number }>>([]);
  const wsCandlesRef = useRef(wsCandles);
  useEffect(() => {
    wsCandlesRef.current = wsCandles;
  }, [wsCandles]);

  // ✅ 보조지표 데이터 ref (실시간 업데이트용)
  const indicatorDataRef = useRef<IndicatorData[]>([]);

  useEffect(() => {
    chartDataRef.current = chartData;
  }, [chartData]);

  // stock/timeframe 변경 시 이전 차트 데이터 초기화 (잘못된 종목 데이터 표시 방지)
  useEffect(() => {
    if (!stock) {
      setChartData([]);
      setMaCalculationData([]);
      setWsCandles([]);
      setLoadError(null);
      backgroundRefetchInProgressRef.current = false;
      return;
    }
    setChartData([]);
    setMaCalculationData([]);
    setWsCandles([]);
    setLoadError(null);
    backgroundRefetchInProgressRef.current = false;
  }, [stock?.symbol, timeframe]);

  // wsCandles → CandleData 변환 (WebSocket 스냅샷 초기 반영용)
  const wsToCandleData = useCallback((raw: Array<{ timestamp: number; open: number; high: number; low: number; close: number; volume?: number }>): CandleData[] => {
    return raw
      .filter((c) => c.open !== undefined && c.close !== undefined)
      .map((c) => ({
        time: timestampToTime(c.timestamp),
        open: c.open,
        high: c.high,
        low: c.low,
        close: c.close,
        volume: c.volume ?? 0,
      }))
      .sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
  }, []);

  // ✅ Context 구독: 실시간 데이터 수신 시 즉시 차트 갱신
  // - 스냅샷(isUpdate:false): chartData 비어 있을 때 WebSocket 데이터로 초기 표시
  // - 업데이트(isUpdate:true) + 새 봉(isNewCandle:true): 전체 차트 리로드 (롤링)
  // - 업데이트(isUpdate:true) + 동일 봉(isNewCandle:false): updateLastCandle로 마지막 캔들만 갱신 (롤링 X)
  useEffect(() => {
    if (!stock || !multiChartCtx) return;
    const unsub = multiChartCtx.subscribe(stock.symbol, timeframe, (data) => {
      if (data.candles && data.candles.length > 0) {
        // ✅ 데이터 수신 시 타이틀바 깜빡임 트리거
        if (data.isUpdate) {
          setDataReceivedBlink(true);
          setTimeout(() => setDataReceivedBlink(false), 300); // 300ms 후 자동 꺼짐
        }
        
        setWsCandles(data.candles);
        const converted = wsToCandleData(data.candles);
        if (converted.length === 0) return;
        
        // /pattern/candles 히스토리보다 먼저 WebSocket(단 1~수 봉)이 오면 1봉만 캔들처럼 보이는 부작용 방지
        if (chartDataRef.current.length === 0 && data.isUpdate) {
          if (data.isNewCandle) {
            setRetryKey((k) => k + 1);
          }
          return;
        }

        // 스냅샷 도착 시 chartData가 비어 있으면 WebSocket 데이터로 초기화(실시간 REST 200봉 등)
        if (!data.isUpdate && chartDataRef.current.length === 0) {
          if (converted.length < MIN_ACCEPTABLE_CANDLES) {
            return;
          }
          setChartData(converted);
          setMaCalculationData(converted);
          setLoading(false);
          console.log(`[ChartItem ${stock.symbol} ${timeframe}] 초기 데이터 설정: ${converted.length}개 캔들`);
          return;
        }
        
        // ✅ 실시간 업데이트 처리
        if (data.isUpdate && chartRef.current) {
          const latest = data.candles[data.candles.length - 1];
          
          // ✅ 새 봉 생성 감지 → 전체 차트 리로드 (롤링)
          if (data.isNewCandle) {
            console.log(`🔄 [ChartItem ${stock.symbol} ${timeframe}] 새 봉 생성 감지 - 전체 차트 리로드`);
            // 기존 데이터 초기화하고 재로드 트리거
            setRetryKey(k => k + 1);
            return;
          }
          
          // ✅ 동일 봉 업데이트 → 마지막 캔들만 갱신 (롤링 X)
          chartRef.current.updateLastCandle({ ...latest, volume: latest.volume ?? 0 });
          const lastIdx = chartDataRef.current.length - 1;
          if (lastIdx >= 0 && chartDataRef.current[lastIdx]) {
            const updatedLast = {
              ...chartDataRef.current[lastIdx],
              open: latest.open,
              high: latest.high,
              low: latest.low,
              close: latest.close,
              volume: latest.volume ?? 0,
            };
            chartDataRef.current[lastIdx] = updatedLast;
            setChartData((prev) => {
              if (prev.length === 0) return prev;
              const next = [...prev];
              next[lastIdx] = updatedLast;
              return next;
            });
            setMaCalculationData((prev) => {
              if (prev.length === 0) return prev;
              const next = [...prev];
              next[lastIdx] = updatedLast;
              return next;
            });
            
            // ✅ 보조지표 실시간 갱신 (투자분석화면과 동일)
            if (indicatorDataRef.current.length > 0) {
              chartRef.current.updateLastIndicators(
                updatedLast,
                chartDataRef.current,
                indicatorDataRef.current
              );
            }
          }
        }
      }
    });
    return unsub;
  }, [stock?.symbol, timeframe, multiChartCtx, wsToCandleData]);

  // ✅ 폴링 백업: Context 미수신 시 5초마다 최신 캔들로 차트 갱신
  const REALTIME_UPDATE_INTERVAL = 5000;
  useEffect(() => {
    if (!stock || chartData.length === 0) return;
    const intervalId = setInterval(() => {
      const candles = wsCandlesRef.current;
      if (candles.length === 0) return;
      const latest = candles[candles.length - 1];
      if (chartRef.current) {
        chartRef.current.updateLastCandle({ ...latest, volume: latest.volume ?? 0 });
      }
      const lastIdx = chartDataRef.current.length - 1;
      if (lastIdx >= 0 && chartDataRef.current[lastIdx]) {
        const updatedLast = {
          ...chartDataRef.current[lastIdx],
          open: latest.open,
          high: latest.high,
          low: latest.low,
          close: latest.close,
          volume: latest.volume ?? 0,
        };
        chartDataRef.current[lastIdx] = updatedLast;
        setChartData((prev) => {
          if (prev.length === 0) return prev;
          const next = [...prev];
          next[lastIdx] = updatedLast;
          return next;
        });
        setMaCalculationData((prev) => {
          if (prev.length === 0) return prev;
          const next = [...prev];
          next[lastIdx] = updatedLast;
          return next;
        });
        
        // ✅ 보조지표 실시간 갱신 (폴링 백업)
        if (chartRef.current && indicatorDataRef.current.length > 0) {
          chartRef.current.updateLastIndicators(
            updatedLast,
            chartDataRef.current,
            indicatorDataRef.current
          );
        }
      }
    }, REALTIME_UPDATE_INTERVAL);
    return () => clearInterval(intervalId);
  }, [stock?.symbol, chartData.length]);

  const status = multiChartCtx?.status ?? 'disconnected';

  // 워밍업 필요 여부: 이동평균/일목/보조지표 사용 시 과거 데이터 필요
  const needsWarmup = Boolean(
    (maLines?.some((m) => m.enabled)) ||
    (ichimokuSettings && (ichimokuSettings.tenkan || ichimokuSettings.kijun || ichimokuSettings.chikou || ichimokuSettings.senkouA || ichimokuSettings.senkouB)) ||
    hasAnyIndicator
  );

  useEffect(() => {
    if (!stock) return;
    let cancelled = false;
    const parseCandles = (raw: any[]): CandleData[] => {
      const toTimeStr = (c: any): string => {
        if (c.time && typeof c.time === 'string') return c.time;
        const ts = c.time ?? c.timestamp;
        if (ts == null) return '';
        const ms = typeof ts === 'number' ? (ts >= 1e12 ? ts : ts * 1000) : new Date(ts).getTime();
        return isFinite(ms) ? new Date(ms).toISOString().slice(0, 19).replace('Z', '') : '';
      };
      const sorted = [...raw].sort((a: any, b: any) => new Date(a.time || a.timestamp).getTime() - new Date(b.time || b.timestamp).getTime());
      return sorted.map((c: any) => ({
        time: toTimeStr(c),
        open: c.open,
        high: c.high,
        low: c.low,
        close: c.close,
        volume: c.volume ?? 0,
        isBuyPoint: c.isBuyPoint,
        isSellPoint: c.isSellPoint,
      })).filter((c) => c.time);
    };
    const loadCandles = async (retryCount = 0): Promise<void> => {
      // ✅ 이미 차트 데이터가 있으면 로딩 스피너로 차트 숨기지 않음 (차트 유지)
      // - 선행스팬/일목 워밍업 리패치
      // - maLines/ichimokuSettings/hasAnyIndicator 등 의존성 변경으로 인한 재실행 (보조지표 차트 깜빡임 방지)
      const hasExistingData = chartDataRef.current.length > 0;
      const isBackgroundRefetch = hasExistingData;
      if (!isBackgroundRefetch) {
        setLoading(true);
        setLoadError(null);
      }
      // ✅ maCalculationData/allIndicatorData를 즉시 비우지 않음 - 비우면 CandlestickChart effect가
      //    차트를 제거한 뒤 data가 아직 갱신되지 않아 early return되어 차트가 사라지는 현상 방지
      let mainCandles: CandleData[] = [];
      try {
        const res = await axios.get(`${API_BASE_URL}/pattern/candles`, {
          params: { market: stock.symbol, timeframe, count: CHART_DISPLAY_COUNT },
        });
        if (cancelled) return;
        const raw = res.data?.data || [];
        mainCandles = raw.length > 0 ? parseCandles(raw) : [];
        if (raw.length === 0 && retryCount < 2) {
          await new Promise((r) => setTimeout(r, 800 + retryCount * 500));
          if (!cancelled) return loadCandles(retryCount + 1);
        }
      } catch {
        // 캔들: 백엔드 /pattern/candles → Upbit 공개 REST (별도 market-data 폴백 없음)
      }
      if (cancelled) return;
      try {
        if (cancelled) return;
        if (mainCandles.length === 0) {
          setChartData([]);
          setMaCalculationData([]);
          setLoadError('캔들 데이터를 업비트에서 불러오지 못했습니다.');
          return;
        }
        setChartData(mainCandles);
        setLoadError(null);
        if (!needsWarmup) {
          setMaCalculationData(mainCandles);
          return;
        }
        let warmupCandles: CandleData[] = [];
        const firstCandleTime = mainCandles[0].time;
        try {
          const warmupRes = await axios.get(`${API_BASE_URL}/pattern/candles`, {
            params: { market: stock.symbol, timeframe, count: INDICATOR_WARMUP_COUNT, to: firstCandleTime },
          });
          if (cancelled) return;
          const warmupRaw = warmupRes.data?.data || [];
          if (warmupRaw.length > 0) {
            warmupCandles = parseCandles(warmupRaw);
          }
        } catch {
          // 워밍업 실패 시 메인만 사용
        }
        if (cancelled) return;
        const enabledMAPeriods = maLines?.filter((m) => m.enabled).map((m) => m.period) ?? [];
        const maxMAPeriod = enabledMAPeriods.length > 0 ? Math.max(...enabledMAPeriods) : 0;
        const ichimokuEnabled = ichimokuSettings && (ichimokuSettings.senkouA || ichimokuSettings.senkouB || ichimokuSettings.kijun || ichimokuSettings.tenkan || ichimokuSettings.chikou);
        const ichimokuMaxPeriod = ichimokuEnabled ? (ichimokuSettings?.senkouBPeriod ?? 52) : 0;
        const MAX_INDICATOR_PERIOD = Math.max(maxMAPeriod, ichimokuMaxPeriod);
        const REQUIRED_TOTAL = MAX_INDICATOR_PERIOD > 0
          ? CHART_DISPLAY_COUNT + MAX_INDICATOR_PERIOD
          : CHART_DISPLAY_COUNT + INDICATOR_WARMUP_COUNT;
        let additionalCandles: CandleData[] = [];
        const currentTotal = warmupCandles.length + mainCandles.length;
        if (currentTotal < REQUIRED_TOTAL) {
          const remainingNeeded = REQUIRED_TOTAL - currentTotal;
          let lastTime = warmupCandles.length > 0 ? warmupCandles[0].time : mainCandles[0].time;
          let totalAdditionalLoaded = 0;
          for (let i = 0; i < 5 && totalAdditionalLoaded < remainingNeeded; i++) {
            const requestCount = Math.min(200, remainingNeeded - totalAdditionalLoaded);
            try {
              const addRes = await axios.get(`${API_BASE_URL}/pattern/candles`, {
                params: { market: stock.symbol, timeframe, count: requestCount, to: lastTime },
              });
              if (cancelled) return;
              const addRaw = addRes.data?.data || [];
              if (addRaw.length === 0) break;
              const newCandles = parseCandles(addRaw);
              additionalCandles = [...newCandles, ...additionalCandles];
              totalAdditionalLoaded += newCandles.length;
              lastTime = newCandles[0].time;
            } catch {
              break;
            }
          }
        }
        if (cancelled) return;
        const allCandles = [...additionalCandles, ...warmupCandles, ...mainCandles];
        setMaCalculationData(allCandles);
      } catch (e) {
        if (!cancelled) {
          if (retryCount < 2) {
            await new Promise((r) => setTimeout(r, 1000 + retryCount * 500));
            if (!cancelled) return loadCandles(retryCount + 1);
          } else {
            console.error('[ChartItem] pattern/candles error', e);
            if (!isBackgroundRefetch) {
              setChartData([]);
              setMaCalculationData([]);
              setLoadError('데이터 로드에 실패했습니다. 새로고침해 주세요.');
            }
            // isBackgroundRefetch 시 WebSocket 등으로 이미 표시 중인 데이터 유지 (백그라운드 보충 effect에서 재시도)
          }
        }
      } finally {
        if (!cancelled && !isBackgroundRefetch) setLoading(false);
      }
    };
    const delayMs = gridIndex * 150;
    const timer = setTimeout(() => loadCandles(), delayMs);
    return () => {
      cancelled = true;
      clearTimeout(timer);
    };
  }, [stock?.symbol, timeframe, refreshTrigger, retryKey, gridIndex, needsWarmup, maLines, ichimokuSettings, hasAnyIndicator]);

  // ✅ 데이터 부족 시 백그라운드 보충: WebSocket 등으로 적은 캔들만 있을 때 REST로 완전한 차트 요청
  useEffect(() => {
    if (!stock || chartData.length === 0 || chartData.length >= MIN_ACCEPTABLE_CANDLES || loadError) return;
    if (backgroundRefetchInProgressRef.current) return;

    let cancelled = false;
    backgroundRefetchInProgressRef.current = true;

    const run = async () => {
      const parseCandles = (raw: any[]): CandleData[] => {
        const toTimeStr = (c: any): string => {
          if (c.time && typeof c.time === 'string') return c.time;
          const ts = c.time ?? c.timestamp;
          if (ts == null) return '';
          const ms = typeof ts === 'number' ? (ts >= 1e12 ? ts : ts * 1000) : new Date(ts).getTime();
          return isFinite(ms) ? new Date(ms).toISOString().slice(0, 19).replace('Z', '') : '';
        };
        return [...raw]
          .sort((a: any, b: any) => new Date(a.time || a.timestamp).getTime() - new Date(b.time || b.timestamp).getTime())
          .map((c: any) => ({
            time: toTimeStr(c),
            open: c.open,
            high: c.high,
            low: c.low,
            close: c.close,
            volume: c.volume ?? 0,
            isBuyPoint: c.isBuyPoint,
            isSellPoint: c.isSellPoint,
          }))
          .filter((c) => c.time);
      };

      let mainCandles: CandleData[] = [];
      try {
        const res = await axios.get(`${API_BASE_URL}/pattern/candles`, {
          params: { market: stock.symbol, timeframe, count: CHART_DISPLAY_COUNT },
        });
        if (!cancelled) {
          const raw = res.data?.data || [];
          mainCandles = raw.length > 0 ? parseCandles(raw) : [];
        }
      } catch {
        // /pattern/candles(Upbit REST) 재시도는 다음 주기에서
      }

      if (!cancelled && mainCandles.length >= MIN_ACCEPTABLE_CANDLES) {
        setChartData(mainCandles);
        setMaCalculationData(mainCandles);
        setLoadError(null);
      }
    };

    run().finally(() => {
      backgroundRefetchInProgressRef.current = false;
    });

    return () => {
      cancelled = true;
    };
  }, [stock?.symbol, timeframe, chartData.length, loadError]);

  // 보조지표 데이터 로드 (선택 시) - 워밍업된 maCalculationData 사용 시 전체 구간 정확 계산
  useEffect(() => {
    const candleDataForApi = maCalculationData.length >= chartData.length ? maCalculationData : chartData;
    // ✅ 보조지표 해제/종목 제거 시에만 clear - candleData 부족 시 clear하지 않음 (로딩 직후 effect 재실행 시 차트 사라짐 방지)
    if (!hasAnyIndicator || !stock) {
      hasLoadedIndicatorsRef.current = false;
      setIndicatorData([]);
      setAllIndicatorData([]);
      return;
    }
    if (candleDataForApi.length < 2) {
      return; // 캔들 데이터 대기 - 기존 indicatorData 유지 (한번 그렸다가 바로 사라지는 현상 방지)
    }
    // ✅ stock/timeframe 변경 시 초기 로딩으로 간주 (해당 종목·봉의 첫 로딩이므로 프로그레스 표시)
    const configKey = { stock: stock.symbol, timeframe };
    if (lastIndicatorConfigRef.current && (lastIndicatorConfigRef.current.stock !== configKey.stock || lastIndicatorConfigRef.current.timeframe !== configKey.timeframe)) {
      hasLoadedIndicatorsRef.current = false;
    }
    lastIndicatorConfigRef.current = configKey;
    let cancelled = false;
    // ✅ 초기 로딩 시에만 프로그레스 표시 (WebSocket 실시간 업데이트로 인한 재요청 시에는 로딩 없이 조용히 갱신)
    const isInitialLoad = !hasLoadedIndicatorsRef.current;
    const load = async () => {
      if (isInitialLoad) setIndicatorLoading(true);
      try {
        const latestSettings = getLatestSettings();
        const maPeriods = maLines?.filter((m) => m.enabled).map((m) => m.period) ?? [];
        const payload = buildUpbitAnalyzePayload({
          symbol: stock.symbol,
          interval: timeframe,
          candleData: candleDataForApi,
          maPeriods,
          bollingerPeriod: indicatorVisibilitySettings.bollingerPeriod,
          bollingerStdDev: indicatorVisibilitySettings.bollingerStdDev,
          settings: latestSettings,
          vrPeriod: latestSettings.vrPeriod || 10,
          vr2Enabled: latestSettings.vr2Enabled || false,
          vr2Period: latestSettings.vr2Period || 10,
        });
        const res = await axios.post(`${API_BASE_URL}/upbit/analyze`, payload);
        if (cancelled) return;
        const raw = Array.isArray(res.data) ? res.data : res.data?.data ?? [];
        const mappedRaw: IndicatorData[] = raw.map((d: any) => normalizeAnalyzeIndicatorRow(d) as IndicatorData);
        const mapped = alignIndicatorRowsToCandleTimes(mappedRaw, candleDataForApi);
        setAllIndicatorData(mapped);
        hasLoadedIndicatorsRef.current = true;
        console.log(`✅ [ChartItem] 보조지표 API 성공: ${mapped.length}개, cci13 샘플=${mapped.filter((d: any) => d.cci13 != null).length}, adx 샘플=${mapped.filter((d: any) => d.adx != null).length}`);
        const skipCount = candleDataForApi.length - chartData.length;
        const displayIndicators = skipCount > 0 ? mapped.slice(skipCount) : mapped;
        setIndicatorData(displayIndicators);
        
        // ✅ 보조지표 데이터 ref에 저장 (실시간 업데이트용)
        indicatorDataRef.current = displayIndicators;
      } catch (e) {
        if (!cancelled) {
          console.error('[ChartItem] indicator load error', e);
          setIndicatorData([]);
        }
      } finally {
        if (!cancelled && isInitialLoad) setIndicatorLoading(false);
      }
    };
    load();
    return () => { cancelled = true; };
    // ✅ chartData/maCalculationData 전체 참조 대신 length만 의존: WebSocket 마지막캔들만 갱신 시 effect 재실행 방지 (로드 취소·재시도 반복으로 보조지표 영원히 미표시되는 현상 방지)
  }, [
    hasAnyIndicator,
    stock?.symbol,
    timeframe,
    chartData.length,
    maCalculationData.length,
    maLines,
    getLatestSettings,
    indicatorVisibilitySettings.bollingerPeriod,
    indicatorVisibilitySettings.bollingerStdDev,
    settingsVersion,
  ]);

  // ✅ 투자분석과 동일: data는 항상 표시용 chartData만 사용
  // - maCalculationData는 maCalculationData prop으로 전달해 일목/MA 계산용으로만 사용
  // - data=maCalculationData로 바꾸면 메인(uniqueData)과 서브(allIndicatorData) 봉 수 불일치 → logical range/시간축 비정렬 → 보조지표 차트 사라짐
  const currentPrice =
    (chartData.length > 0 ? chartData[chartData.length - 1]?.close : null) ??
    (wsCandles.length > 0 ? wsCandles[wsCandles.length - 1]?.close : null);
  const enabledCount = Object.values(indicatorSettings).filter(Boolean).length;
  const enabledKeys = INDICATOR_MENU_ITEMS.filter((i) => indicatorSettings[i.key]).map((i) => i.key);
  const orderedKeys = (() => {
    if (indicatorOrder && indicatorOrder.length > 0) {
      const ordered = indicatorOrder.filter((k) => enabledKeys.includes(k));
      enabledKeys.forEach((k) => {
        if (!ordered.includes(k)) ordered.push(k);
      });
      return ordered;
    }
    return enabledKeys;
  })();

  const handleSubPanelReorder = useCallback(
    (fromIndex: number, toIndex: number) => {
      if (!onIndicatorOrderChange || orderedKeys.length < 2) return;
      const next = [...orderedKeys];
      const [removed] = next.splice(fromIndex, 1);
      next.splice(toIndex, 0, removed);
      onIndicatorOrderChange(next);
    },
    [orderedKeys, onIndicatorOrderChange]
  );

  const handleIndicatorDelete = useCallback(
    (panelType: string) => {
      setIndicatorSettings((p) => ({ ...p, [panelType]: false }));
    },
    [setIndicatorSettings]
  );

  // 보조지표가 있으면 캔들 영역 높이를 비율로 조절 (리사이즈 핸들로 변경 가능)
  const RESIZE_HANDLE_HEIGHT = 8;
  const TREND_BAR_HEIGHT = 0; // 추세바 비표시 (hideTrendAlignmentBar)
  const BOTTOM_TOOLBAR_HEIGHT = 40; // CandlestickChart 보조지표 하단 툴바 (compactToolbar)
  const MIN_CANDLE_HEIGHT = 80; // LightweightCharts 최소 렌더링 높이 (30px 이하 시 차트 숨김)
  // ✅ 전체화면+보조지표 있을 때: 전체 영역 활용 (하단 여백 제거). 보조지표 없을 때만 fullscreenHeight로 비율 유지
  const baseHeight = (isChartFullscreen && fullscreenHeight != null && !hasAnyIndicator)
    ? fullscreenHeight
    : chartContainerHeight;
  // ✅ 멀티차트: 보조지표 하단 툴바 제거 시 공간 예약 불필요
  const hideSubPanelToolbar = true;
  const toolbarHeight = (isChartFullscreen && hasAnyIndicator && !hideSubPanelToolbar) ? BOTTOM_TOOLBAR_HEIGHT : 0;
  const availableForSplit = Math.max(0, baseHeight - RESIZE_HANDLE_HEIGHT - TREND_BAR_HEIGHT - toolbarHeight);
  const n = hasAnyIndicator ? Math.max(1, orderedKeys.length) : 0;
  // ✅ 전체화면: 보조지표 최대 140px로 확대하여 영역 꽉 채움. 일반 모드: 최대 55px
  const subpanelMax = isChartFullscreen && hasAnyIndicator ? 140 : 55;

  let SUBPANEL_HEIGHT = 0;
  let maxCandleHeight = MIN_CANDLE_HEIGHT;
  let candleHeight: number;

  if (hasAnyIndicator && preferCandleHeightRatio) {
    // 캔들 + 보조지표 합 = availableForSplit (= baseHeight - 리사이즈 핸들 등 1회 차감)
    // splitBudget을 다시 8px 빼면 드래그 비율 분모가 어긋남. usedSub로 ch를 덮어쓰면 subpanelMax(55) 때문에 비율이 무시됨.
    const splitBudget = Math.max(1, availableForSplit);
    const minRatioFromPixels = Math.min(0.9, MIN_CANDLE_HEIGHT / splitBudget);
    const minR = minCandleHeightRatio != null
      ? Math.max(minCandleHeightRatio, minRatioFromPixels)
      : minRatioFromPixels;
    const r = Math.max(minR, Math.min(0.9, candleHeightRatio));
    const chHi = Math.max(MIN_CANDLE_HEIGHT, splitBudget - n * 25);
    const chLo = Math.min(chHi, Math.max(MIN_CANDLE_HEIGHT, Math.round(minR * splitBudget)));
    let ch = Math.round(r * splitBudget);
    ch = Math.max(chLo, Math.min(ch, chHi));
    const indBudget = Math.max(0, splitBudget - ch);
    const sphRaw = n > 0 ? indBudget / n : 0;
    SUBPANEL_HEIGHT = n > 0 ? Math.min(subpanelMax, Math.max(25, Math.floor(sphRaw))) : 0;
    if (n > 0 && SUBPANEL_HEIGHT > indBudget / n + 1e-6) {
      SUBPANEL_HEIGHT = Math.max(25, Math.floor(indBudget / n));
    }
    maxCandleHeight = chHi;
    candleHeight = ch;
  } else if (hasAnyIndicator) {
    const rawSubpanel = n > 0 ? Math.floor(Math.max(0, availableForSplit - MIN_CANDLE_HEIGHT) / n) : 0;
    SUBPANEL_HEIGHT = Math.min(subpanelMax, Math.max(25, rawSubpanel));
    let totalSubpanelHeight = n * SUBPANEL_HEIGHT;
    if (totalSubpanelHeight + MIN_CANDLE_HEIGHT > availableForSplit) {
      SUBPANEL_HEIGHT = rawSubpanel;
      totalSubpanelHeight = n * SUBPANEL_HEIGHT;
    }
    maxCandleHeight = Math.max(MIN_CANDLE_HEIGHT, availableForSplit - totalSubpanelHeight);
    candleHeight = maxCandleHeight;
  } else {
    candleHeight =
      fullscreenHeight ?? chartFillHeight ?? chartContainerHeight ?? Math.max(MIN_CANDLE_HEIGHT, baseHeight);
  }

  const subPanels = orderedKeys.map((k) => ({ type: k as any, height: SUBPANEL_HEIGHT, enabled: true }));
  // ✅ 보조지표 없을 때 위 분기에서 candleHeight 설정됨

  const handleCandleHeightResize = useCallback(
    (newHeight: number) => {
      if (!onCandleHeightRatioChange || availableForSplit <= 0) return;
      const splitBudget = Math.max(1, availableForSplit);
      const minRatioFromPixels = Math.min(0.9, MIN_CANDLE_HEIGHT / splitBudget);
      const minR = minCandleHeightRatio != null
        ? Math.max(minCandleHeightRatio, minRatioFromPixels)
        : minRatioFromPixels;
      const newRatio = Math.max(minR, Math.min(0.9, newHeight / splitBudget));
      onCandleHeightRatioChange(newRatio);
    },
    [availableForSplit, onCandleHeightRatioChange, minCandleHeightRatio]
  );

  const showDropZone = (isOver || droppableIsOver) && !stock;

  if (!stock) {
    return (
      <Box
        ref={setNodeRef}
        sx={{
          height: '100%',
          minHeight: 180,
          border: '2px dashed',
          borderColor: showDropZone ? 'primary.main' : 'divider',
          borderRadius: 1,
          bgcolor: showDropZone ? 'action.hover' : 'background.paper',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          transition: 'all 0.2s',
        }}
      >
        <Box sx={{ textAlign: 'center' }}>
          <IconButton color="primary" size="large" onClick={onAddClick} sx={{ mb: 0.5 }}>
            <AddIcon fontSize="large" />
          </IconButton>
          <Typography variant="body2" color="text.secondary">
            + 종목 추가
          </Typography>
          <Typography variant="caption" color="text.disabled" display="block">
            클릭 또는 드래그하여 추가
          </Typography>
        </Box>
      </Box>
    );
  }

  return (
    <Box
      ref={setNodeRef}
      sx={{
        height: '100%',
        minHeight: 180,
        borderRadius: 1,
        overflow: 'hidden',
        bgcolor: 'background.paper',
        border: '1px solid',
        borderColor: isSelected ? '#e91e63' : 'divider',
        position: 'relative',
        display: 'flex',
        flexDirection: 'column',
        opacity: isTitleBarDragging ? 0.25 : 1,
        transition: 'opacity 0.15s ease',
      }}
      >
      <Box
        sx={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          px: 1,
          py: 0.5,
          ...(isLatestAlert
            ? {
                background: 'linear-gradient(135deg, #1976d2 0%, #42a5f5 50%, #90caf9 100%)',
                color: 'white',
                borderBottom: '1px solid rgba(255,255,255,0.3)',
                '& .MuiIconButton-root': { color: 'rgba(255,255,255,0.95)' },
                '& .MuiButton-root': { color: 'rgba(255,255,255,0.95)', borderColor: 'rgba(255,255,255,0.6)' },
                '& .MuiSelect-select, .MuiInputLabel-root': { color: 'rgba(255,255,255,0.95) !important' },
                '& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.5)' },
                '& .alert-locked-timeframe': { color: 'rgba(255,255,255,0.95)' },
              }
            : {
                bgcolor: 'action.hover',
                borderBottom: '1px solid',
                borderColor: 'divider',
              }),
          // ✅ 데이터 수신 시 연두색 형광 깜빡임
          ...(dataReceivedBlink && !isLatestAlert ? {
            boxShadow: '0 0 10px 3px rgba(204, 255, 0, 0.7), inset 0 0 8px 2px rgba(204, 255, 0, 0.3)',
            transition: 'box-shadow 0.15s ease-out',
          } : {}),
          flexShrink: 0,
          gap: 0.75,
          flexWrap: 'nowrap',
          overflow: 'hidden',
          minWidth: 0,
        }}
      >
        <Box
          ref={setTitleDragRef}
          {...(enableTitleBarReorder ? titleDragAttributes : {})}
          {...(enableTitleBarReorder ? titleDragListeners : {})}
          sx={{
            display: 'flex',
            alignItems: 'center',
            minWidth: 0,
            flex: 1,
            gap: 0.25,
            overflow: 'hidden',
            ...(enableTitleBarReorder ? { cursor: 'grab', touchAction: 'none', '&:active': { cursor: 'grabbing' } } : {}),
          }}
        >
          {enableTitleBarReorder && (
            <Tooltip title="드래그하여 차트 위치 이동" arrow>
              <DragIndicatorIcon
                fontSize="small"
                sx={{
                  flexShrink: 0,
                  opacity: 0.65,
                  color: isLatestAlert ? 'rgba(255,255,255,0.9)' : 'text.secondary',
                }}
              />
            </Tooltip>
          )}
          <Typography variant="caption" fontWeight="bold" noWrap sx={{ minWidth: 0, flexShrink: 1 }}>
            {stock.koreanName}
            <Typography
              component="span"
              variant="caption"
              sx={{
                fontWeight: 'bold',
                ml: 0.5,
                color: isLatestAlert ? 'rgba(255,255,255,0.95)' : '#81c784',
              }}
            >
              {currentPrice != null ? ` : ${currentPrice.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })}` : ''}
            </Typography>
          </Typography>
        </Box>
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexShrink: 0 }}>
          <Tooltip title="보조지표 추가" arrow>
            <Button
              variant="outlined"
              size="small"
              onClick={(e) => setIndicatorMenuAnchor(e.currentTarget)}
              startIcon={<BarChartIcon sx={{ fontSize: 16 }} />}
              endIcon={<KeyboardArrowDown sx={{ fontSize: 16 }} />}
              sx={{ fontSize: '11px', textTransform: 'none', borderColor: 'divider', px: 1, py: 0.25, minHeight: 28 }}
            >
              보조지표({enabledCount})
            </Button>
          </Tooltip>
          <Menu
            anchorEl={indicatorMenuAnchor}
            open={Boolean(indicatorMenuAnchor)}
            onClose={() => setIndicatorMenuAnchor(null)}
            anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
            transformOrigin={{ vertical: 'top', horizontal: 'left' }}
            slotProps={{ paper: { sx: { maxHeight: 400, width: 220 } } }}
          >
            {INDICATOR_MENU_ITEMS.map((item, idx) => (
              <React.Fragment key={item.key}>
                {idx === 1 && <Divider sx={{ my: 0.5 }} />}
                <MenuItem
                  onClick={() => setIndicatorSettings((p) => ({ ...p, [item.key]: !p[item.key] }))}
                  sx={{ py: 0.5 }}
                >
                  <ListItemIcon>
                    <Checkbox edge="start" checked={!!indicatorSettings[item.key]} tabIndex={-1} disableRipple size="small" />
                  </ListItemIcon>
                  <ListItemText primary={item.label} primaryTypographyProps={{ fontSize: '13px' }} />
                </MenuItem>
              </React.Fragment>
            ))}
          </Menu>
          {lockTimeframe ? (
            <Tooltip title="알림 조건의 시간봉입니다. 변경할 수 없습니다." arrow>
              <Typography
                className="alert-locked-timeframe"
                variant="caption"
                sx={{
                  fontSize: '11px',
                  fontWeight: 700,
                  whiteSpace: 'nowrap',
                  px: 0.75,
                  py: 0.25,
                  border: '1px solid',
                  borderColor: isLatestAlert ? 'rgba(255,255,255,0.45)' : 'divider',
                  borderRadius: 1,
                  lineHeight: 1.6,
                }}
              >
                봉 {TIMEFRAME_OPTIONS.find((o) => o.value === timeframe)?.label ?? timeframe}
              </Typography>
            </Tooltip>
          ) : (
            <FormControl size="small" sx={{ minWidth: 70 }}>
              <InputLabel sx={{ fontSize: '11px' }}>봉</InputLabel>
              <Select
                value={timeframe}
                label="봉"
                onChange={(e) => onTimeframeChange?.(id, e.target.value)}
                sx={{ fontSize: '11px', height: 28 }}
              >
                {TIMEFRAME_OPTIONS.map((o) => (
                  <MenuItem key={o.value} value={o.value} sx={{ fontSize: '12px' }}>
                    {o.label}
                  </MenuItem>
                ))}
              </Select>
            </FormControl>
          )}
        </Box>
        <Box sx={{ display: 'flex', alignItems: 'center', gap: 0, flexShrink: 0 }}>
          {status === 'connecting' && (
            <CircularProgress size={14} sx={{ mr: 0.5, verticalAlign: 'middle' }} />
          )}
          {onChartFullscreen && (
            <Tooltip title={isChartFullscreen ? '전체화면 종료' : '전체화면'}>
              <IconButton
                size="small"
                onClick={(e) => { e.stopPropagation(); onChartFullscreen(); }}
                aria-label={isChartFullscreen ? '전체화면 종료' : '전체화면'}
              >
                {isChartFullscreen ? <FullscreenExitIcon fontSize="small" /> : <FullscreenIcon fontSize="small" />}
              </IconButton>
            </Tooltip>
          )}
          {onDetailClick && (
            <Tooltip title="상세보기">
              <IconButton
                size="small"
                onClick={(e) => { e.stopPropagation(); onDetailClick(stock, timeframe); }}
                aria-label="상세보기"
              >
                <VisibilityIcon fontSize="small" />
              </IconButton>
            </Tooltip>
          )}
          <IconButton size="small" onClick={(e) => { e.stopPropagation(); onRemove(); }} aria-label="닫기">
            <CloseIcon fontSize="small" />
          </IconButton>
        </Box>
      </Box>
      <Box
        ref={setChartContainerRef}
        sx={{
          flex: 1,
          minHeight: 0,
          minWidth: 1, // ✅ flex 아이템이 0으로 수축하는 것 방지 (lightweight-charts "Value is null" 원인)
          overflow: 'hidden',
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        {loading ? (
          <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1, minHeight: 0 }}>
            <CircularProgress size={32} />
          </Box>
        ) : loadError && chartData.length === 0 ? (
          <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', flex: 1, minHeight: 0, gap: 1 }}>
            <Typography variant="body2" color="error">
              {loadError}
            </Typography>
            <Button size="small" variant="outlined" onClick={() => { setLoadError(null); setRetryKey((k) => k + 1); }}>
              새로고침
            </Button>
          </Box>
        ) : chartData.length > 0 ? (
          // ✅ 단일 CandlestickChart로 통합 - hasAnyIndicator 전환 시 remount 방지
          // ✅ 보조지표 없을 때: ref로 영역 측정 → 차트가 100% fit
          <Box
            ref={!hasAnyIndicator ? setChartFillRef : undefined}
            sx={{
              flex: 1,
              minHeight: 0,
              display: 'flex',
              flexDirection: 'column',
              overflow: 'hidden',
              position: 'relative',
            }}
          >
            {/* 보조지표 계산·로딩 중 프로그레스 오버레이 */}
            {indicatorLoading && hasAnyIndicator && (
              <Box
                sx={{
                  position: 'absolute',
                  inset: 0,
                  zIndex: 10,
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'center',
                  justifyContent: 'center',
                  bgcolor: 'rgba(0,0,0,0.35)',
                  backdropFilter: 'blur(2px)',
                }}
              >
                <CircularProgress size={36} sx={{ color: 'primary.light' }} />
                <Typography variant="caption" sx={{ mt: 1, color: 'grey.300', fontWeight: 500 }}>
                  보조지표 계산 중...
                </Typography>
              </Box>
            )}
            <CandlestickChart
              key={`chart-${stock?.symbol}-${timeframe}`}
              ref={chartRef}
              data={chartData}
              indicatorData={indicatorData.length > 0 ? indicatorData : undefined}
              height={Math.max(MIN_CANDLE_HEIGHT, candleHeight)}
              showIndicatorOverlay={false}
              showIndicatorToggle={false}
              hideTimeScale={false}
              showSubPanelTimeScale={false}
              hideSubPanelToolbar={true}
              hideNavigationToolbar
              hideTrendAlignmentBar
              showTooltip={isChartFullscreen}
              subPanels={subPanels}
              onSubPanelReorder={handleSubPanelReorder}
              onIndicatorDelete={handleIndicatorDelete}
              onCandleHeightResize={handleCandleHeightResize}
              showCandleResizeHandle={hasAnyIndicator && (!!isChartFullscreen || !!preferCandleHeightRatio)}
              compactToolbar={true}
              maLines={maLines}
              ichimokuSettings={ichimokuSettings}
              maCalculationData={maCalculationData.length > 0 ? maCalculationData : chartData}
              allIndicatorData={allIndicatorData.length > 0 ? allIndicatorData : undefined}
              overlaySettingsOverride={overlaySettingsOverride}
              hideLegends={!isChartFullscreen}
              indicatorSettings={investmentAnalysisIndicatorSettings}
              colorSettings={investmentAnalysisColorSettings}
              colorVersion={colorVersion}
              settingsVersion={settingsVersion}
              alertMarkers={navAlertMarkers}
              ichimokuColors={ichimokuSettings ? {
                tenkan: investmentAnalysisColorSettings?.ichimokuTenkan ?? '#2962FF',
                kijun: investmentAnalysisColorSettings?.ichimokuKijun ?? '#E91E63',
                chikou: investmentAnalysisColorSettings?.ichimokuChikou ?? '#00E676',
                senkouA: investmentAnalysisColorSettings?.ichimokuSenkouA ?? '#26A69A',
                senkouB: investmentAnalysisColorSettings?.ichimokuSenkouB ?? '#FF7043',
                cloudBullish: investmentAnalysisColorSettings?.ichimokuCloudBullish,
                cloudBearish: investmentAnalysisColorSettings?.ichimokuCloudBearish,
                tenkanWidth: investmentAnalysisColorSettings?.ichimokuTenkanWidth,
                kijunWidth: investmentAnalysisColorSettings?.ichimokuKijunWidth,
                chikouWidth: investmentAnalysisColorSettings?.ichimokuChikouWidth,
                senkouAWidth: investmentAnalysisColorSettings?.ichimokuSenkouAWidth,
                senkouBWidth: investmentAnalysisColorSettings?.ichimokuSenkouBWidth,
              } : undefined}
            />
          </Box>
        ) : (
          <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1, minHeight: 0 }}>
            <Typography variant="body2" color="text.secondary">
              데이터 없음
            </Typography>
          </Box>
        )}
      </Box>
    </Box>
  );
};
