/* eslint-disable @typescript-eslint/no-redeclare */
import React, { useEffect, useRef, useState, forwardRef, useImperativeHandle, useMemo, useCallback } from 'react';
import { createChart, ColorType, IChartApi, ISeriesApi, Time } from 'lightweight-charts';
import { Box, Typography, IconButton, CircularProgress, Chip, Slider, Dialog, DialogTitle, DialogContent, DialogActions, Button, Table, TableBody, TableRow, TableCell, Menu, MenuItem, ListItemIcon, ListItemText, Divider, Tooltip } from '@mui/material';
import { DragIndicator, VisibilityOff, Settings, Delete, MoreVert, Star, StarBorder, ContentCopy, ViewList, Schedule, PushPin, ZoomOut, ZoomIn, ChevronLeft, ChevronRight, RestartAlt, Fullscreen, FullscreenExit, Add, Remove, Close } from '@mui/icons-material';
import { DndContext, closestCenter, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { CandleData, IndicatorData } from '../services/backtestApi';
import { useAppTheme, ThemeMode } from '../contexts/ThemeContext';
import { useIndicatorVisibility } from '../contexts/IndicatorVisibilityContext';
import { useChartColors } from '../contexts/ChartColorContext';
import { useIndicatorSettings } from '../contexts/IndicatorSettingsContext';
import { useUpbitData } from '../contexts/UpbitDataContext';

/**
 * ✅ TrendIndicatorBar는 이제 별도 파일 (TrendIndicatorBar.tsx)에 있습니다
 * 
 * 아래는 CandlestickChart 컴포넌트입니다
 */

// ❌ 기존 TrendIndicatorBar 정의 제거됨 (별도 파일로 이동)
// 이제 TrendIndicatorBar.tsx에서 import됩니다

/**
 * 선행스팬 ON 시 과거에는 서브 봉 수가 메인보다 적어 시간(setVisibleRange) 동기화가 필요했으나,
 * placeholder·투명 범위 시리즈로 논리봉이 맞춰진 경우가 많다. 메인은 handleScroll 등에서
 * setVisibleLogicalRange로 이동하므로, 서브에 setVisibleRange만 적용하면 fixLeftEdge 등에서
 * 동일 시각이 픽셀상 어긋날 수 있다 → 가시 논리 범위가 있으면 항상 그것을 우선한다.
 */
/**
 * 캔들+보조지표 세로 스택에서 우측 가격축 너비를 통일해야 timeToCoordinate·전역 크로스헤어 X가 패널 간 일치한다.
 * (메인 100 vs CCI 112 등이면 동일 시각이 픽셀상 좌우로 어긋남 — 일목 선행 시 더 두드러짐)
 */
function stackedPanelsRightPriceScaleMinWidth(
  subPanels: Array<{ enabled?: boolean; type: string }> | undefined,
  isMobile: boolean
): number {
  if (isMobile) return 40;
  const enabled = subPanels?.filter((p) => p.enabled) ?? [];
  let w = 100;
  for (const p of enabled) {
    if (p.type === 'cci') w = Math.max(w, 112);
    else if (p.type === 'dmi') w = Math.max(w, 128);
  }
  return w;
}

function applyMainVisibleToSubCharts(
  mainTimeScale: ReturnType<IChartApi['timeScale']>,
  subCharts: Record<string, IChartApi | null | undefined>,
  useTimeRangeSync?: boolean
): void {
  const useTimeFallback = !!useTimeRangeSync;
  const logicalRange = mainTimeScale.getVisibleLogicalRange();
  const visibleTime = mainTimeScale.getVisibleRange();

  Object.values(subCharts).forEach((subChart) => {
    if (!subChart) return;
    try {
      if (logicalRange) {
        subChart.timeScale().setVisibleLogicalRange(logicalRange);
        return;
      }
      if (
        useTimeFallback &&
        visibleTime &&
        visibleTime.from != null &&
        visibleTime.to != null
      ) {
        subChart.timeScale().setVisibleRange({
          from: visibleTime.from as Time,
          to: visibleTime.to as Time,
        });
      }
    } catch {
      // ignore
    }
  });
}

/**
 * 일목균형표 판단 로직
 * 
 * @param currentPrice 현재 종가
 * @param tenkan 전환선 값
 * @param kijun 기준선 값
 * @param senkouA 선행스팬1 값 (현재 시점의 26일 미래)
 * @param senkouB 선행스팬2 값 (현재 시점의 26일 미래)
 * @param chikou 후행스팬 값 (26일 전 가격)
 * @returns 시장 분석 결과
 */
interface IchimokuAnalysis {
  cloudPosition: '구름 위' | '구름 내부' | '구름 아래' | '불명';
  tkCross: '골든크로스' | '데드크로스' | '중립';
  chikouTrend: '상승 추세' | '하락 추세' | '중립';
  cloudThickness: number;
  cloudType: '상승 구름' | '하락 구름' | '불명';
  signal: '강한 매수' | '매수' | '중립' | '매도' | '강한 매도';
}

function analyzeIchimoku(
  currentPrice: number,
  tenkan?: number,
  kijun?: number,
  senkouA?: number,
  senkouB?: number,
  chikou?: number,
  previousPrice?: number
): IchimokuAnalysis {
  const analysis: IchimokuAnalysis = {
    cloudPosition: '불명',
    tkCross: '중립',
    chikouTrend: '중립',
    cloudThickness: 0,
    cloudType: '불명',
    signal: '중립'
  };

  // 1. 구름 위치 판단
  if (senkouA !== undefined && senkouB !== undefined) {
    const cloudTop = Math.max(senkouA, senkouB);
    const cloudBottom = Math.min(senkouA, senkouB);

    analysis.cloudThickness = Math.abs(senkouA - senkouB);
    analysis.cloudType = senkouA >= senkouB ? '상승 구름' : '하락 구름';

    if (currentPrice > cloudTop) {
      analysis.cloudPosition = '구름 위';
    } else if (currentPrice < cloudBottom) {
      analysis.cloudPosition = '구름 아래';
    } else {
      analysis.cloudPosition = '구름 내부';
    }
  }

  // 2. 전환선 vs 기준선 (TK Cross)
  if (tenkan !== undefined && kijun !== undefined) {
    if (tenkan > kijun) {
      analysis.tkCross = '골든크로스';
    } else if (tenkan < kijun) {
      analysis.tkCross = '데드크로스';
    }
  }

  // 3. 후행스팬 추세 (현재 가격 vs 26일 전 가격)
  if (chikou !== undefined && previousPrice !== undefined) {
    if (currentPrice > previousPrice) {
      analysis.chikouTrend = '상승 추세';
    } else if (currentPrice < previousPrice) {
      analysis.chikouTrend = '하락 추세';
    }
  }

  // 4. 종합 신호 판단
  let score = 0;

  // 구름 위치 점수
  if (analysis.cloudPosition === '구름 위') score += 2;
  else if (analysis.cloudPosition === '구름 아래') score -= 2;

  // TK Cross 점수
  if (analysis.tkCross === '골든크로스') score += 1;
  else if (analysis.tkCross === '데드크로스') score -= 1;

  // 후행스팬 점수
  if (analysis.chikouTrend === '상승 추세') score += 1;
  else if (analysis.chikouTrend === '하락 추세') score -= 1;

  // 구름 타입 점수
  if (analysis.cloudType === '상승 구름') score += 1;
  else if (analysis.cloudType === '하락 구름') score -= 1;

  // 최종 신호
  if (score >= 4) analysis.signal = '강한 매수';
  else if (score >= 2) analysis.signal = '매수';
  else if (score <= -4) analysis.signal = '강한 매도';
  else if (score <= -2) analysis.signal = '매도';
  else analysis.signal = '중립';

  return analysis;
}

/**
 * 볼린저밴드 영역 Canvas Overlay 컴포넌트
 * 상한선과 하한선 사이를 canvas polygon으로 채움
 */
interface BollingerBandOverlayProps {
  chartRef: React.RefObject<IChartApi | null>;
  candleSeriesRef: React.RefObject<ISeriesApi<'Candlestick'> | null>;
  bandData: Array<{ time: number; upper: number; lower: number }>;
  priceRange: { min: number; max: number };
  containerWidth: number;
  containerHeight: number;
  backgroundColor?: string;
  yAxisScaleFactor?: number; // ✅ Y축 스케일 팩터 (밴드 영역 재그리기 트리거용)
}

const BollingerBandOverlay: React.FC<BollingerBandOverlayProps> = ({
  chartRef,
  candleSeriesRef,
  bandData,
  priceRange,
  containerWidth,
  containerHeight,
  backgroundColor = 'rgba(33, 150, 243, 0.15)', // ✅ 기본 투명도 증가 (더 잘 보이도록)
  yAxisScaleFactor = 1.0
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  console.log(`🎨 [BollingerBandOverlay] 컴포넌트 렌더링:`, {
    dataLength: bandData.length,
    backgroundColor,
    containerWidth,
    containerHeight,
    hasChartRef: !!chartRef.current,
    hasCandleSeriesRef: !!candleSeriesRef.current
  });

  const drawBand = useCallback(() => {
    const canvas = canvasRef.current;
    const chart = chartRef.current;
    const candleSeries = candleSeriesRef.current;

    console.log(`🎨 [BollingerBand drawBand] 시작:`, {
      hasCanvas: !!canvas,
      hasChart: !!chart,
      hasSeries: !!candleSeries,
      dataLength: bandData.length,
      backgroundColor
    });

    if (!canvas || !chart || !candleSeries || bandData.length < 2) {
      console.warn('⚠️ [BollingerBand] 렌더링 조건 미충족');
      return;
    }

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    // Canvas 크기 설정 (Retina 디스플레이 대응)
    const dpr = window.devicePixelRatio || 1;
    canvas.width = containerWidth * dpr;
    canvas.height = containerHeight * dpr;
    canvas.style.width = `${containerWidth}px`;
    canvas.style.height = `${containerHeight}px`;
    ctx.scale(dpr, dpr);

    // 초기화
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    try {
      const timeScale = chart.timeScale();

      // ✅ Y축 라벨 영역 제외 (우측 100px는 Y축 라벨 영역)
      const priceScaleWidth = 100; // Y축 라벨 영역 너비
      const chartDrawWidth = containerWidth - priceScaleWidth; // 실제 차트 그리기 영역 너비

      // ✅ 클리핑 영역 설정: Y축 라벨 영역을 제외한 영역에만 그리기
      ctx.save();
      ctx.beginPath();
      ctx.rect(0, 0, chartDrawWidth, containerHeight);
      ctx.clip();

      const priceToY = (price: number): number => {
        const coord = candleSeries.priceToCoordinate(price);
        if (coord === null) {
          return containerHeight / 2;
        }
        return coord;
      };

      // 각 세그먼트를 polygon으로 그림
      for (let i = 0; i < bandData.length - 1; i++) {
        const curr = bandData[i];
        const next = bandData[i + 1];

        // 시간 좌표 변환
        const x1 = timeScale.timeToCoordinate(curr.time as Time);
        const x2 = timeScale.timeToCoordinate(next.time as Time);

        if (x1 === null || x2 === null) continue;

        // 화면 밖의 세그먼트는 스킵
        // ✅ chartDrawWidth를 기준으로 판단
        if ((x1 < -100 && x2 < -100) || (x1 > chartDrawWidth + 100 && x2 > chartDrawWidth + 100)) {
          continue;
        }

        // 가격을 Y좌표로 변환
        const y1Upper = priceToY(curr.upper);
        const y1Lower = priceToY(curr.lower);
        const y2Upper = priceToY(next.upper);
        const y2Lower = priceToY(next.lower);

        ctx.fillStyle = backgroundColor;

        // Polygon 그리기 (4개 꼭지점) - 상한선과 하한선 사이를 채움
        ctx.beginPath();
        ctx.moveTo(x1, y1Upper);  // 현재 시점의 상한선
        ctx.lineTo(x2, y2Upper);  // 다음 시점의 상한선
        ctx.lineTo(x2, y2Lower);  // 다음 시점의 하한선
        ctx.lineTo(x1, y1Lower);  // 현재 시점의 하한선
        ctx.closePath();
        ctx.fill();
      }

      // ✅ 클리핑 영역 복원
      ctx.restore();
      
      console.log(`✅ [BollingerBand] 배경 렌더링 완료: ${bandData.length}개 포인트, 색상: ${backgroundColor}, 그리기 영역: ${chartDrawWidth}px (Y축 제외)`);
    } catch (error) {
      console.error('❌ [BollingerBand] Canvas 그리기 오류:', error);
    }
  }, [chartRef, candleSeriesRef, bandData, priceRange, containerWidth, containerHeight, backgroundColor, yAxisScaleFactor]);

  // 초기 그리기
  useEffect(() => {
    drawBand();
  }, [drawBand]);

  // 차트 변경 시 다시 그리기
  useEffect(() => {
    const chart = chartRef.current;
    if (!chart) return;

    const timeScale = chart.timeScale();
    const handleVisibleRangeChange = () => {
      drawBand();
    };

    timeScale.subscribeVisibleLogicalRangeChange(handleVisibleRangeChange);

    return () => {
      timeScale.unsubscribeVisibleLogicalRangeChange(handleVisibleRangeChange);
    };
  }, [chartRef, drawBand]);

  return (
    <canvas
      ref={canvasRef}
      key={backgroundColor}
      style={{
        position: 'absolute',
        top: 0,
        left: 0,
        width: containerWidth,
        height: containerHeight,
        pointerEvents: 'none',
        zIndex: 2, // ✅ z-index 증가 (일목균형표 구름보다 앞에 표시)
      }}
    />
  );
};

/**
 * 일목균형표 구름 영역 Canvas Overlay 컴포넌트
 * 업비트 방식: 두 선(선행스팬1, 2) 사이를 canvas polygon으로 채움
 * 
 * 핵심 구현:
 * 1. 미래 시점(26일 선행)까지 포함한 구름 영역 표시
 * 2. 차트의 전체 가격 범위와 동기화된 정확한 Y좌표 계산
 * 3. 조건부 색상 (상승: 녹색, 하락: 빨강)
 */
interface IchimokuCloudOverlayProps {
  chartRef: React.RefObject<IChartApi | null>;
  candleSeriesRef: React.RefObject<ISeriesApi<'Candlestick'> | null>; // ✅ 정확한 Y좌표 변환을 위해 추가
  isDisposedRef?: React.RefObject<boolean>; // dispose 시 콜백 스킵용
  cloudData: Array<{ time: number; senkouA: number; senkouB: number }>;
  priceRange: { min: number; max: number }; // 차트 전체 가격 범위
  containerWidth: number;
  containerHeight: number;
  cloudBullishColor?: string; // 상승 구름 색상
  cloudBearishColor?: string; // 하락 구름 색상
  yAxisScaleFactor?: number; // ✅ Y축 스케일 팩터 (구름 영역 재그리기 트리거용)
  /** 선행스팬 선 (Canvas로 그려 lightweight-charts Line 시리즈 "Value is null" 회피) */
  senkouALineData?: Array<{ time: number; value: number }>;
  senkouBLineData?: Array<{ time: number; value: number }>;
  senkouAColor?: string;
  senkouBColor?: string;
  senkouAWidth?: number;
  senkouBWidth?: number;
  /** 전환선/기준선 (Canvas로 그려 addLineSeries "Value is null" 회피) */
  tenkanLineData?: Array<{ time: number; value: number }>;
  kijunLineData?: Array<{ time: number; value: number }>;
  tenkanColor?: string;
  kijunColor?: string;
  tenkanWidth?: number;
  kijunWidth?: number;
}

const IchimokuCloudOverlay: React.FC<IchimokuCloudOverlayProps> = ({
  chartRef,
  candleSeriesRef,
  isDisposedRef,
  cloudData,
  priceRange,
  containerWidth,
  containerHeight,
  cloudBullishColor = 'rgba(239, 83, 80, 0.2)',
  cloudBearishColor = 'rgba(38, 166, 154, 0.2)',
  yAxisScaleFactor = 1.0,
  senkouALineData = [],
  senkouBLineData = [],
  senkouAColor = '#26a69a',
  senkouBColor = '#ef5350',
  senkouAWidth = 1,
  senkouBWidth = 1,
  tenkanLineData = [],
  kijunLineData = [],
  tenkanColor = '#2196f3',
  kijunColor = '#f44336',
  tenkanWidth = 1,
  kijunWidth = 1.5,
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  const drawCloud = useCallback(() => {
    if (isDisposedRef?.current) return;
    const canvas = canvasRef.current;
    const chart = chartRef.current;
    const candleSeries = candleSeriesRef.current;

    const hasCloud = cloudData.length >= 2;
    const hasSenkouA = senkouALineData.length >= 2;
    const hasSenkouB = senkouBLineData.length >= 2;
    const hasTenkan = tenkanLineData.length >= 2;
    const hasKijun = kijunLineData.length >= 2;
    const hasAnythingToDraw = hasCloud || hasSenkouA || hasSenkouB || hasTenkan || hasKijun;

    if (!canvas || !chart || !candleSeries || !hasAnythingToDraw) {
      if (!candleSeries) console.warn('⚠️ [IchimokuCloud] candleSeries 없음');
      return;
    }

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    // Canvas 크기 설정 (Retina 디스플레이 대응)
    const dpr = window.devicePixelRatio || 1;
    canvas.width = containerWidth * dpr;
    canvas.height = containerHeight * dpr;
    canvas.style.width = `${containerWidth}px`;
    canvas.style.height = `${containerHeight}px`;
    ctx.scale(dpr, dpr);

    // 초기화
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    try {
      const timeScale = chart.timeScale();

      console.log(`📊 [IchimokuCloud] priceRange prop: ${priceRange.min.toFixed(0)} ~ ${priceRange.max.toFixed(0)}`);

      // ✅ Y축 라벨 영역 제외 (우측 100px는 Y축 라벨 영역)
      const priceScaleWidth = 100; // Y축 라벨 영역 너비
      const chartDrawWidth = containerWidth - priceScaleWidth; // 실제 차트 그리기 영역 너비

      // ✅ 클리핑 영역 설정: Y축 라벨 영역을 제외한 영역에만 그리기
      ctx.save();
      ctx.beginPath();
      ctx.rect(0, 0, chartDrawWidth, containerHeight);
      ctx.clip();

      /**
       * ✅ 가격을 Y픽셀 좌표로 변환
       * priceToCoordinate가 숫자를 반환하면 **항상 그 좌표만** 쓴다(클램프만 적용).
       * 이전에는 0~containerHeight 밖이면 priceRange 선형 보간으로 바꿔, 같은 구름 구간 안에서
       * 차트 좌표계와 보간 좌표계가 섞여 화면 전체 높이로 번지는 삼각형/직사각형이 생길 수 있었다.
       * coordinate가 null일 때만 priceRange 폴백.
       */
      const priceToY = (price: number): number => {
        const coord = candleSeries.priceToCoordinate(price);
        if (coord !== null && isFinite(coord)) {
          if (containerHeight <= 0) return 0;
          return Math.max(0, Math.min(containerHeight - 1, coord));
        }
        const { min, max } = priceRange;
        if (max > min && isFinite(min) && isFinite(max)) {
          const normalized = Math.max(0, Math.min(1, (price - min) / (max - min)));
          return containerHeight * (1 - normalized);
        }
        return containerHeight / 2;
      };

      /** 선행 A·B가 선형 보간 구간 (0~1) 안에서 교차하는 비율 s. 없으면 null (TradingView/업비트형 채움: 자기교차 사각형 fill 방지) */
      const senkouCrossFraction = (a1: number, b1: number, a2: number, b2: number): number | null => {
        const d1 = a1 - b1;
        const d2 = a2 - b2;
        const denom = d1 - d2;
        if (!isFinite(denom) || Math.abs(denom) < 1e-14) return null;
        const s = d1 / denom;
        if (!isFinite(s) || s <= 1e-9 || s >= 1 - 1e-9) return null;
        return s;
      };

      // 각 세그먼트를 polygon으로 그림 (업비트 방식)
      // 미래 시점(26일 선행)까지 포함하여 렌더링
      for (let i = 0; i < cloudData.length - 1; i++) {
        const curr = cloudData[i];
        const next = cloudData[i + 1];

        // 시간 좌표 변환 (미래 시점 포함)
        const x1 = timeScale.timeToCoordinate(curr.time as Time);
        const x2 = timeScale.timeToCoordinate(next.time as Time);

        if (x1 === null || x2 === null) continue;

        if (next.time <= curr.time) continue;
        if (Math.abs(x2 - x1) < 0.5) continue;

        // 화면 밖의 세그먼트는 스킵 (성능 최적화)
        // ✅ chartDrawWidth를 기준으로 판단
        if ((x1 < -100 && x2 < -100) || (x1 > chartDrawWidth + 100 && x2 > chartDrawWidth + 100)) {
          continue;
        }

        // ✅ 유효성 검사: 0/NaN/Infinity 스킵 (수평 블록·캔들 가림 방지)
        const isValid = (v: number) => v != null && !isNaN(v) && isFinite(v) && v > 1e-10;
        if (!isValid(curr.senkouA) || !isValid(curr.senkouB) || !isValid(next.senkouA) || !isValid(next.senkouB)) {
          continue;
        }

        // ✅ 가격 범위 검증: 차트 가격 범위의 10배를 벗어나는 값은 비정상 데이터로 간주하고 스킵
        const priceRangeExtended = (priceRange.max - priceRange.min) * 10;
        const minValidPrice = priceRange.min - priceRangeExtended;
        const maxValidPrice = priceRange.max + priceRangeExtended;
        const isPriceInRange = (v: number) => v >= minValidPrice && v <= maxValidPrice;
        
        if (!isPriceInRange(curr.senkouA) || !isPriceInRange(curr.senkouB) || 
            !isPriceInRange(next.senkouA) || !isPriceInRange(next.senkouB)) {
          console.warn(`⚠️ [IchimokuCloud] 가격 범위 초과 스킵: senkou=[${curr.senkouA.toFixed(0)}, ${curr.senkouB.toFixed(0)}] → [${next.senkouA.toFixed(0)}, ${next.senkouB.toFixed(0)}], 유효범위=[${minValidPrice.toFixed(0)}, ${maxValidPrice.toFixed(0)}]`);
          continue;
        }

        // 가격을 Y좌표로 변환 (차트의 가격 축과 동기화)
        const y1A = priceToY(curr.senkouA);
        const y1B = priceToY(curr.senkouB);
        const y2A = priceToY(next.senkouA);
        const y2B = priceToY(next.senkouB);

        const sCross = senkouCrossFraction(curr.senkouA, curr.senkouB, next.senkouA, next.senkouB);

        if (sCross != null) {
          const xc = x1 + sCross * (x2 - x1);
          const pCross =
            curr.senkouA + sCross * (next.senkouA - curr.senkouA);
          const yc = priceToY(pCross);
          const bullLeft = curr.senkouA >= curr.senkouB;
          const bullRight = next.senkouA >= next.senkouB;
          ctx.fillStyle = bullLeft ? cloudBullishColor : cloudBearishColor;
          ctx.beginPath();
          ctx.moveTo(x1, y1A);
          ctx.lineTo(xc, yc);
          ctx.lineTo(x1, y1B);
          ctx.closePath();
          ctx.fill();
          ctx.fillStyle = bullRight ? cloudBullishColor : cloudBearishColor;
          ctx.beginPath();
          ctx.moveTo(xc, yc);
          ctx.lineTo(x2, y2A);
          ctx.lineTo(x2, y2B);
          ctx.closePath();
          ctx.fill();
        } else {
          const isBullish = curr.senkouA >= curr.senkouB;
          ctx.fillStyle = isBullish ? cloudBullishColor : cloudBearishColor;
          ctx.beginPath();
          ctx.moveTo(x1, y1A);
          ctx.lineTo(x2, y2A);
          ctx.lineTo(x2, y2B);
          ctx.lineTo(x1, y1B);
          ctx.closePath();
          ctx.fill();
        }
      }

      // ✅ 선행스팬 선 그리기 (Canvas로 처리하여 lightweight-charts Line 시리즈 "Value is null" 회피)
      const drawLineSeries = (data: Array<{ time: number; value: number }>, color: string, lineWidth: number) => {
        if (data.length < 2) return;
        const isValid = (v: number) => v != null && !isNaN(v) && isFinite(v) && v > 1e-10;
        ctx.beginPath();
        let first = true;
        for (let i = 0; i < data.length; i++) {
          const curr = data[i];
          const x = timeScale.timeToCoordinate(curr.time as Time);
          if (x === null || !isValid(curr.value)) {
            first = true; // 갭 시 새 선분 시작
            continue;
          }
          const y = priceToY(curr.value);
          if (first) {
            ctx.moveTo(x, y);
            first = false;
          } else {
            ctx.lineTo(x, y);
          }
        }
        ctx.strokeStyle = color;
        ctx.lineWidth = lineWidth;
        ctx.stroke();
      };
      if (hasSenkouA) drawLineSeries(senkouALineData, senkouAColor, senkouAWidth);
      if (hasSenkouB) drawLineSeries(senkouBLineData, senkouBColor, senkouBWidth);
      if (hasTenkan) drawLineSeries(tenkanLineData, tenkanColor, tenkanWidth);
      if (hasKijun) drawLineSeries(kijunLineData, kijunColor, kijunWidth);

      // ✅ 클리핑 영역 복원
      ctx.restore();

      console.log(`✅ [IchimokuCloud] 구름 렌더링 완료: ${cloudData.length}개 포인트, 선행A=${senkouALineData.length}, 선행B=${senkouBLineData.length}, 가격범위: ${priceRange.min.toFixed(0)} ~ ${priceRange.max.toFixed(0)}, 그리기 영역: ${chartDrawWidth}px (Y축 제외)`);
      console.log(`🎨 [IchimokuCloud] 구름 색상: 상승=${cloudBullishColor}, 하락=${cloudBearishColor}`);
    } catch (error) {
      console.error('❌ [IchimokuCloud] Canvas 그리기 오류:', error);
    }
  }, [chartRef, isDisposedRef, cloudData, priceRange, containerWidth, containerHeight, cloudBullishColor, cloudBearishColor, candleSeriesRef, yAxisScaleFactor, senkouALineData, senkouBLineData, senkouAColor, senkouBColor, senkouAWidth, senkouBWidth, tenkanLineData, kijunLineData, tenkanColor, kijunColor, tenkanWidth, kijunWidth]);

  // 초기 그리기
  useEffect(() => {
    drawCloud();
  }, [drawCloud]);

  // 차트 변경 시 다시 그리기
  useEffect(() => {
    const chart = chartRef.current;
    if (!chart) return;

    const timeScale = chart.timeScale();
    const handleVisibleRangeChange = () => {
      drawCloud();
    };

    timeScale.subscribeVisibleLogicalRangeChange(handleVisibleRangeChange);

    return () => {
      timeScale.unsubscribeVisibleLogicalRangeChange(handleVisibleRangeChange);
    };
  }, [chartRef, drawCloud]);

  return (
    <canvas
      ref={canvasRef}
      key={`${cloudBullishColor}-${cloudBearishColor}`} // 색상 변경 시 Canvas 재생성
      style={{
        position: 'absolute',
        top: 0,
        left: 0,
        width: containerWidth,
        height: containerHeight,
        pointerEvents: 'none', // 마우스 이벤트 차트로 전달
        zIndex: 2, // 차트 위에 표시 (구름은 semi-transparent로 캔들 비침, -1이면 차트에 가려져 미표시됨)
      }}
    />
  );
};

/**
 * 단순 이동평균(SMA) 계산 함수
 */
const calculateSMA = (data: { time: number; close: number }[], period: number): { time: number; value: number | null }[] => {
  const result: { time: number; value: number | null }[] = [];

  for (let i = 0; i < data.length; i++) {
    if (i < period - 1) {
      // 초기 기간에는 null 값 설정
      result.push({
        time: data[i].time,
        value: null,
      });
    } else {
      let sum = 0;
      for (let j = 0; j < period; j++) {
        sum += data[i - j].close;
      }
      result.push({
        time: data[i].time,
        value: sum / period,
      });
    }
  }

  return result;
};

/**
 * 지수 이동평균(EMA) 계산 함수
 */
const calculateEMA = (data: { time: number; close: number }[], period: number): { time: number; value: number | null }[] => {
  const result: { time: number; value: number | null }[] = [];
  const multiplier = 2 / (period + 1);

  if (data.length < period) {
    // 데이터가 부족하면 모두 null로 반환
    return data.map(d => ({ time: d.time, value: null }));
  }

  let ema = 0;

  for (let i = 0; i < data.length; i++) {
    if (i < period - 1) {
      // 초기 기간에는 null 값 설정
      result.push({ time: data[i].time, value: null });
    } else if (i === period - 1) {
      // 첫 EMA는 SMA로 시작
      let sum = 0;
      for (let j = 0; j < period; j++) {
        sum += data[i - j].close;
      }
      ema = sum / period;
      result.push({ time: data[i].time, value: ema });
    } else {
      // 이후는 EMA 공식 적용
      ema = (data[i].close - ema) * multiplier + ema;
      result.push({ time: data[i].time, value: ema });
    }
  }

  return result;
};

/**
 * 차트 데이터 검증 함수 - null, undefined, NaN, Infinity 값 제거
 */
const validateChartData = (data: { time: number; value: number | null }[]): { time: number; value: number }[] => {
  return data
    .filter((d): d is { time: number; value: number } =>
      d !== null &&
      d !== undefined &&
      d.time !== null &&
      d.time !== undefined &&
      !isNaN(d.time) &&
      isFinite(d.time) &&
      d.time > 0 &&
      d.value !== null &&
      d.value !== undefined &&
      !isNaN(d.value) &&
      isFinite(d.value)
    );
};

/** Histogram/Volume 시리즈용: 유효한 time·value만 남기고 시간 오름차순 정렬, 동일 time은 마지막만 유지 (LWCharts Value is null 방지) */
const sanitizeHistogramSeriesData = <T extends { time: Time | number; value: number; color?: string }>(
  bars: T[]
): T[] => {
  const bySec = new Map<number, T>();
  for (const b of bars) {
    if (!b || b.value === undefined || b.value === null) continue;
    const v = Number(b.value);
    if (!Number.isFinite(v)) continue;
    const rawT = b.time;
    const sec = typeof rawT === 'number' ? Math.floor(rawT) : Math.floor(Number(rawT));
    if (!Number.isFinite(sec) || sec <= 0) continue;
    const color = typeof b.color === 'string' && b.color.length > 0 ? b.color : undefined;
    bySec.set(sec, { ...b, time: sec as Time, value: v, ...(color ? { color } : {}) } as T);
  }
  return Array.from(bySec.entries())
    .sort((a, b) => a[0] - b[0])
    .map(([, row]) => row);
};

/**
 * 일목균형표 계산 함수
 */
const calculateIchimoku = (data: { time: number; high: number; low: number; close: number }[],
  tenkanPeriod: number = 9,
  kijunPeriod: number = 26,
  senkouBPeriod: number = 52) => {
  const result: {
    time: number;
    tenkan?: number;
    kijun?: number;
    chikou?: number;
    senkouA?: number;
    senkouB?: number;
  }[] = [];

  const getHighLow = (start: number, end: number) => {
    let high = -Infinity;
    let low = Infinity;
    for (let i = start; i <= end; i++) {
      if (data[i].high > high) high = data[i].high;
      if (data[i].low < low) low = data[i].low;
    }
    return { high, low };
  };

  for (let i = 0; i < data.length; i++) {
    const item: any = { time: data[i].time };

    // 전환선 (Tenkan-sen): (9일 최고가 + 9일 최저가) / 2
    if (i >= tenkanPeriod - 1) {
      const { high, low } = getHighLow(i - tenkanPeriod + 1, i);
      item.tenkan = (high + low) / 2;
    }

    // 기준선 (Kijun-sen): (26일 최고가 + 26일 최저가) / 2
    if (i >= kijunPeriod - 1) {
      const { high, low } = getHighLow(i - kijunPeriod + 1, i);
      item.kijun = (high + low) / 2;
    }

    // 후행스팬 (Chikou Span): 현재 종가를 26일 후행
    item.chikou = data[i].close;

    // 선행스팬1 (Senkou Span A): (전환선 + 기준선) / 2, 26일 선행
    if (i >= Math.max(tenkanPeriod, kijunPeriod) - 1) {
      const { high: tHigh, low: tLow } = getHighLow(i - tenkanPeriod + 1, i);
      const { high: kHigh, low: kLow } = getHighLow(i - kijunPeriod + 1, i);
      const tenkan = (tHigh + tLow) / 2;
      const kijun = (kHigh + kLow) / 2;
      item.senkouA = (tenkan + kijun) / 2;
    }

    // 선행스팬2 (Senkou Span B): (52일 최고가 + 52일 최저가) / 2, 26일 선행
    if (i >= senkouBPeriod - 1) {
      const { high, low } = getHighLow(i - senkouBPeriod + 1, i);
      item.senkouB = (high + low) / 2;
    }

    result.push(item);
  }

  return result;
};

export interface ChartHandle {
  setCrosshairPosition: (time: Time) => void;
  clearCrosshair: () => void;
  setVisibleRange: (from: number, to: number) => void;
  setVisibleLogicalRange: (from: number, to: number) => void;
  getVisibleRange: () => { from: number; to: number } | null;
  getVisibleLogicalRange: () => { from: number; to: number } | null;
  resize: () => void;
  fitContent: () => void;
  timeScale: () => {
    timeToCoordinate: (time: number) => number | null;
    coordinateToTime: (x: number) => number | null;
  };
  priceScale: () => {
    priceToCoordinate: (price: number) => number | null;
    coordinateToPrice: (y: number) => number | null;
  };
  // ✅ 스크롤/줌 이벤트 구독 (외부 차트 동기화용)
  subscribeVisibleLogicalRangeChange: (callback: (range: { from: number; to: number }) => void) => void;
  unsubscribeVisibleLogicalRangeChange: (callback: (range: { from: number; to: number }) => void) => void;
  // ✅ 알림 마커 추가 (매수/매도 신호 표시)
  addAlertMarker: (time: Time, signalType: 'BUY' | 'SELL') => void;
  clearAlertMarkers: () => void;
  // ✅ 실시간 캔들 업데이트 (WebSocket에서 사용)
  updateLastCandle: (candle: { open: number; high: number; low: number; close: number; volume?: number; timestamp?: number | string; time?: string | number }, isNewCandle?: boolean) => void;
  // ✅ 실시간 지표 업데이트 (WebSocket에서 사용 - 이동평균선, 볼린저밴드, 일목균형표 등)
  updateLastIndicators: (candle: CandleData, allCandles: CandleData[], allIndicatorData: IndicatorData[]) => void;
}

// 차트 툴 타입 정의 (ChartToolbar와 동일)
export type ChartTool =
  | 'crosshair'
  | 'magnifier'
  | 'trendLine'
  | 'horizontalLine'
  | 'verticalLine'
  | 'rectangle'
  | 'fibonacci'
  | 'fibonacciTimezone'
  | 'fibonacciFan'
  | 'fibonacciCircle'
  | 'fibonacciWedge'
  | 'text'
  | 'brush'
  | 'measure'
  | 'diagonalLine'
  | 'angleLine'
  | 'parallelChannel';

export interface AlertMarker {
  time: Time;
  position: 'aboveBar' | 'belowBar';
  color: string;
  shape: 'arrowUp' | 'arrowDown' | 'circle';
  text: string;
}

export interface CandlestickChartProps {
  data: CandleData[];
  indicatorData?: IndicatorData[];
  height?: number;
  onTimeRangeChange?: (from: number, to: number) => void;
  onCrosshairMove?: (time: Time | null) => void;
  onVisibleRangeChange?: (from: number, to: number) => void;
  // 네비게이션 기능
  onNavigate?: (direction: 'past' | 'future') => void;
  showLeftButton?: boolean;
  showRightButton?: boolean;
  isLoading?: boolean;
  // 알림 마커
  alertMarkers?: AlertMarker[];
  // 보조지표 오버레이 표시 여부 (기본값: false)
  showIndicatorOverlay?: boolean;
  // 보조지표 토글 버튼 표시 여부 (기본값: showIndicatorOverlay와 동일)
  showIndicatorToggle?: boolean;
  // 외부 크로스헤어 동기화 (보조지표 차트에서 호버 시)
  externalHoverTime?: Time | null;
  // 외부 크로스헤어 활성화 여부 (보조지표 호버 시 자체 크로스헤어 숨김)
  hideNativeCrosshair?: boolean;
  // X축 타임스케일 숨김 여부 (기본값: false - 보조지표가 있을 때 캔들차트의 X축 숨김)
  hideTimeScale?: boolean;
  // ✅ 보조지표(서브패널) 하단에 날짜 라벨 표시 여부 (기본값: true - 투자분석에서 마지막 보조지표에 날짜 표시, 멀티차트에서는 false로 비표시)
  showSubPanelTimeScale?: boolean;
  // 툴팁 표시 여부 (기본값: true)
  showTooltip?: boolean;
  // ✅ 매수/매도 마커 클릭 핸들러
  onMarkerClick?: (timestamp: string, tradeType: 'BUY' | 'SELL') => void;
  // ✅ 활성 툴 (트레이딩뷰 모드)
  activeTool?: ChartTool | null;
  // ✅ 차트 클릭 핸들러 (피보나치 타임존 등)
  onChartClick?: (time: number) => void;
  // ✅ 추세 판단 MA 설정 (차트 하단 추세 표시바용)
  trendShortMA?: number;   // 단기 MA (기본값: 10)
  trendMediumMA?: number;  // 중기 MA (기본값: 20)
  trendLongMA?: number;    // 장기 MA (기본값: 60)
  // ✅ 차트 마우스 이벤트 핸들러 (드래그 방식 드로잉)
  onChartMouseDown?: (time: number) => void;
  onChartMouseMove?: (time: number) => void;
  onChartMouseUp?: (time: number) => void;
  // ✅ 줌 레벨 변경 핸들러 (보조지표 차트 동기화용)
  onZoomChange?: (fromTime: number, toTime: number) => void;
  // ✅ 차트 뷰 모드 ('basic' | 'tradingview')
  chartViewMode?: 'basic' | 'tradingview';
  // ✅ MA/Ichimoku 계산용 추가 데이터 (화면에는 표시하지 않고 계산에만 사용)
  maCalculationData?: CandleData[];
  // ✅ 전체 400개 지표 데이터 (MA 렌더링용 maValues 포함)
  allIndicatorData?: IndicatorData[];
  // ✅ 이동평균선 설정
  maLines?: Array<{
    id: string;
    enabled: boolean;
    period: number;
    color: string;
    width: number;
    type: 'SMA' | 'EMA';
  }>;
  // ✅ 일목균형표 설정
  ichimokuSettings?: {
    tenkan: boolean;
    kijun: boolean;
    chikou: boolean;
    senkouA: boolean;
    senkouB: boolean;
    tenkanPeriod: number;
    kijunPeriod: number;
    senkouBPeriod: number;
  };
  // ✅ 일목균형표 색상 및 선 굵기
  ichimokuColors?: {
    tenkan: string;
    kijun: string;
    chikou: string;
    senkouA: string;
    senkouB: string;
    cloudBullish?: string;  // 상승 구름 색상
    cloudBearish?: string;  // 하락 구름 색상
    tenkanWidth?: number;
    kijunWidth?: number;
    chikouWidth?: number;
    senkouAWidth?: number;
    senkouBWidth?: number;
  };
  // ✅ 서브차트 패널 설정 (TradingView 스타일)
  subPanels?: Array<{
    type: 'volume' | 'macd' | 'rsi' | 'stochastic' | 'cci' | 'adx' | 'obv' | 'newPsychological' | 'investPsychological' | 'bwi' | 'dmi' | 'williamsR' | 'momentum' | 'trix' | 'volumeOsc' | 'vr' | 'disparity';
    height?: number;  // 서브패널 높이 (기본값: 120px)
    enabled: boolean;
  }>;
  // ✅ 서브패널 타이틀 클릭 핸들러 (설정 화면 열기)
  onSubPanelSettingsClick?: (panelType: string, title: string) => void;
  // ✅ 보조지표 삭제 핸들러 (보조지표 off 설정)
  onIndicatorDelete?: (panelType: string) => void;
  // ✅ 보조지표 설정 (기준선 표시용)
  indicatorSettings?: any;
  // ✅ 차트 색상 설정 (기준선 색상용)
  colorSettings?: any;
  // ✅ 서브패널 순서 변경 핸들러
  onSubPanelReorder?: (fromIndex: number, toIndex: number) => void;
  // ✅ 모바일 모드 플래그 (모바일 최적화 터치 설정 적용)
  isMobileMode?: boolean;
  // ✅ 외부에서 제어하는 시간 범위 (실시간 동기화)
  sharedTimeRange?: { from: number; to: number } | null;
  // ✅ 색상 버전 (변경 시 서브패널 강제 업데이트용)
  colorVersion?: number;
  // ✅ 설정 버전 (변경 시 서브패널 강제 업데이트용)
  settingsVersion?: number;
  // ✅ 리사이즈 중 플래그 (차트 갱신 방지)
  isResizing?: boolean;
  // ✅ 외부 강제 업데이트 카운터 (보조지표 실시간 갱신용)
  externalForceUpdate?: number;
  // ✅ 컴팩트 툴바 (멀티차트 등 작은 차트에서 버튼 크기 축소)
  compactToolbar?: boolean;
  // ✅ 보조지표 하단 줌/스크롤/리셋 툴바 숨김 (멀티차트 전용 - 캔들 아래에 이미 동일 툴바 있음)
  hideSubPanelToolbar?: boolean;
  // ✅ 캔들 위·보조지표 하단 줌/좌우이동/리셋 툴바 숨김 (멀티차트·알림목록 — 투자분석은 미전달 시 표시)
  hideNavigationToolbar?: boolean;
  // ✅ 캔들 하단 정배열/역배열 추세 바 숨김 (멀티차트·알림목록)
  hideTrendAlignmentBar?: boolean;
  // ✅ 차트 내 범례 숨김 (멀티차트 등)
  hideLegends?: boolean;
  // ✅ 멀티차트 등에서 context 대신 사용할 오버레이 설정 (볼린저 등)
  overlaySettingsOverride?: {
    bollingerUpper?: boolean;
    bollingerMiddle?: boolean;
    bollingerLower?: boolean;
    bollingerPeriod?: number;
    bollingerStdDev?: number;
  };
  // ✅ 캔들 영역 높이 리사이즈 핸들 (캔들/보조지표 경계에 드래그 버튼)
  onCandleHeightResize?: (newHeight: number) => void;
  showCandleResizeHandle?: boolean;
}

// ✅ 드래그 가능한 서브패널 컴포넌트
interface SortableSubPanelProps {
  panel: {
    type: 'volume' | 'macd' | 'rsi' | 'stochastic' | 'cci' | 'adx' | 'obv' | 'newPsychological' | 'investPsychological' | 'bwi' | 'dmi' | 'williamsR' | 'momentum' | 'trix' | 'volumeOsc' | 'vr' | 'disparity';
    height?: number;
    enabled: boolean;
  };
  index: number;
  isEven: boolean;
  chartColors: any;
  themeMode: ThemeMode;
  subChartContainersRef: React.MutableRefObject<{ [key: string]: HTMLDivElement | null }>;
  onSubPanelSettingsClick?: (panelType: string, title: string) => void;
  onIndicatorDelete?: (panelType: string) => void;  // ✅ 보조지표 삭제(off) 핸들러
  showDragHandle: boolean;
  isMaximized?: boolean;
  onMaximize?: () => void;
  onRestore?: () => void;
  colorVersion?: number;  // ✅ 색상 버전
  settingsVersion?: number;  // ✅ 설정 버전
}

const SortableSubPanel: React.FC<SortableSubPanelProps> = ({
  panel,
  index,
  isEven,
  chartColors,
  themeMode,
  subChartContainersRef,
  onSubPanelSettingsClick,
  onIndicatorDelete,
  showDragHandle,
  isMaximized = false,
  onMaximize,
  onRestore,
  colorVersion,
  settingsVersion,
}) => {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: panel.type });

  // ✅ 그래프 선 선택 상태 관리
  const [isSelected, setIsSelected] = useState(false);
  // ✅ 툴박스 메뉴 앵커
  const [toolboxAnchor, setToolboxAnchor] = useState<null | HTMLElement>(null);
  // ✅ 더보기 서브메뉴 앵커
  const [moreMenuAnchor, setMoreMenuAnchor] = useState<null | HTMLElement>(null);

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  const alternatingBgColor = themeMode === 'dark'
    ? (isEven ? 'rgba(255, 255, 255, 0.02)' : chartColors.background)
    : (isEven ? 'rgba(0, 0, 0, 0.02)' : chartColors.background);

  const titleMap: Record<string, string> = {
    volume: '거래량',
    macd: 'MACD',
    rsi: 'RSI',
    stochastic: 'Stochastic Slow',
    cci: 'CCI',
    adx: 'ADX',
    obv: 'OBV',
    williamsR: 'Williams %R',
    trix: 'TRIX',
    disparity: '이격도',
    bwi: 'BWI',
    volumeOsc: 'VolumeOSC',
    vr: 'VR',
    newPsychological: '신심리도',
    investPsychological: '투자심리도',
    dmi: 'DMI',
  };

  // ✅ 보조지표 설정값 가져오기 (프론트엔드)
  const { indicatorSettings, updateIndicatorSettings } = useIndicatorSettings();
  // ✅ 백엔드 설정값 가져오기
  const { backendSettings } = useUpbitData();
  // ✅ 차트 색상 설정 가져오기
  const { colors: colorSettings } = useChartColors();
  
  // ✅ 디버그: 서브패널 렌더링 및 버전 변경 감지
  console.log(`🎬 [SortableSubPanel:${panel.type}] 렌더링됨 - colorVersion: ${colorVersion}, settingsVersion: ${settingsVersion} [BUILD_V6]`);
  
  useEffect(() => {
    console.log(`🔔 [SortableSubPanel:${panel.type}] 버전 변경 감지 - colorVersion: ${colorVersion}, settingsVersion: ${settingsVersion} [BUILD_V6]`);
  }, [colorVersion, settingsVersion, panel.type]);
  
  // ✅ 보조지표별 설정값 텍스트 생성 (프론트엔드)
  const getSettingsText = (panelType: string): React.ReactNode => {
    if (!indicatorSettings || !colorSettings) return '';
    
    switch (panelType) {
      case 'macd': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.macdLine }}>{indicatorSettings.macdFastPeriod}</span>
            {','}
            <span style={{ color: colorSettings.macdLine }}>{indicatorSettings.macdSlowPeriod}</span>
            {','}
            <span style={{ color: colorSettings.signalLine }}>{indicatorSettings.macdSignalPeriod}</span>
            {')'}
          </>
        );
      }
      case 'stochastic': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.stochasticK }}>{indicatorSettings.stochasticKPeriod}</span>
            {','}
            <span style={{ color: colorSettings.stochasticD }}>{indicatorSettings.stochasticDPeriod}</span>
            {','}
            <span style={{ color: colorSettings.stochasticK }}>{indicatorSettings.stochasticSlowing}</span>
            {')'}
          </>
        );
      }
      case 'rsi': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.rsiColor }}>{indicatorSettings.rsiPeriod}</span>
            {')'}
          </>
        );
      }
      case 'cci': {
        const values: Array<{ value: number; color: string }> = [];
        values.push({ value: indicatorSettings.cciPeriod, color: colorSettings.cciColor });
        if (indicatorSettings.cciSignalEnabled) {
          values.push({ value: indicatorSettings.cciSignalPeriod, color: colorSettings.cciSignalColor });
        }
        return (
          <>
            {values.map((v, idx) => (
              <span key={idx} style={{ color: v.color }}>
                {v.value}{idx < values.length - 1 ? ',' : ''}
              </span>
            ))}
          </>
        );
      }
      case 'adx': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.adxColor }}>{indicatorSettings.adxPeriod}</span>
            {')'}
          </>
        );
      }
      case 'dmi': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.plusDiColor }}>{indicatorSettings.dmiPeriod}</span>
            {')'}
          </>
        );
      }
      case 'williamsR': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.williamsRColor }}>{indicatorSettings.williamsRPeriod}</span>
            {')'}
          </>
        );
      }
      case 'obv': {
        const values: Array<{ value: number; color: string }> = [];
        if (indicatorSettings.obvEnabled) {
          values.push({ value: indicatorSettings.obvPeriod, color: colorSettings.obvColor });
        }
        if (indicatorSettings.obvSignalEnabled) {
          values.push({ value: indicatorSettings.obvSignalPeriod, color: colorSettings.obvSignalColor });
        }
        if (values.length === 0) return '';
        return (
          <>
            {values.map((v, idx) => (
              <span key={idx} style={{ color: v.color }}>
                {v.value}{idx < values.length - 1 ? ',' : ''}
              </span>
            ))}
          </>
        );
      }
      case 'bwi': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.bwiColor }}>{indicatorSettings.bwiPeriod}</span>
            {')'}
          </>
        );
      }
      case 'trix': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.trixColor }}>{indicatorSettings.trixPeriod}</span>
            {','}
            <span style={{ color: colorSettings.trixSignalColor }}>{indicatorSettings.trixSignalPeriod}</span>
            {')'}
          </>
        );
      }
      case 'volumeOsc': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.volumeOscColor }}>{indicatorSettings.volumeOscShortPeriod}</span>
            {','}
            <span style={{ color: colorSettings.volumeOscColor }}>{indicatorSettings.volumeOscLongPeriod}</span>
            {')'}
          </>
        );
      }
      case 'vr': {
        const values: Array<{ value: number; color: string }> = [];
        values.push({ value: indicatorSettings.vrPeriod, color: colorSettings.vrColor });
        if (indicatorSettings.vr2Enabled) {
          values.push({ value: indicatorSettings.vr2Period, color: colorSettings.vr2Color });
        }
        return (
          <>
            {values.map((v, idx) => (
              <span key={idx} style={{ color: v.color }}>
                {v.value}{idx < values.length - 1 ? ',' : ''}
              </span>
            ))}
          </>
        );
      }
      case 'newPsychological': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.newPsychologicalColor }}>{indicatorSettings.newPsychologicalPeriod}</span>
            {')'}
          </>
        );
      }
      case 'investPsychological': {
        return (
          <>
            {'('}
            <span style={{ color: colorSettings.investPsychologicalColor }}>{indicatorSettings.investPsychologicalPeriod}</span>
            {')'}
          </>
        );
      }
      case 'disparity': {
        const values: Array<{ value: number; color: string }> = [];
        if (indicatorSettings.disparityUltraShortEnabled) {
          values.push({ value: indicatorSettings.disparityUltraShortPeriod, color: colorSettings.disparityUltraShortColor });
        }
        if (indicatorSettings.disparityShortEnabled) {
          values.push({ value: indicatorSettings.disparityShortPeriod, color: colorSettings.disparityShortColor });
        }
        if (indicatorSettings.disparityMidEnabled) {
          values.push({ value: indicatorSettings.disparityMidPeriod, color: colorSettings.disparityMidColor });
        }
        if (indicatorSettings.disparityLongEnabled) {
          values.push({ value: indicatorSettings.disparityLongPeriod, color: colorSettings.disparityLongColor });
        }
        if (values.length === 0) return '';
        return (
          <>
            {values.map((v, idx) => (
              <span key={idx} style={{ color: v.color }}>
                {v.value}{idx < values.length - 1 ? ',' : ''}
              </span>
            ))}
          </>
        );
      }
      default:
        return '';
    }
  };


  // ✅ 툴박스 메뉴 핸들러
  const handleToolboxClose = () => {
    setToolboxAnchor(null);
    setMoreMenuAnchor(null);
  };

  const handleHideGraph = () => {
    setIsSelected(false);
    handleToolboxClose();
  };

  const handleSettings = () => {
    if (onSubPanelSettingsClick) {
      onSubPanelSettingsClick(panel.type, titleMap[panel.type] || panel.type.toUpperCase());
    }
    handleToolboxClose();
  };

  const handleDelete = () => {
    // ✅ 보조지표 삭제(off 설정)
    if (onIndicatorDelete) {
      onIndicatorDelete(panel.type);
    }
    handleToolboxClose();
  };

  const handleMoreClick = (event: React.MouseEvent<HTMLElement>) => {
    setMoreMenuAnchor(event.currentTarget);
  };

  const handleMoreClose = () => {
    setMoreMenuAnchor(null);
  };

  // ✅ 최대화/복원 핸들러
  const handleMaximizeToggle = (e: React.MouseEvent) => {
    e.stopPropagation();
    if (isMaximized && onRestore) {
      onRestore();
    } else if (!isMaximized && onMaximize) {
      onMaximize();
    }
  };

  return (
    <Box
      ref={setNodeRef}
      style={isMaximized ? {} : style}
      sx={{
        width: '100%',
        height: isMaximized ? '100%' : (panel.height || 120),
        position: isMaximized ? 'absolute' : 'relative',
        boxSizing: 'border-box',  // ✅ border를 width 안에 포함
        // ✅ border 대신 outline 사용 (차트 영역 너비에 영향 없음)
        border: 'none',
        outline: isSelected
          ? '2px solid #3b82f6'  // ✅ 선택 시 파란색 테두리
          : themeMode === 'dark' 
          ? '1px solid rgba(255, 255, 255, 0.15)' 
          : '1px solid rgba(0, 0, 0, 0.15)',
        outlineOffset: '-1px',  // ✅ outline을 안쪽으로 (border처럼 보이게)
        borderTop: `1px solid ${chartColors.gridColor}`,  // ✅ 구분선은 유지
        bgcolor: alternatingBgColor,
        opacity: isDragging ? 0.5 : 1,
        boxShadow: isDragging ? '0 4px 12px rgba(0,0,0,0.3)' : isSelected ? '0 0 8px rgba(59, 130, 246, 0.5)' : 'none',
        ...(isMaximized && {
          top: 0,
          left: 0,
          right: 0,
          bottom: 0,
          zIndex: 10,
          border: 'none',
        }),
      }}
    >
      <div
        ref={(el) => {
          subChartContainersRef.current[panel.type] = el;
        }}
        onClick={() => {
          // ✅ 차트 영역 클릭 시 선택 상태 토글
          setIsSelected(!isSelected);
        }}
        onDoubleClick={(e) => {
          // ✅ 차트 영역 더블클릭 시 설정 팝업 열기
          e.stopPropagation();
          if (onSubPanelSettingsClick) {
            onSubPanelSettingsClick(panel.type, titleMap[panel.type] || panel.type.toUpperCase());
          }
        }}
        style={{
          width: '100%',
          height: '100%',
          position: 'relative',
          cursor: 'pointer',
          boxSizing: 'border-box',
        }}
      />
      
      {/* 서브차트 타이틀 및 백엔드 설정값 - flex로 나란히 배치 */}
      <Box
        sx={{
          position: 'absolute',
          top: 4,
          left: 4,
          zIndex: 10,
          display: 'flex',
          alignItems: 'center',
          gap: 0.8,
        }}
      >
        {/* 타이틀 버튼 - 파란색 칩 스타일 (업비트 스타일) */}
        <Box
          onClick={(e) => {
            e.stopPropagation();
            if (onSubPanelSettingsClick) {
              onSubPanelSettingsClick(panel.type, titleMap[panel.type] || panel.type.toUpperCase());
            }
          }}
          sx={{
            cursor: 'pointer',
            display: 'flex',
            alignItems: 'center',
            gap: 1,
            pl: 1.5,
            pr: 1.5,
            py: 0.3,
            borderRadius: 0.8,
            bgcolor: themeMode === 'dark'
              ? 'rgba(100, 100, 100, 0.5)'
              : 'rgba(200, 200, 200, 0.6)',
            '&:hover': {
              bgcolor: themeMode === 'dark'
                ? 'rgba(120, 120, 120, 0.6)'
                : 'rgba(180, 180, 180, 0.7)',
            },
            transition: 'background-color 0.2s',
            border: '1px solid',
            borderColor: themeMode === 'dark'
              ? 'rgba(150, 150, 150, 0.3)'
              : 'rgba(160, 160, 160, 0.4)',
            boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
          }}
        >
          <Typography
            variant="caption"
            sx={{
              fontSize: '11px',
              color: themeMode === 'dark' ? '#ffffff' : '#1a1a1a',
              fontWeight: 700,
              textShadow: themeMode === 'dark' ? '0 1px 2px rgba(0,0,0,0.3)' : 'none',
            }}
          >
            {titleMap[panel.type] || panel.type.toUpperCase()}
            {typeof getSettingsText(panel.type) === 'string' ? (
              <Typography
                component="span"
                sx={{
                  fontSize: '10px',
                  color: themeMode === 'dark' ? 'rgba(255, 255, 255, 0.85)' : 'rgba(0, 0, 0, 0.7)',
                  fontWeight: 500,
                  ml: 0.5,
                }}
              >
                {getSettingsText(panel.type)}
              </Typography>
            ) : (
              <Box
                component="span"
                sx={{
                  fontSize: '10px',
                  fontWeight: 500,
                  ml: 0.5,
                }}
              >
                {getSettingsText(panel.type)}
              </Box>
            )}
          </Typography>
        </Box>

        {/* 백엔드 설정값 표시 제거됨 */}
      </Box>

      {/* ✅ 우측 상단 삭제(X) 버튼 - 항상 표시 (멀티차트 등). 우측 격자 셀에서 짤림 방지용 패딩 확보 */}
      {onIndicatorDelete && (
        <Tooltip title="보조지표 제거">
          <IconButton
            size="small"
            onClick={(e) => {
              e.stopPropagation();
              handleDelete();
            }}
            sx={{
              position: 'absolute',
              top: 4,
              right: 4,
              zIndex: 11,
              p: 0.5,
              bgcolor: themeMode === 'dark' ? 'rgba(60, 60, 60, 0.8)' : 'rgba(220, 220, 220, 0.8)',
              '&:hover': {
                bgcolor: themeMode === 'dark' ? 'rgba(100, 100, 100, 0.9)' : 'rgba(200, 200, 200, 0.95)',
              },
            }}
          >
            <Close sx={{ fontSize: 16 }} />
          </IconButton>
        </Tooltip>
      )}

      {/* ✅ 툴박스 - 타이틀 우측에 표시 (선택 시에만) */}
      {isSelected && (
        <Box
          sx={{
            position: 'absolute',
            top: 4,
            left: titleMap[panel.type] ? 100 : 80,  // 타이틀 우측
            zIndex: 11,
            display: 'flex',
            gap: 0.5,
            bgcolor: themeMode === 'dark' ? 'rgba(30, 30, 30, 0.95)' : 'rgba(255, 255, 255, 0.95)',
            borderRadius: 0.8,
            border: '1px solid',
            borderColor: themeMode === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)',
            boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
            p: 0.3,
          }}
        >
          <IconButton
            size="small"
            onClick={(e) => {
              e.stopPropagation();
              handleHideGraph();
            }}
            sx={{ p: 0.5 }}
            title="감추기"
          >
            <VisibilityOff sx={{ fontSize: 16 }} />
          </IconButton>
          <IconButton
            size="small"
            onClick={(e) => {
              e.stopPropagation();
              handleSettings();
            }}
            sx={{ p: 0.5 }}
            title="설정"
          >
            <Settings sx={{ fontSize: 16 }} />
          </IconButton>
          <IconButton
            size="small"
            onClick={(e) => {
              e.stopPropagation();
              handleDelete();
            }}
            sx={{ p: 0.5 }}
            title="삭제"
          >
            <Delete sx={{ fontSize: 16 }} />
          </IconButton>
          <IconButton
            size="small"
            onClick={(e) => {
              e.stopPropagation();
              handleMoreClick(e);
            }}
            sx={{ p: 0.5 }}
            title="더보기"
          >
            <MoreVert sx={{ fontSize: 16 }} />
          </IconButton>
        </Box>
      )}

      {/* ✅ 더보기 서브메뉴 */}
      <Menu
        anchorEl={moreMenuAnchor}
        open={Boolean(moreMenuAnchor)}
        onClose={handleMoreClose}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
        transformOrigin={{ vertical: 'top', horizontal: 'left' }}
      >
        <MenuItem onClick={() => { console.log('즐겨찾기 추가'); handleMoreClose(); }}>
          <ListItemIcon>
            <Star fontSize="small" />
          </ListItemIcon>
          <ListItemText primary="즐겨찾기에 인디케이터 추가" primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
        <Divider />
        <MenuItem onClick={() => { console.log('보는차례'); handleMoreClose(); }}>
          <ListItemIcon>
            <ViewList fontSize="small" />
          </ListItemIcon>
          <ListItemText primary="보는차례" primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
        <MenuItem onClick={() => { console.log('인터벌 가시성'); handleMoreClose(); }}>
          <ListItemIcon>
            <Schedule fontSize="small" />
          </ListItemIcon>
          <ListItemText primary="인터벌 가시성" primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
        <MenuItem onClick={() => { console.log('스케일 고정'); handleMoreClose(); }}>
          <ListItemIcon>
            <PushPin fontSize="small" />
          </ListItemIcon>
          <ListItemText primary="스케일 고정 (현재 오른쪽)" primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
        <Divider />
        <MenuItem onClick={() => { console.log('커피'); handleMoreClose(); }}>
          <ListItemText primary="커피 ⌘C" primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
        <Divider />
        <MenuItem onClick={() => { handleHideGraph(); handleMoreClose(); }}>
          <ListItemIcon>
            <VisibilityOff fontSize="small" />
          </ListItemIcon>
          <ListItemText primary="감추기" primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
        <MenuItem onClick={() => { handleDelete(); handleMoreClose(); }}>
          <ListItemIcon>
            <Delete fontSize="small" />
          </ListItemIcon>
          <ListItemText primary="없애기" primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
        <Divider />
        <MenuItem onClick={() => { console.log('오브젝트 트리'); handleMoreClose(); }}>
          <ListItemText primary="오브젝트 트리..." primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
        <MenuItem onClick={() => { handleSettings(); handleMoreClose(); }}>
          <ListItemIcon>
            <Settings fontSize="small" />
          </ListItemIcon>
          <ListItemText primary="설정..." primaryTypographyProps={{ fontSize: '13px' }} />
        </MenuItem>
      </Menu>

      {/* 우측 컨트롤 버튼 그룹 */}
      {showDragHandle && (
        <>
          {isMaximized ? (
            // 최대화 상태: 복원 버튼을 좌측 상단 타이틀 아래에 표시
            <Box
              sx={{
                position: 'absolute',
                top: 40,
                left: 4,
                zIndex: 20,
                display: 'flex',
                flexDirection: 'column',
                gap: 0.5,
                opacity: 0.6,
                '&:hover': {
                  opacity: 1,
                },
                transition: 'opacity 0.2s',
              }}
            >
              <IconButton
                size="small"
                onClick={handleMaximizeToggle}
                sx={{
                  p: 0.5,
                  bgcolor: 'action.hover',
                  borderRadius: 0.5,
                  '&:hover': {
                    bgcolor: 'action.selected',
                  },
                }}
                title="복원"
              >
                <FullscreenExit sx={{ fontSize: 18, color: '#8b95a5' }} />
              </IconButton>
            </Box>
          ) : (
            // 일반 상태: 최대화 버튼과 드래그 핸들을 좌측 상단 타이틀 아래에 표시
            <Box
              sx={{
                position: 'absolute',
                top: 40,
                left: 4,
                zIndex: 20,
                display: 'flex',
                flexDirection: 'column',
                gap: 0.5,
                opacity: 0.6,
                '&:hover': {
                  opacity: 1,
                },
                transition: 'opacity 0.2s',
              }}
            >
              {/* 최대화 버튼 */}
              <IconButton
                size="small"
                onClick={handleMaximizeToggle}
                sx={{
                  p: 0.5,
                  bgcolor: 'action.hover',
                  borderRadius: 0.5,
                  '&:hover': {
                    bgcolor: 'action.selected',
                  },
                }}
                title="최대화"
              >
                <Fullscreen sx={{ fontSize: 18, color: '#8b95a5' }} />
              </IconButton>
              
              {/* 드래그 핸들 */}
              <Box
                {...attributes}
                {...listeners}
                sx={{
                  cursor: isDragging ? 'grabbing' : 'grab',
                  display: 'flex',
                  alignItems: 'center',
                  justifyContent: 'center',
                  p: 0.5,
                  borderRadius: 0.5,
                  bgcolor: isDragging ? 'action.selected' : 'action.hover',
                }}
              >
                <DragIndicator sx={{ fontSize: 18, color: '#8b95a5' }} />
              </Box>
            </Box>
          )}
        </>
      )}
    </Box>
  );
};

/**
 * 캔들스틱 차트 컴포넌트
 * lightweight-charts v4를 사용
 */
const CandlestickChart = forwardRef<ChartHandle, CandlestickChartProps>(
  ({ data, indicatorData, height = 400, onTimeRangeChange, onCrosshairMove, onVisibleRangeChange, onNavigate, showLeftButton = true, showRightButton = false, isLoading = false, showIndicatorOverlay = false, showIndicatorToggle, externalHoverTime, hideNativeCrosshair = false, hideTimeScale = false, showSubPanelTimeScale = true, showTooltip = true, onMarkerClick, activeTool = 'crosshair', onChartClick, trendShortMA = 10, trendMediumMA = 20, trendLongMA = 60, onChartMouseDown, onChartMouseMove, onChartMouseUp, onZoomChange, chartViewMode = 'basic', maCalculationData, allIndicatorData, maLines, ichimokuSettings, ichimokuColors, subPanels = [], onSubPanelSettingsClick, onIndicatorDelete, indicatorSettings, colorSettings, onSubPanelReorder, isMobileMode = false, sharedTimeRange, colorVersion, settingsVersion, alertMarkers = [], isResizing = false, externalForceUpdate = 0, compactToolbar = false, hideSubPanelToolbar = false, hideNavigationToolbar = false, hideTrendAlignmentBar = false, overlaySettingsOverride, hideLegends = false, onCandleHeightResize, showCandleResizeHandle = false }, ref) => {
    
    // ✅ 디버그: CandlestickChart가 받은 props 확인
    console.log(`🎬 [CandlestickChart] 렌더링됨 - colorVersion: ${colorVersion}, settingsVersion: ${settingsVersion} [BUILD_V6]`);
    
    // ✅ 멀티차트 등에서 indicatorSettings prop 미전달 시 Context 사용 (가로 기준선 표시)
    const { indicatorSettings: contextIndicatorSettings } = useIndicatorSettings();
    const effectiveIndicatorSettings = indicatorSettings ?? contextIndicatorSettings;
    
    // ✅ maLines, ichimokuSettings, ichimokuColors를 직렬화하여 안정적인 참조 생성
    const maLinesKey = useMemo(() => JSON.stringify(maLines), [maLines]);
    const ichimokuSettingsKey = useMemo(() => JSON.stringify(ichimokuSettings), [ichimokuSettings]);
    const ichimokuColorsKey = useMemo(() => JSON.stringify(ichimokuColors), [ichimokuColors]);
    // ✅ effectiveIndicatorSettings 직렬화 - object 참조 변경 시 과도한 서브차트 effect 재실행 방지
    const effectiveIndicatorSettingsKey = useMemo(
      () => (effectiveIndicatorSettings ? JSON.stringify(effectiveIndicatorSettings) : ''),
      [effectiveIndicatorSettings]
    );
    const overlaySettingsOverrideKey = useMemo(
      () => JSON.stringify(overlaySettingsOverride ?? {}),
      [overlaySettingsOverride]
    );
    /** 알림·내비 마커만 바뀔 때 메인 차트 effect가 재실행되지 않아 setMarkers가 스킵되는 것을 방지 */
    const alertMarkersKey = useMemo(() => JSON.stringify(alertMarkers ?? []), [alertMarkers]);
    const dataForMarkersRef = useRef(data);
    dataForMarkersRef.current = data;

    /**
     * 투자분석: 백테스트/투자실행 후 isBuyPoint·isSellPoint만 바뀌고 length·첫봉 time은 그대로인 경우가 많음.
     * 메인 차트 effect가 재실행되지 않아 setMarkers가 스킵되던 문제를 이 키로 감지한다 (실시간 OHLC만 바뀌면 키 불변).
     */
    const dataTradeMarkerSyncKey = useMemo(() => {
      const parts: string[] = [];
      for (let i = 0; i < data.length; i++) {
        const d = data[i] as CandleData;
        if (d.isBuyPoint) parts.push(`B:${String(d.time)}`);
        if (d.isSellPoint) parts.push(`S:${String(d.time)}`);
      }
      return parts.join('|');
    }, [data]);

    /** 줌/스크롤 시 updateTradeMarkerPositions가 최신 매수·매도 시점을 읽도록 ref 유지 */
    const tradeMarkerOverlaySpecsRef = useRef<Array<{ timeSec: number; type: 'BUY' | 'SELL' }>>([]);
    const uniqueDataForTradeOverlayRef = useRef<CandleData[]>([]);

    // showIndicatorToggle이 명시적으로 설정되지 않으면 showIndicatorOverlay 값을 따름
    const shouldShowToggle = showIndicatorToggle !== undefined ? showIndicatorToggle : showIndicatorOverlay;
    const chartContainerRef = useRef<HTMLDivElement>(null);
    const chartRef = useRef<IChartApi | null>(null);
    const candleSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
    // ✅ 서브차트 refs (TradingView 스타일)
    const subChartContainersRef = useRef<{ [key: string]: HTMLDivElement | null }>({});
    const subChartsRef = useRef<{ [key: string]: IChartApi | null }>({});
    const subSeriesRef = useRef<{ [key: string]: any }>({});
    /** 선행스팬 ON일 때는 메인(플레이스홀더 봉)·서브(실봉만) 논리봉 수가 달라 “시간 기준” 동기화가 필수. 구독 핸들러·setTimeout은 effect 클로저보다 매 렌더 갱신 ref로 최신 모드를 읽는다 */
    const senkouSpanActiveForSubSyncRef = useRef(false);
    senkouSpanActiveForSubSyncRef.current = !!(ichimokuSettings?.senkouA || ichimokuSettings?.senkouB);
    const isSettingRangeRef = useRef(false);
    const isSettingExternalCrosshairRef = useRef(false); // 외부 크로스헤어 설정 중 플래그
    
    // ✅ 실시간 업데이트용: 이동평균선, 볼린저밴드 시리즈 저장
    const maSeriesMapRef = useRef<Map<number | string, ISeriesApi<'Line'>>>(new Map());
    const bollingerSeriesRef = useRef<{
      upper?: ISeriesApi<'Line'>;
      middle?: ISeriesApi<'Line'>;
      lower?: ISeriesApi<'Line'>;
    }>({});
    const mainChartTimeRangeRef = useRef<{ from: Time; to: Time } | null>(null); // ✅ 메인 차트 시간 범위 저장
    // ✅ 마우스 휠 줌 기능 제거 (하단 슬라이더로 대체)
    const markersRef = useRef<any[]>([]); // ✅ 마커 데이터를 ref로 저장 (줌인/줌아웃 시 유지)
    const activeToolRef = useRef<ChartTool | null>(activeTool);
    
    // ✅ 업비트 스타일 크로스헤어: 전역 수직선 + 개별 차트 수평선
    const [globalCrosshairX, setGlobalCrosshairX] = useState<number | null>(null);
    const [activeCrosshairChart, setActiveCrosshairChart] = useState<string | null>(null); // 'main' or panel.type
    const chartMainContainerRef = useRef<HTMLDivElement>(null); // 전체 차트 영역 ref
    
    // ✅ 차트 내비게이션 툴바 표시 여부 (모바일에서는 숨김)
    const [showNavigationToolbar, setShowNavigationToolbar] = useState(!isMobileMode);
    
    // ✅ 보조지표 최대화 상태 관리
    const [maximizedPanel, setMaximizedPanel] = useState<string | null>(null);
    // ✅ 캔들 영역 리사이즈 중 상태 (드래그 핸들 시각 피드백)
    const [isCandleResizing, setIsCandleResizing] = useState(false);
    
    // ✅ 드래그 중 상태 관리 (차트 갱신 방지용) - ref로 관리하여 useEffect dependency 문제 방지
    const isDraggingRef = useRef<boolean>(false);
    const isResizingRef = useRef<boolean>(false);
    
    // ✅ @dnd-kit 센서 설정 (드래그 앤 드롭)
    const sensors = useSensors(
      useSensor(PointerSensor, {
        activationConstraint: {
          distance: 8, // 8px 이상 움직여야 드래그 시작 (클릭과 구분)
        },
      })
    ); // ✅ 활성 툴 ref
    const lastClickTimeRef = useRef<{ time: number; timestamp: string; tradeType: 'BUY' | 'SELL' } | null>(null); // ✅ 더블클릭 감지용
    const isChartReadyRef = useRef<boolean>(false);  // ✅ 차트 준비 완료 플래그
    const pendingCrosshairRef = useRef<Time | null>(null);  // ✅ 대기 중인 크로스헤어 시간
    const dataRef = useRef<CandleData[]>(data); // ✅ 데이터를 ref로 저장 (클릭 핸들러에서 최신 데이터 참조)
    const initialLogicalRangeRef = useRef<{ from: number; to: number } | null>(null); // ✅ 초기 logical range 저장 (재설정용)
    const preservedViewRangeRef = useRef<{ from: number; to: number; scrollPosition: number } | null>(null); // ✅ 재생성 직전 사용자 뷰 범위 저장
    const initialBarSpacingRef = useRef<number>(8); // ✅ 초기 barSpacing 저장 (재설정용)
    const fitContentTimeoutsRef = useRef<number[]>([]); // ✅ fitContent setTimeout ID 저장 (cleanup용)
    const isChartDisposingRef = useRef<boolean>(false); // ✅ 차트 제거 중 플래그 (fitContent 호출 방지)
    const isUserInteractingRef = useRef<boolean>(false); // ✅ 사용자 인터랙션 중 플래그 (자동 동기화 방지)
    const syncTimeoutsRef = useRef<number[]>([]); // ✅ 동기화 setTimeout ID 저장 (cleanup용)

    // ★ 클로저 문제 해결: 항상 최신 콜백 참조
    const onCrosshairMoveRef = useRef(onCrosshairMove);
    const onVisibleRangeChangeRef = useRef(onVisibleRangeChange);
    const onNavigateRef = useRef(onNavigate);
    const onMarkerClickRef = useRef(onMarkerClick);
    const onChartClickRef = useRef(onChartClick);
    const onChartMouseDownRef = useRef(onChartMouseDown);
    const onChartMouseMoveRef = useRef(onChartMouseMove);
    const onChartMouseUpRef = useRef(onChartMouseUp);
    const onZoomChangeRef = useRef(onZoomChange);

    // ✅ 리사이즈 종료를 감지하기 위한 ref와 state
    const prevIsResizingRef = useRef(isResizing);
    const [forceUpdateCounter, setForceUpdateCounter] = useState(0);

    useEffect(() => {
      dataRef.current = data; // ✅ data 업데이트 시 ref도 업데이트
      
      // 🔍 디버깅: CandlestickChart가 받은 data prop의 마커 상태 확인
      console.log('🔍 [CandlestickChart useEffect] data prop 업데이트됨:', {
        총_캔들수: data?.length || 0,
        매수_마커수: data?.filter(c => c.isBuyPoint).length || 0,
        매도_마커수: data?.filter(c => c.isSellPoint).length || 0,
        매수_마커_샘플: data?.filter(c => c.isBuyPoint).slice(0, 3).map(c => ({
          time: c.time,
          isBuyPoint: c.isBuyPoint
        })) || [],
        매도_마커_샘플: data?.filter(c => c.isSellPoint).slice(0, 3).map(c => ({
          time: c.time,
          isSellPoint: c.isSellPoint
        })) || [],
      });
      
      onCrosshairMoveRef.current = onCrosshairMove;
      onVisibleRangeChangeRef.current = onVisibleRangeChange;
      onNavigateRef.current = onNavigate;
      onMarkerClickRef.current = onMarkerClick;
      onChartClickRef.current = onChartClick;
      onChartMouseDownRef.current = onChartMouseDown;
      onChartMouseMoveRef.current = onChartMouseMove;
      onChartMouseUpRef.current = onChartMouseUp;
      onZoomChangeRef.current = onZoomChange;
      activeToolRef.current = activeTool;
      
      // ✅ 리사이즈 종료 감지 - 차트 재생성 트리거
      if (prevIsResizingRef.current === true && isResizing === false) {
        console.log('🔄 [차트] 리사이즈 종료 - 서브차트 강제 재생성');
        // 강제로 서브차트 useEffect 재실행
        setForceUpdateCounter(prev => prev + 1);
      }
      
      isResizingRef.current = isResizing;
      prevIsResizingRef.current = isResizing;
    }, [data, onCrosshairMove, onVisibleRangeChange, onNavigate, onMarkerClick, onChartClick, onChartMouseDown, onChartMouseMove, onChartMouseUp, onZoomChange, activeTool, isResizing]);

    // ✅ 외부에서 제어하는 시간 범위 동기화 (모든 차트 간 X축 동기화)
    useEffect(() => {
      if (!chartRef.current || !sharedTimeRange) return;

      const { from, to } = sharedTimeRange;

      try {
        chartRef.current.timeScale().setVisibleRange({
          from: from as Time,
          to: to as Time,
        });
        console.log(`✅ [CandlestickChart] 외부 시간 범위 동기화: ${new Date(from * 1000).toISOString().slice(11, 19)} ~ ${new Date(to * 1000).toISOString().slice(11, 19)}`);
      } catch (e) {
        console.warn(`[CandlestickChart] 외부 시간 범위 동기화 실패:`, e);
      }
    }, [sharedTimeRange]); // ✅ sharedTimeRange 변경 시마다 실행

    // ✅ 차트 내비게이션 툴바 핸들러
    const handleZoomIn = useCallback(() => {
      const chart = chartRef.current;
      if (!chart || !data || data.length === 0) return;
      
      // ✅ 사용자 인터랙션 플래그 설정 (자동 동기화 방지)
      isUserInteractingRef.current = true;
      setTimeout(() => {
        isUserInteractingRef.current = false;
      }, 2000); // 2초 후 플래그 해제
      
      // ✅ 범위 설정 중 플래그 (무한 루프 방지)
      isSettingRangeRef.current = true;
      
      try {
        // ✅ barSpacing을 조절하여 간격 확대 (우측 자동 고정)
        const timeScale = chart.timeScale();
        const options = timeScale.options();
        const currentBarSpacing = options.barSpacing || 8;
        const newBarSpacing = Math.min(currentBarSpacing * 1.3, 50); // 30% 증가, 최대 50
        
        console.log(`🔍 [크게보기] barSpacing: ${currentBarSpacing} → ${newBarSpacing}`);
        
        chart.applyOptions({
          timeScale: {
            barSpacing: newBarSpacing,
          },
        });
        
        // 보조지표 차트도 동기화
        Object.values(subChartsRef.current).forEach(subChart => {
          if (subChart) {
            try {
              subChart.applyOptions({
                timeScale: {
                  barSpacing: newBarSpacing,
                },
              });
            } catch (e) {
              console.warn('보조지표 줌 동기화 실패:', e);
            }
          }
        });
        
        // ✅ 동기화 강제 적용 (50ms 후) — 선행스팬 ON은 시간 기준 동기화
        const senkouEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;
        setTimeout(() => {
          applyMainVisibleToSubCharts(timeScale, subChartsRef.current, senkouEnabled);
        }, 50);
      } finally {
        // ✅ 100ms 후 플래그 해제 (동기화 완료 후)
        setTimeout(() => {
          isSettingRangeRef.current = false;
        }, 100);
      }
    }, [data, ichimokuSettings]);

    const handleZoomOut = useCallback(() => {
      const chart = chartRef.current;
      if (!chart || !data || data.length === 0) return;
      
      // ✅ 사용자 인터랙션 플래그 설정 (자동 동기화 방지)
      isUserInteractingRef.current = true;
      setTimeout(() => {
        isUserInteractingRef.current = false;
      }, 2000); // 2초 후 플래그 해제
      
      // ✅ 범위 설정 중 플래그 (무한 루프 방지)
      isSettingRangeRef.current = true;
      
      try {
        // ✅ barSpacing을 조절하여 간격 축소 (우측 자동 고정)
        const timeScale = chart.timeScale();
        const options = timeScale.options();
        const currentBarSpacing = options.barSpacing || 8;
        const newBarSpacing = Math.max(currentBarSpacing * 0.7, 3); // 30% 감소, 최소 3
        
        console.log(`🔍 [작게보기] barSpacing: ${currentBarSpacing} → ${newBarSpacing}`);
        
        chart.applyOptions({
          timeScale: {
            barSpacing: newBarSpacing,
          },
        });
        
        // 보조지표 차트도 동기화
        Object.values(subChartsRef.current).forEach(subChart => {
          if (subChart) {
            try {
              subChart.applyOptions({
                timeScale: {
                  barSpacing: newBarSpacing,
                },
              });
            } catch (e) {
              console.warn('보조지표 줌 동기화 실패:', e);
            }
          }
        });
        
        // ✅ 동기화 강제 적용 (50ms 후) — 선행스팬 ON은 시간 기준 동기화
        const senkouEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;
        setTimeout(() => {
          applyMainVisibleToSubCharts(timeScale, subChartsRef.current, senkouEnabled);
        }, 50);
      } finally {
        // ✅ 100ms 후 플래그 해제 (동기화 완료 후)
        setTimeout(() => {
          isSettingRangeRef.current = false;
        }, 100);
      }
    }, [data, ichimokuSettings]);

    const handleScrollLeft = useCallback(() => {
      const chart = chartRef.current;
      if (!chart || !data || data.length === 0) return;
      
      const timeScale = chart.timeScale();
      const logicalRange = timeScale.getVisibleLogicalRange();
      if (!logicalRange) return;
      
      // ✅ 범위 설정 중 플래그 (무한 루프 방지)
      isSettingRangeRef.current = true;
      beginSuppressExternalRangeSync();
      
      // ✅ 범위 크기를 정확히 유지 (간격 변경 방지)
      const rangeSize = Math.max(1, logicalRange.to - logicalRange.from);
      const scrollAmount = Math.max(1, rangeSize * 0.05); // 5% 스크롤 (민감도 낮춤)
      
      // 좌로 스크롤 = 과거 데이터 보기 (from/to 모두 감소, 범위 크기 유지)
      let newFrom = logicalRange.from - scrollAmount;
      let newTo = logicalRange.to - scrollAmount;
      
      // ✅ 최소 범위 제한 (0 미만으로 가지 않도록)
      if (newFrom < 0) {
        newFrom = 0;
        newTo = rangeSize as any; // 범위 크기 유지
      }

      // 경계 상태로 이동이 0인 경우: 범위를 조금 줄여서라도 사용자가 이동 반응을 보게 함
      if (Math.abs(newFrom - logicalRange.from) < 0.0001 && Math.abs(newTo - logicalRange.to) < 0.0001) {
        const reducedRange = Math.max(20, Math.floor(rangeSize * 0.8));
        newFrom = Math.max(0, logicalRange.from - Math.max(1, reducedRange * 0.15));
        newTo = newFrom + reducedRange;
      }
      
      try {
        timeScale.setVisibleLogicalRange({ from: newFrom as any, to: newTo as any });
        const senkouEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;
        applyMainVisibleToSubCharts(timeScale, subChartsRef.current, senkouEnabled);
        setTimeout(() => {
          applyMainVisibleToSubCharts(timeScale, subChartsRef.current, senkouEnabled);
        }, 50);
      } finally {
        // ✅ 100ms 후 플래그 해제 (동기화 완료 후)
        setTimeout(() => {
          isSettingRangeRef.current = false;
        }, 100);
      }
    }, [data, ichimokuSettings]);

    const handleScrollRight = useCallback(() => {
      const chart = chartRef.current;
      if (!chart || !data || data.length === 0) return;
      
      const timeScale = chart.timeScale();
      const logicalRange = timeScale.getVisibleLogicalRange();
      if (!logicalRange) return;
      
      // ✅ 범위 설정 중 플래그 (무한 루프 방지)
      isSettingRangeRef.current = true;
      beginSuppressExternalRangeSync();
      
      // ✅ 범위 크기를 정확히 유지 (간격 변경 방지)
      const rangeSize = Math.max(1, logicalRange.to - logicalRange.from);
      const scrollAmount = Math.max(1, rangeSize * 0.05); // 5% 스크롤 (민감도 낮춤)
      
      // 우로 스크롤 = 최신 데이터 보기 (from/to 모두 증가, 범위 크기 유지)
      let newFrom = logicalRange.from + scrollAmount;
      let newTo = logicalRange.to + scrollAmount;
      
      // ✅ 최대 범위 제한 (데이터 범위 + rightOffset 내에서만 이동)
      // 업비트 스타일: 마지막 캔들 우측에 충분한 빈 공간을 허용 (200 바 정도)
      const maxLogicalIndex = data.length - 1 + 200;
      if (newTo > maxLogicalIndex) {
        newTo = maxLogicalIndex as any;
        newFrom = (newTo - rangeSize) as any; // 범위 크기 유지
      }

      // 경계 상태로 이동이 0인 경우: 범위를 조금 줄여서라도 사용자가 이동 반응을 보게 함
      if (Math.abs(newFrom - logicalRange.from) < 0.0001 && Math.abs(newTo - logicalRange.to) < 0.0001) {
        const reducedRange = Math.max(20, Math.floor(rangeSize * 0.8));
        newTo = Math.min(maxLogicalIndex as number, logicalRange.to + Math.max(1, reducedRange * 0.15));
        newFrom = Math.max(0, newTo - reducedRange);
      }
      
      try {
        timeScale.setVisibleLogicalRange({ from: newFrom as any, to: newTo as any });
        const senkouEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;
        applyMainVisibleToSubCharts(timeScale, subChartsRef.current, senkouEnabled);
        setTimeout(() => {
          applyMainVisibleToSubCharts(timeScale, subChartsRef.current, senkouEnabled);
        }, 50);
      } finally {
        // ✅ 100ms 후 플래그 해제 (동기화 완료 후)
        setTimeout(() => {
          isSettingRangeRef.current = false;
        }, 100);
      }
    }, [data, ichimokuSettings]);

    const handleResetChart = useCallback(() => {
      const chart = chartRef.current;
      if (!chart || !data || data.length === 0) return;
      
      // ✅ 범위 설정 중 플래그 (무한 루프 방지)
      isSettingRangeRef.current = true;
      
      try {
        console.log('🔄 [재설정] 초기 barSpacing으로 복원:', initialBarSpacingRef.current);
        
        // ✅ 1단계: 초기 barSpacing으로 복원
        chart.applyOptions({
          timeScale: {
            barSpacing: initialBarSpacingRef.current,
          },
        });
        
        // 보조지표 차트도 동일한 barSpacing 적용
        Object.values(subChartsRef.current).forEach(subChart => {
          if (subChart) {
            try {
              subChart.applyOptions({
                timeScale: {
                  barSpacing: initialBarSpacingRef.current,
                },
              });
            } catch (e) {
              console.warn('보조지표 barSpacing 재설정 실패:', e);
            }
          }
        });
        
        // ✅ 2단계: 마지막 캔들이 우측에 오도록 범위 설정
        setTimeout(() => {
          const timeScale = chart.timeScale();
          
          // fitContent로 전체 범위 계산
          chart.timeScale().fitContent();
          
          // 현재 범위 가져오기
          const logicalRange = timeScale.getVisibleLogicalRange();
          if (logicalRange) {
            const rangeSize = logicalRange.to - logicalRange.from;
            
            // 마지막 인덱스를 우측 끝으로 설정 (rightOffset 제외)
            const lastIndex = data.length - 1;
            const newTo = lastIndex;
            const newFrom = newTo - rangeSize;
            
            console.log('🔄 [재설정] 범위 조정:', { from: newFrom, to: newTo, dataLength: data.length });
            
            timeScale.setVisibleLogicalRange({
              from: Math.max(0, newFrom) as any,
              to: newTo as any,
            });
            
            const senkouEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;
            applyMainVisibleToSubCharts(timeScale, subChartsRef.current, senkouEnabled);
            setTimeout(() => {
              applyMainVisibleToSubCharts(timeScale, subChartsRef.current, senkouEnabled);
            }, 100);
          }
          
          console.log('✅ [재설정] 완료');
        }, 100);
      } finally {
        // ✅ 250ms 후 플래그 해제 (범위 설정 완료 및 동기화 재적용 대기)
        setTimeout(() => {
          isSettingRangeRef.current = false;
        }, 250);
      }
    }, [data, ichimokuSettings]);

    // 테마 색상 가져오기
    const { chartColors: themeChartColors, themeMode } = useAppTheme();
    
    // 사용자 지정 색상 가져오기
    const { colors: userColors } = useChartColors();
    
    // ✅ 메인 차트 관련 색상만 추출 (보조지표 색상 변경 시 재렌더링 방지)
    const mainChartKeys = [
      'upColor', 'downColor', 'wickUpColor', 'wickDownColor', 'borderUpColor', 'borderDownColor',
      'candleUpColor', 'candleDownColor',
      'ma1Color', 'ma2Color', 'ma3Color', 'ma4Color',
      'bollingerUpperColor', 'bollingerMiddleColor', 'bollingerLowerColor',
      'ichimokuTenkanColor', 'ichimokuKijunColor', 'ichimokuSenkouAColor', 'ichimokuSenkouBColor', 'ichimokuChikouColor',
      'envelopeUpperColor', 'envelopeLowerColor', 'envelopeMiddleColor',
    ];
    
    // ✅ 메인 차트 관련 색상 값을 문자열로 직렬화 (보조지표 색상 변경 무시)
    const mainChartColorsStr = useMemo(() => {
      const values = mainChartKeys.map(key => `${key}:${(userColors as any)[key]}`).join('|');
      return values;
    }, [userColors]); // eslint-disable-line react-hooks/exhaustive-deps
    
    // ✅ 테마 색상과 사용자 색상 병합 (사용자 색상이 우선)
    const chartColors = useMemo(() => ({
      ...themeChartColors,
      ...userColors,
    }), [themeChartColors, mainChartColorsStr]); // eslint-disable-line react-hooks/exhaustive-deps

    // 보조지표 표시 설정
    const { settings, updateSettings } = useIndicatorVisibility();

    // showIndicatorOverlay가 false일 때는 settings 변경으로 인한 리렌더링 방지
    const effectiveSettings = useMemo(() => {
      if (!showIndicatorOverlay) return null;
      return settings;
    }, [showIndicatorOverlay, settings]);

    const [maValues, setMaValues] = useState<{ ma12: number | null; ma26: number | null; ma60: number | null }>({
      ma12: null,
      ma26: null,
      ma60: null,
    });

    const [tooltipData, setTooltipData] = useState<{
      visible: boolean;
      x: number;
      y: number;
      time: string;
      open: number;
      high: number;
      low: number;
      close: number;
      change: number;
      changePercent: number;
      volume: number;
    } | null>(null);

    // ✅ 더블 클릭으로 고정된 툴팁 상태
    const [pinnedTooltipData, setPinnedTooltipData] = useState<{
      visible: boolean;
      x: number;
      y: number;
      time: string;
      open: number;
      high: number;
      low: number;
      close: number;
      change: number;
      changePercent: number;
      volume: number;
    } | null>(null);
    
    const tooltipRef = useRef<HTMLDivElement>(null);

    // 커스텀 크로스헤어 오버레이 상태
    const [externalCrosshair, setExternalCrosshair] = useState<{
      x: number;
      y: number;
      price: number;
      visible: boolean;
    } | null>(null);

    // 마우스가 차트 위에 있는지 추적
    const [isMouseOverChart, setIsMouseOverChart] = useState(false);

    // 일목균형표 구름 데이터 (Canvas Overlay용)
    const [ichimokuCloudData, setIchimokuCloudData] = useState<Array<{ time: number; senkouA: number; senkouB: number }>>([]);
    // 선행스팬 선 데이터 (Canvas Overlay용 - addLineSeries 대체하여 "Value is null" 회피)
    const [senkouALineData, setSenkouALineData] = useState<Array<{ time: number; value: number }>>([]);
    const [senkouBLineData, setSenkouBLineData] = useState<Array<{ time: number; value: number }>>([]);
    const [tenkanLineData, setTenkanLineData] = useState<Array<{ time: number; value: number }>>([]);
    const [kijunLineData, setKijunLineData] = useState<Array<{ time: number; value: number }>>([]);

    // 볼린저밴드 배경 데이터 (Canvas Overlay용)
    const [bollingerBandData, setBollingerBandData] = useState<Array<{ time: number; upper: number; lower: number }>>([]);

    // 트렌드 바 상세 정보 모달 상태
    const [trendDetailModal, setTrendDetailModal] = useState<{
      visible: boolean;
      timestamp: string;
      maShort: number;
      maMedium: number;
      maLong: number;
      trendType: '정배열' | '역배열' | '혼조';
      allMaValues?: any; // 모든 MA 값들
    } | null>(null);

    // 차트 전체 가격 범위 (구름 영역 Y좌표 계산용)
    const [chartPriceRange, setChartPriceRange] = useState<{ min: number; max: number }>({ min: 0, max: 0 });
    
    // 돋보기 기능 상태
    const [magnifierPos, setMagnifierPos] = useState<{ x: number; y: number } | null>(null);
    const magnifierActive = activeTool === 'magnifier';
    const [magnifierZoomLevel, setMagnifierZoomLevel] = useState<number>(1); // 1 = 기본, 2 = 2배 확대
    const isMagnifierZoomingRef = useRef(false); // 돋보기 줌 중일 때 다른 range 조정 차단
    const [magnifierCenterTime, setMagnifierCenterTime] = useState<number | null>(null); // 돋보기 중심 시간
    const [magnifierShowChart, setMagnifierShowChart] = useState<boolean>(false); // 클릭 시에만 차트 표시
    const magnifierShowChartRef = useRef(false); // magnifierShowChart의 ref 버전 (이벤트 핸들러용)
    const magnifierJustClickedRef = useRef(false); // 클릭 직후 플래그 (마우스 이동 무시용)
    const magnifierClickPosRef = useRef<{ x: number; y: number } | null>(null); // 클릭 시점의 마우스 위치
    const magnifierChartRef = useRef<any>(null); // 돋보기용 별도 차트
    const magnifierContainerRef = useRef<HTMLDivElement | null>(null); // 돋보기 차트 컨테이너
    const [magnifierTradeMarkers, setMagnifierTradeMarkers] = useState<Array<{
      x: number;
      y: number;
      type: 'BUY' | 'SELL';
      time: string;
    }>>([]); // 돋보기용 마커 오버레이
    const [magnifierCloudData, setMagnifierCloudData] = useState<Array<{ time: number; senkouA: number; senkouB: number }>>([]);  // 돋보기용 구름 데이터
    const [magnifierPriceRange, setMagnifierPriceRange] = useState<{ min: number; max: number }>({ min: 0, max: 0 });  // 돋보기용 가격 범위
    const magnifierChartContainerRef = useRef<HTMLDivElement | null>(null);  // 돋보기 차트 wrapper (Canvas Overlay용)

    // magnifierShowChart state와 ref 동기화
    useEffect(() => {
      magnifierShowChartRef.current = magnifierShowChart;
    }, [magnifierShowChart]);

    // ✅ 고정된 툴팁 외부 클릭 시 닫기
    useEffect(() => {
      if (!pinnedTooltipData?.visible) return;

      const handleClickOutside = (event: MouseEvent) => {
        const target = event.target as HTMLElement;
        
        // 툴팁 내부 클릭인지 확인
        if (tooltipRef.current && tooltipRef.current.contains(target)) {
          return;
        }
        
        // 툴팁 외부 클릭 시 닫기
        console.log(`📌 [툴팁 닫기] 외부 클릭 감지`);
        setPinnedTooltipData(null);
      };

      // 짧은 딜레이 후 이벤트 리스너 등록 (더블 클릭 이벤트와 충돌 방지)
      const timeoutId = setTimeout(() => {
        document.addEventListener('click', handleClickOutside);
      }, 100);

      return () => {
        clearTimeout(timeoutId);
        document.removeEventListener('click', handleClickOutside);
      };
    }, [pinnedTooltipData?.visible]);

    // 가로 스크롤바 상태
    const [scrollbarValue, setScrollbarValue] = useState<number>(100); // 100 = 오른쪽 끝
    const [totalTimeRange, setTotalTimeRange] = useState<{ from: number; to: number } | null>(null);
    const [currentVisibleRange, setCurrentVisibleRange] = useState<{ from: number; to: number } | null>(null);
    const isScrollingRef = useRef(false);
    const suppressExternalRangeSyncRef = useRef(false);
    const suppressExternalRangeSyncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
    const beginSuppressExternalRangeSync = useCallback((ms: number = 220) => {
      suppressExternalRangeSyncRef.current = true;
      if (suppressExternalRangeSyncTimeoutRef.current) {
        clearTimeout(suppressExternalRangeSyncTimeoutRef.current);
      }
      suppressExternalRangeSyncTimeoutRef.current = setTimeout(() => {
        suppressExternalRangeSyncRef.current = false;
        suppressExternalRangeSyncTimeoutRef.current = null;
      }, ms);
    }, []);

    useEffect(() => {
      return () => {
        if (suppressExternalRangeSyncTimeoutRef.current) {
          clearTimeout(suppressExternalRangeSyncTimeoutRef.current);
        }
      };
    }, []);

    // ✅ Y축 스케일 팩터 (1.0 = 기본, > 1.0 = 확대, < 1.0 = 축소)
    const [yAxisScaleFactor, setYAxisScaleFactor] = useState<number>(1.0);
    const yAxisScaleFactorRef = useRef<number>(1.0);
    const candleDataRawRef = useRef<Array<{ time: number; open: number; high: number; low: number; close: number; volume: number }>>([]);
    /**
     * 메인 캔들 시리즈 setData에 넣은 **전체** 봉(실봉+일목 미래 플레이스홀더)의 Unix 초.
     * data 프롭은 실봉만 있어 서브차트 범위 확장·클램프·미래 구간 평탄 연장에 이 배열이 필요함.
     */
    const mainCandleDisplayTimesRef = useRef<number[]>([]);
    /** 일목 선행스팬용 평평한 미래 캔들 time 목록 — WS 갱신 시 실제 봉만 바꾸면 우측 lastValue가 플레이스홀더에 고정되는 문제 방지 */
    const ichimokuFlatCandleTimesRef = useRef<number[]>([]);

    // visible range 추적
    const visibleRangeRef = useRef<{ from: number; to: number } | null>(null);
    
    // ✅ 탭 전환 시 차트 범위 보존용 ref
    const savedVisibleRangeBeforeHiddenRef = useRef<{ from: number; to: number; logicalFrom: number; logicalTo: number } | null>(null);
    /** 논리봉이 이보다 적으면 저장/복원하지 않음 — 극단 줌(수 봉만)이 고정되어 캔들이 판처럼 보이는 문제 방지 */
    const MIN_LOGICAL_BARS_TO_PERSIST = 16;
    const logicalBarsVisible = (lr: { from: number; to: number } | null) =>
      lr && Number.isFinite(lr.from) && Number.isFinite(lr.to) ? Math.max(0, lr.to - lr.from) : 0;

    // ✅ Y축 스케일 팩터 ref 동기화 및 차트 업데이트
    useEffect(() => {
      yAxisScaleFactorRef.current = yAxisScaleFactor;
      
      // 차트가 있으면 즉시 업데이트 트리거
      if (chartRef.current && candleSeriesRef.current) {
        const timeScale = chartRef.current.timeScale();
        const currentRange = timeScale.getVisibleLogicalRange();
        if (currentRange) {
          // 동일한 범위로 설정하여 autoscaleInfoProvider 재실행 트리거
          timeScale.setVisibleLogicalRange(currentRange);
        }
      }
    }, [yAxisScaleFactor]);

    // ✅ 차트 파괴 여부 플래그 (cleanup 시 모든 핸들러 무시)
    const isDisposedRef = useRef<boolean>(false);

    // ✅ DevTools 토글 대응 Refs
    const resizeObserverRef = useRef<ResizeObserver | null>(null);
    const devicePixelRatioRef = useRef<number>(window.devicePixelRatio || 1);
    const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
    const isChartCreatingRef = useRef<boolean>(false);
    const containerSizeRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 });

    // ✅ 외부 스크롤/줌 이벤트 구독자들 (CandlestickTestPage의 독립 IndicatorChart 동기화용)
    const externalLogicalRangeChangeCallbacksRef = useRef<Set<(range: { from: number; to: number }) => void>>(new Set());

    // 매수/매도 마커 오버레이 상태
    const [tradeMarkers, setTradeMarkers] = useState<Array<{
      x: number;
      y: number;
      type: 'BUY' | 'SELL';
      time: string;
    }>>([]);

    // ❌ 스크롤바 위치 자동 업데이트 제거 (좌우 스크롤 시 스케일 변경 방지)
    // useEffect(() => {
    //   if (currentVisibleRange && totalTimeRange && !isScrollingRef.current) {
    //     const totalDuration = totalTimeRange.to - totalTimeRange.from;
    //     const scrollPosition = (currentVisibleRange.to - totalTimeRange.from) / totalDuration;
    //     setScrollbarValue(Math.round(scrollPosition * 100));
    //   }
    // }, [currentVisibleRange, totalTimeRange]);

    useImperativeHandle(ref, () => ({
      setCrosshairPosition: (time: Time) => {
        if (chartRef.current && candleSeriesRef.current && data.length > 0) {
          try {
            const targetTime = time as number;
            const dataPoint = data.find(d => {
              const dataTime = new Date(d.time).getTime() / 1000;
              return Math.floor(dataTime) === Math.floor(targetTime);
            });

            if (dataPoint) {
              chartRef.current.setCrosshairPosition(dataPoint.close, time, candleSeriesRef.current);
            }
          } catch (e) {
            // 에러 무시
          }
        }
      },
      clearCrosshair: () => {
        if (chartRef.current) {
          chartRef.current.clearCrosshairPosition();
        }
      },
      setVisibleRange: (from: number, to: number) => {
        if (chartRef.current && !isSettingRangeRef.current) {
          isSettingRangeRef.current = true;
          try {
            // ✅ 단순하게 visible range만 설정 (깜빡임 방지)
            chartRef.current.timeScale().setVisibleRange({
              from: from as Time,
              to: to as Time,
            });
          } catch (e) {
            // 에러 무시
          }
          setTimeout(() => {
            isSettingRangeRef.current = false;
          }, 10);
        }
      },
      setVisibleLogicalRange: (from: number, to: number) => {
        if (chartRef.current && !isSettingRangeRef.current) {
          isSettingRangeRef.current = true;
          try {
            // ✅ logical range 설정 (스케일 유지하면서 스크롤)
            chartRef.current.timeScale().setVisibleLogicalRange({
              from,
              to,
            });
          } catch (e) {
            // 에러 무시
          }
          setTimeout(() => {
            isSettingRangeRef.current = false;
          }, 10);
        }
      },
      getVisibleRange: () => {
        if (chartRef.current) {
          try {
            const range = chartRef.current.timeScale().getVisibleRange();
            if (range) {
              return {
                from: range.from as number,
                to: range.to as number,
              };
            }
          } catch (e) {
            // 에러 무시
          }
        }
        return null;
      },
      getVisibleLogicalRange: () => {
        if (chartRef.current) {
          try {
            const range = chartRef.current.timeScale().getVisibleLogicalRange();
            if (range) {
              return {
                from: range.from,
                to: range.to,
              };
            }
          } catch (e) {
            // 에러 무시
          }
        }
        return null;
      },
      resize: () => {
        if (chartRef.current && chartContainerRef.current) {
          chartRef.current.applyOptions({
            width: chartContainerRef.current.clientWidth,
            height: height,
          });
        }
      },
      fitContent: () => {
        if (chartRef.current) {
          try {
            chartRef.current.timeScale().fitContent();
          } catch (e) {
            console.error('fitContent 오류:', e);
          }
        }
      },
      timeScale: () => ({
        timeToCoordinate: (time: number) => {
          if (chartRef.current) {
            return chartRef.current.timeScale().timeToCoordinate(time as Time);
          }
          return null;
        },
        coordinateToTime: (x: number) => {
          if (chartRef.current) {
            const time = chartRef.current.timeScale().coordinateToTime(x);
            return time as number | null;
          }
          return null;
        },
      }),
      priceScale: () => ({
        priceToCoordinate: (price: number) => {
          if (candleSeriesRef.current) {
            return candleSeriesRef.current.priceToCoordinate(price);
          }
          return null;
        },
        coordinateToPrice: (y: number) => {
          if (candleSeriesRef.current) {
            return candleSeriesRef.current.coordinateToPrice(y);
          }
          return null;
        }
      }),
      // ✅ 외부 차트가 메인 차트의 스크롤/줌 이벤트를 구독
      subscribeVisibleLogicalRangeChange: (callback: (range: { from: number; to: number }) => void) => {
        externalLogicalRangeChangeCallbacksRef.current.add(callback);
        console.log(`✅ [ChartHandle] 외부 logical range 구독 추가 (총 ${externalLogicalRangeChangeCallbacksRef.current.size}개)`);
      },
      // ✅ 구독 해제
      unsubscribeVisibleLogicalRangeChange: (callback: (range: { from: number; to: number }) => void) => {
        externalLogicalRangeChangeCallbacksRef.current.delete(callback);
        console.log(`✅ [ChartHandle] 외부 logical range 구독 해제 (총 ${externalLogicalRangeChangeCallbacksRef.current.size}개)`);
      },
      // ✅ 알림 마커 추가 (현재는 props로 전달하므로 빈 구현)
      addAlertMarker: (time: Time, signalType: 'BUY' | 'SELL') => {
        console.log(`✅ [ChartHandle] 알림 마커 추가: ${signalType} at ${time}`);
        // alertMarkers는 props로 전달되므로 여기서는 로그만 출력
      },
      // ✅ 알림 마커 초기화
      clearAlertMarkers: () => {
        console.log(`✅ [ChartHandle] 알림 마커 초기화`);
        // alertMarkers는 props로 전달되므로 여기서는 로그만 출력
      },
      // ✅ 실시간 캔들 업데이트 (마지막 캔들만 업데이트)
      // ✅ 스크롤 위치 유지: 갱신 전 visible range 저장 → 업데이트 후 복원
      updateLastCandle: (candle: { open: number; high: number; low: number; close: number; volume?: number; timestamp?: number | string; time?: string | number }, isNewCandle?: boolean) => {
        if (candleSeriesRef.current && chartRef.current) {
          try {
            const toSec = (t: number | string | undefined): number => {
              if (t == null || t === '') return NaN;
              if (typeof t === 'number') {
                return t > 1e12 ? Math.floor(t / 1000) : Math.floor(t);
              }
              const ms = new Date(t).getTime();
              return Number.isFinite(ms) ? Math.floor(ms / 1000) : NaN;
            };

            // timestamp/time 정규화: 초 단위 Unix timestamp (lightweight-charts 형식)
            const rawTs = candle.timestamp ?? candle.time;
            let timeInSeconds = toSec(rawTs as number | string | undefined);
            
            // ✅ candleDataRawRef를 사용하여 실제 캔들 데이터 업데이트 (플레이스홀더 제외)
            const rawCandles = candleDataRawRef.current || [];
            if (rawCandles.length === 0) {
              return;
            }
            
            const lastIdx = rawCandles.length - 1;
            const lastCandle = rawCandles[lastIdx];
            const lastCandleTime = toSec(lastCandle.time as number | string | undefined);

            // WS에 시각이 없거나 파싱 실패 시: 차트 마지막 봉 시각으로만 갱신 (상위에서 동일 진행 봉으로 판별했다고 가정)
            if (typeof timeInSeconds !== 'number' || isNaN(timeInSeconds) || !isFinite(timeInSeconds)) {
              if (!Number.isFinite(lastCandleTime)) {
                return;
              }
              timeInSeconds = lastCandleTime;
            }

            let barIntervalSec = 60;
            if (lastIdx >= 1) {
              const prevT = toSec(rawCandles[lastIdx - 1].time as number | string | undefined);
              if (Number.isFinite(prevT) && Number.isFinite(lastCandleTime)) {
                barIntervalSec = Math.max(60, Math.abs(lastCandleTime - prevT));
              }
            }
            
            // ✅ 과거 봉 무시 / 새 봉(다음 구간)은 여기서 처리하지 않음 — 상위에서 증분 로드
            if (!Number.isFinite(lastCandleTime) || timeInSeconds < lastCandleTime) {
              return;
            }
            if (timeInSeconds >= lastCandleTime + barIntervalSec) {
              return;
            }
            
            // ✅ 같은 진행 봉: 시리즈 time은 차트에 이미 올라간 마지막 봉 시각으로 통일 (WS·REST 초 불일치 방지)
            const seriesTime = lastCandleTime;
            rawCandles[lastIdx] = {
              ...lastCandle,
              open: candle.open,
              high: candle.high,
              low: candle.low,
              close: candle.close,
              volume: candle.volume ?? lastCandle.volume,
              time: seriesTime,
            };

            const flatTimes = ichimokuFlatCandleTimesRef.current;
            const v = candle.close;

            // ✅ 일목 미래 플레이스홀더가 시리즈 맨 끝에 있으면 LWCharts는 "가장 최신 time"의 봉만 update 가능.
            //    실제 진행 봉(더 과거 time)에 update() 시 "Cannot update oldest data" → 캔들이 안 움직임.
            //    플레이스홀더가 있을 때는 실캔들+플레이스홀더 전체를 setData로 동기화한다.
            if (flatTimes.length > 0) {
              const fullForSeries = rawCandles.map((c) => {
                const tNum =
                  typeof c.time === 'number' ? Math.floor(c.time) : toSec(c.time as string);
                return {
                  time: tNum as any,
                  open: c.open,
                  high: c.high,
                  low: c.low,
                  close: c.close,
                  volume: c.volume ?? 0,
                };
              });
              for (let i = 0; i < flatTimes.length; i++) {
                const t = flatTimes[i];
                fullForSeries.push({
                  time: t as any,
                  open: v,
                  high: v,
                  low: v,
                  close: v,
                  volume: 0,
                });
              }
              candleSeriesRef.current.setData(fullForSeries as any);
              
              // ✅ 새 봉 생성 시 차트를 최신 위치로 자동 스크롤
              if (isNewCandle && chartRef.current) {
                console.log('🔄 [Chart] 새 봉 생성 - 차트 자동 스크롤');
                chartRef.current.timeScale().scrollToRealTime();
              }
            } else {
              candleSeriesRef.current.update({
                time: seriesTime as any,
                open: candle.open,
                high: candle.high,
                low: candle.low,
                close: candle.close,
              } as any);
              
              // ✅ 새 봉 생성 시 차트를 최신 위치로 자동 스크롤
              if (isNewCandle && chartRef.current) {
                console.log('🔄 [Chart] 새 봉 생성 - 차트 자동 스크롤');
                chartRef.current.timeScale().scrollToRealTime();
              }
            }
          } catch (error) {
            console.error('[updateLastCandle] 오류:', error);
          }
        } else {
          console.warn('⚠️ [ChartHandle] candleSeriesRef가 없음 - 업데이트 스킵');
        }
      },
      // ✅ 실시간 지표 업데이트 (WebSocket에서 사용 - 이동평균선, 볼린저밴드 등)
      updateLastIndicators: (updatedCandle: CandleData, allCandles: CandleData[], allIndicatorData: IndicatorData[]) => {
        try {
          if (isDisposedRef.current || !chartRef.current || allCandles.length === 0) return;

          const lastTime = allCandles[allCandles.length - 1].time;
          const lastIdx = allCandles.length - 1;

          /** updateLastCandle / buildCandleDataRaw와 동일: 초 단위 Unix (ms 숫자는 /1000) */
          const toChartUnixSeconds = (t: number | string | undefined): number => {
            if (t == null || t === '') return NaN;
            if (typeof t === 'number') {
              return t > 1e12 ? Math.floor(t / 1000) : Math.floor(t);
            }
            const ms = new Date(t as string).getTime();
            return Number.isFinite(ms) ? Math.floor(ms / 1000) : NaN;
          };

          const rawRef = candleDataRawRef.current;
          let chartTimeSec = NaN;
          if (
            rawRef.length > 0 &&
            lastIdx >= 0 &&
            lastIdx < rawRef.length &&
            rawRef[lastIdx] != null &&
            Number.isFinite(rawRef[lastIdx]!.time)
          ) {
            chartTimeSec = Math.floor(rawRef[lastIdx]!.time as number);
          }
          if (!Number.isFinite(chartTimeSec)) {
            chartTimeSec = toChartUnixSeconds(allCandles[lastIdx]?.time as any);
          }
          if (!Number.isFinite(chartTimeSec)) {
            console.warn('⚠️ [updateLastIndicators] 마지막 봉 time 변환 실패 — 스킵');
            return;
          }

          /** setData(parseTime)가 부동소수 초를 넣거나, 일목·투자분석 등으로 시리즈 끝 time이 진행봉과 다를 때
           *  chartTimeSec로만 update() 하면 LW가 실패·무시 → 우측 라벨만 바뀌고 선은 멈춘 것처럼 보일 수 있음 */
          const seriesUpdateTime = (
            s: { data?: () => readonly { time: Time }[] } | null | undefined
          ): Time | null => {
            if (isDisposedRef.current) return null;
            if (!s || typeof s.data !== 'function') return null;
            try {
              const pts = s.data();
              if (!pts || pts.length === 0) return null;
              const target = Math.floor(chartTimeSec);
              for (let i = pts.length - 1; i >= 0; i--) {
                const t = pts[i]?.time;
                if (typeof t === 'number' && Number.isFinite(t) && Math.floor(t) === target) {
                  return t as Time;
                }
              }
              const lastT = pts[pts.length - 1]?.time;
              if (typeof lastT === 'number' && Number.isFinite(lastT)) {
                const lastSec = Math.floor(lastT);
                if (target === lastSec) return lastT as Time;
                if (target > lastSec) return target as Time;
              }
              return null;
            } catch {
              return null;
            }
          };
          
          /** ✅ 안전한 업데이트 헬퍼: 시리즈에 해당 시간 데이터가 있을 때만 업데이트 */
          const safeUpdate = (
            series: any,
            value: number | { value: number; color?: string },
            indicatorName: string
          ): boolean => {
            if (!series || typeof series.update !== 'function') return false;
            const updateTime = seriesUpdateTime(series);
            if (updateTime === null) {
              console.log(`⚠️ [${indicatorName}] 데이터 부족 - 업데이트 스킵`);
              return false;
            }
            if (typeof value === 'number') {
              series.update({ time: updateTime, value });
            } else {
              series.update({ time: updateTime, ...value });
            }
            return true;
          };
          
          console.log(
            `📊 [updateLastIndicators] 실시간 지표 업데이트 시작: time=${lastTime}, chartTimeSec=${chartTimeSec}, lastIdx=${lastIdx}`
          );

          // ✅ 이동평균선 마지막 값만 재계산 및 update() 방식 적용
          maSeriesMapRef.current.forEach((series, period) => {
            try {
              const periodNum = typeof period === 'number' ? period : parseInt(period, 10);
              
              // 마지막 MA 값만 계산
              if (lastIdx >= periodNum - 1) {
                const slice = allCandles.slice(lastIdx - periodNum + 1, lastIdx + 1);
                const sum = slice.reduce((acc, c) => acc + c.close, 0);
                const maValue = sum / periodNum;
                
                // ✅ 시리즈에 해당 시간의 데이터가 있는지 확인
                const updateTime = seriesUpdateTime(series);
                if (updateTime !== null) {
                  series.update({
                    time: updateTime,
                    value: maValue,
                  });
                  console.log(`✅ [MA${period}] update() 완료: ${maValue.toFixed(2)}`);
                } else {
                  console.log(`⚠️ [MA${period}] 데이터 부족 - 업데이트 스킵`);
                }
              }
            } catch (error) {
              console.error(`❌ [MA${period}] 업데이트 오류:`, error);
            }
          });

          // ✅ 볼린저밴드 마지막 값만 재계산 및 update() 방식 적용
          try {
            const bollingerPeriod = 20;
            if (lastIdx >= bollingerPeriod - 1) {
              const slice = allCandles.slice(lastIdx - bollingerPeriod + 1, lastIdx + 1);
              const sum = slice.reduce((acc, c) => acc + c.close, 0);
              const mean = sum / bollingerPeriod;
              
              const variance = slice.reduce((acc, c) => {
                const diff = c.close - mean;
                return acc + diff * diff;
              }, 0) / bollingerPeriod;
              
              const stdDev = Math.sqrt(variance);
              const upperBand = mean + 2 * stdDev;
              const lowerBand = mean - 2 * stdDev;
              
              if (bollingerSeriesRef.current.upper) {
                const u = bollingerSeriesRef.current.upper;
                const updateTime = seriesUpdateTime(u);
                if (updateTime !== null) {
                  u.update({
                    time: updateTime,
                    value: upperBand,
                  });
                  console.log(`✅ [Bollinger Upper] update() 완료: ${upperBand.toFixed(2)}`);
                } else {
                  console.log(`⚠️ [Bollinger Upper] 데이터 부족 - 업데이트 스킵`);
                }
              }
              
              if (bollingerSeriesRef.current.middle) {
                const m = bollingerSeriesRef.current.middle;
                const updateTime = seriesUpdateTime(m);
                if (updateTime !== null) {
                  m.update({
                    time: updateTime,
                    value: mean,
                  });
                  console.log(`✅ [Bollinger Middle] update() 완료: ${mean.toFixed(2)}`);
                } else {
                  console.log(`⚠️ [Bollinger Middle] 데이터 부족 - 업데이트 스킵`);
                }
              }
              
              if (bollingerSeriesRef.current.lower) {
                const lo = bollingerSeriesRef.current.lower;
                const updateTime = seriesUpdateTime(lo);
                if (updateTime !== null) {
                  lo.update({
                    time: updateTime,
                    value: lowerBand,
                  });
                  console.log(`✅ [Bollinger Lower] update() 완료: ${lowerBand.toFixed(2)}`);
                } else {
                  console.log(`⚠️ [Bollinger Lower] 데이터 부족 - 업데이트 스킵`);
                }
              }
            }
          } catch (bollErr) {
            console.error('❌ [Bollinger] 실시간 업데이트 오류:', bollErr);
          }

          // ✅ 서브패널 보조지표 실시간 업데이트
          // 캔들 N개 vs 지표 M개 불일치 시 allIndicatorData[lastIdx]가 undefined가 되어 전부 스킵되던 문제 방지
          if (lastIdx >= 0) {
            let lastIndicator: IndicatorData | undefined;
            if (allIndicatorData && allIndicatorData.length > 0) {
              let indIdx = Math.min(lastIdx, allIndicatorData.length - 1);
              lastIndicator = allIndicatorData[indIdx];
              const ct = chartTimeSec;
              const rawInd = lastIndicator as any;
              if (rawInd) {
                const indT = toChartUnixSeconds(
                  (rawInd.time ?? rawInd.timestamp ?? rawInd.date) as string | number | undefined
                );
                if (!Number.isFinite(indT) || indT !== ct) {
                  for (let i = allIndicatorData.length - 1; i >= 0; i--) {
                    const row = allIndicatorData[i] as any;
                    const t = toChartUnixSeconds(
                      (row?.time ?? row?.timestamp ?? row?.date) as string | number | undefined
                    );
                    if (Number.isFinite(t) && t === ct) {
                      lastIndicator = allIndicatorData[i];
                      break;
                    }
                  }
                }
              }
            }

            let updatedCount = 0;

            // ✅ 일목 선행스팬: 서브 거래량 히스토그램도 미래 봉까지 포함 → 마지막 logical bar가 미래 time이라
            //    실제 진행 봉 time으로 update() 시 메인 캔들과 동일하게 실패할 수 있음 → 전체 setData로 동기화
            const flatTimesSub = ichimokuFlatCandleTimesRef.current;
            const useSubVolumeFullSet = flatTimesSub.length > 0;
            const parseSubTime = (raw: string | number | undefined): number => {
              if (raw == null || raw === '') return NaN;
              if (typeof raw === 'number') {
                return raw > 1e12 ? Math.floor(raw / 1000) : Math.floor(raw);
              }
              const ms = new Date(raw as string).getTime();
              return Number.isFinite(ms) ? Math.floor(ms / 1000) : NaN;
            };
              
            // Volume 업데이트 (항상 updatedCandle.volume 사용 — lastIndicator 없어도 실행)
            const volumeSeries = subSeriesRef.current['volume'];
              if (volumeSeries && updatedCandle.volume !== undefined) {
                if (useSubVolumeFullSet) {
                  const rising = colorSettings?.volumeRising || '#ef5350';
                  const falling = colorSettings?.volumeFalling || '#2196f3';
                  const pts: { time: any; value: number; color: string }[] = [];
                  for (const d of allCandles) {
                    if (d.volume === undefined || d.volume === null) continue;
                    // ✅ volume 값 유효성 체크
                    if (!isFinite(d.volume) || d.volume < 0) continue;
                    const t = parseSubTime(d.time as any);
                    if (!Number.isFinite(t) || t <= 0) continue;
                    const o = d.open ?? d.close ?? 0;
                    const c = d.close ?? o;
                    pts.push({
                      time: t as any,
                      value: d.volume as number,
                      color: c >= o ? rising : falling,
                    });
                  }
                  for (const ft of flatTimesSub) {
                    pts.push({ time: ft as any, value: 0, color: rising });
                  }
                  const validPts = sanitizeHistogramSeriesData(
                    pts.filter((p) => isFinite(p.value) && p.value >= 0 && Number(p.time) > 0) as {
                      time: Time;
                      value: number;
                      color: string;
                    }[]
                  );
                  if (validPts.length > 0) {
                    volumeSeries.setData(validPts as any);
                  }
                  updatedCount++;
                  console.log(`✅ [Volume] setData() 완료 (일목 플레이스홀더 포함 ${pts.length}개)`);
                } else {
                  // ✅ volume 값 유효성 체크
                  if (isFinite(updatedCandle.volume) && updatedCandle.volume >= 0) {
                    const updateTime = seriesUpdateTime(volumeSeries);
                    if (updateTime !== null) {
                      volumeSeries.update({
                        time: updateTime,
                        value: updatedCandle.volume,
                        color: updatedCandle.close >= updatedCandle.open ? '#26A69A' : '#EF5350',
                      });
                      updatedCount++;
                      console.log(`✅ [Volume] update() 완료: ${updatedCandle.volume.toFixed(2)}`);
                    } else {
                      console.log(`⚠️ [Volume] 데이터 부족 - 업데이트 스킵`);
                    }
                  } else {
                    console.warn(`⚠️ [Volume] 유효하지 않은 값: ${updatedCandle.volume}`);
                  }
                }
              }
              
            if (lastIndicator) {
              // MACD 업데이트
              const macdSeries = subSeriesRef.current['macd'];
              if (macdSeries) {
                let macdUpdated = false;
                const macdSignalVal =
                  lastIndicator.macdSignal ??
                  lastIndicator.signalLine ??
                  (lastIndicator as { signal?: number }).signal;
                
                // MACD Line 업데이트
                if (macdSeries.macdLine && lastIndicator.macdLine !== undefined) {
                  const updateTime = seriesUpdateTime(macdSeries.macdLine);
                  if (updateTime !== null) {
                    macdSeries.macdLine.update({
                      time: updateTime,
                      value: lastIndicator.macdLine,
                    });
                    macdUpdated = true;
                    console.log(`✅ [MACD Line] update() 완료: ${lastIndicator.macdLine.toFixed(2)}`);
                  } else {
                    console.log(`⚠️ [MACD Line] 데이터 부족 - 업데이트 스킵`);
                  }
                }
                
                // Signal Line 업데이트 (REST는 signalLine, WS 병합은 macdSignal 위주)
                if (macdSeries.signalLine && macdSignalVal !== undefined) {
                  const updateTime = seriesUpdateTime(macdSeries.signalLine);
                  if (updateTime !== null) {
                    macdSeries.signalLine.update({
                      time: updateTime,
                      value: macdSignalVal,
                    });
                    macdUpdated = true;
                    console.log(`✅ [MACD Signal] update() 완료: ${macdSignalVal.toFixed(2)}`);
                  } else {
                    console.log(`⚠️ [MACD Signal] 데이터 부족 - 업데이트 스킵`);
                  }
                }
                
                // Histogram 업데이트 (REST DTO는 histogram, WS는 macd_histogram → macdHistogram)
                let histogramValue = lastIndicator.macdHistogram ?? lastIndicator.histogram;
                if (
                  histogramValue === undefined &&
                  lastIndicator.macdLine !== undefined &&
                  macdSignalVal !== undefined
                ) {
                  histogramValue = lastIndicator.macdLine - macdSignalVal;
                  console.log(`🔄 [MACD Histogram] 프론트엔드 계산: ${histogramValue.toFixed(2)}`);
                }
                
                // ✅ histogramValue 유효성 체크 추가
                if (macdSeries.histogram && histogramValue !== undefined && histogramValue !== null && isFinite(histogramValue)) {
                  const updateTime = seriesUpdateTime(macdSeries.histogram);
                  if (updateTime !== null) {
                    macdSeries.histogram.update({
                      time: updateTime,
                      value: histogramValue,
                      color: histogramValue >= 0 ? '#26A69A' : '#EF5350',
                    });
                    macdUpdated = true;
                    console.log(`✅ [MACD Histogram] update() 완료: ${histogramValue.toFixed(2)}`);
                  } else {
                    console.log(`⚠️ [MACD Histogram] 데이터 부족 - 업데이트 스킵`);
                  }
                } else if (macdSeries.histogram && histogramValue !== undefined) {
                  console.warn(`⚠️ [MACD Histogram] 유효하지 않은 값: ${histogramValue}`);
                }
                
                if (macdUpdated) {
                  updatedCount++;
                }
              }
              
              // RSI 업데이트 (ref: { main, overbought?, oversold?, signal? })
              const rsiEntry = subSeriesRef.current['rsi'];
              const rsiMain =
                rsiEntry && typeof rsiEntry === 'object' && 'main' in rsiEntry
                  ? (rsiEntry as { main: { update: (p: any) => void } }).main
                  : rsiEntry;
              if (
                rsiMain &&
                typeof (rsiMain as any).update === 'function' &&
                lastIndicator.rsi !== undefined &&
                lastIndicator.rsi !== null
              ) {
                const rv = Number(lastIndicator.rsi);
                if (Number.isFinite(rv)) {
                  const updateTime = seriesUpdateTime(rsiMain as any);
                  if (updateTime !== null) {
                    (rsiMain as any).update({ time: updateTime, value: rv });
                    const rsiExt = rsiEntry as {
                      overbought?: { update: (p: any) => void };
                      oversold?: { update: (p: any) => void };
                      signal?: { update: (p: any) => void };
                    };
                    if (rsiExt && typeof rsiExt === 'object') {
                      if (rsiExt.overbought && typeof rsiExt.overbought.update === 'function') {
                        rsiExt.overbought.update({ time: updateTime, value: rv });
                      }
                      if (rsiExt.oversold && typeof rsiExt.oversold.update === 'function') {
                        rsiExt.oversold.update({ time: updateTime, value: rv });
                      }
                      if (
                        rsiExt.signal &&
                        typeof rsiExt.signal.update === 'function' &&
                        effectiveIndicatorSettings?.rsiSignalEnabled
                      ) {
                        const sigP = effectiveIndicatorSettings.rsiSignalPeriod || 9;
                        let rsiSigVal: number | undefined =
                          (lastIndicator as any).rsiSignal != null
                            ? Number((lastIndicator as any).rsiSignal)
                            : undefined;
                        if (rsiSigVal === undefined || !Number.isFinite(rsiSigVal)) {
                          if (lastIdx >= sigP - 1) {
                            let sum = 0;
                            let ok = true;
                            for (let j = 0; j < sigP; j++) {
                              const r = allIndicatorData[lastIdx - j]?.rsi;
                              if (r === undefined || r === null || !Number.isFinite(Number(r))) {
                                ok = false;
                                break;
                              }
                              sum += Number(r);
                            }
                            if (ok) rsiSigVal = sum / sigP;
                          }
                        }
                        if (rsiSigVal !== undefined && Number.isFinite(rsiSigVal)) {
                          rsiExt.signal.update({ time: seriesUpdateTime(rsiExt.signal as any) ?? updateTime, value: rsiSigVal });
                        }
                      }
                    }
                    updatedCount++;
                    console.log(`✅ [RSI] update() 완료: ${rv.toFixed(2)}`);
                  } else {
                    console.log(`⚠️ [RSI] 데이터 부족 - 업데이트 스킵`);
                  }
                }
              }
              
              // Stochastic 업데이트
              const stochSeries = subSeriesRef.current['stochastic'];
              if (stochSeries) {
                let stochUpdated = false;
                const stochKLive = lastIndicator.stochasticK ?? lastIndicator.stochK;
                const stochDLive = lastIndicator.stochasticD ?? lastIndicator.stochD;
                if (stochSeries.kLine && stochKLive !== undefined && stochKLive !== null) {
                  const updateTime = seriesUpdateTime(stochSeries.kLine);
                  if (updateTime !== null) {
                    stochSeries.kLine.update({
                      time: updateTime,
                      value: stochKLive as number,
                    });
                    stochUpdated = true;
                    console.log(`✅ [Stochastic %K] update() 완료: ${Number(stochKLive).toFixed(2)}`);
                  } else {
                    console.log(`⚠️ [Stochastic %K] 데이터 부족 - 업데이트 스킵`);
                  }
                }
                if (stochSeries.dLine && stochDLive !== undefined && stochDLive !== null) {
                  const updateTime = seriesUpdateTime(stochSeries.dLine);
                  if (updateTime !== null) {
                    stochSeries.dLine.update({
                      time: updateTime,
                      value: stochDLive as number,
                    });
                    stochUpdated = true;
                    console.log(`✅ [Stochastic %D] update() 완료: ${Number(stochDLive).toFixed(2)}`);
                  } else {
                    console.log(`⚠️ [Stochastic %D] 데이터 부족 - 업데이트 스킵`);
                  }
                }
                if (stochUpdated) {
                  updatedCount++;
                }
              }
              
              // Stochastic Slow 업데이트
              const stochSlowSeries = subSeriesRef.current['stochasticSlow'];
              if (stochSlowSeries) {
                let stochSlowUpdated = false;
                const slowKLive = (lastIndicator as any).stochasticSlowK ?? (lastIndicator as any).slowK;
                const slowDLive = (lastIndicator as any).stochasticSlowD ?? (lastIndicator as any).slowD;
                if (stochSlowSeries.kLine && slowKLive !== undefined && slowKLive !== null) {
                  if (safeUpdate(stochSlowSeries.kLine, slowKLive as number, 'Stochastic Slow %K')) {
                    stochSlowUpdated = true;
                    console.log(`✅ [Stochastic Slow %K] update() 완료: ${Number(slowKLive).toFixed(2)}`);
                  }
                }
                if (stochSlowSeries.dLine && slowDLive !== undefined && slowDLive !== null) {
                  if (safeUpdate(stochSlowSeries.dLine, slowDLive as number, 'Stochastic Slow %D')) {
                    stochSlowUpdated = true;
                    console.log(`✅ [Stochastic Slow %D] update() 완료: ${Number(slowDLive).toFixed(2)}`);
                  }
                }
                if (stochSlowUpdated) {
                  updatedCount++;
                }
              }
              
              // CCI 업데이트 (ref: { main, overbought?, oversold?, signal? } — 메인과 동일 값으로 영역 시리즈 동기화)
              const cciEntry = subSeriesRef.current['cci'];
              const cciMain =
                cciEntry && typeof cciEntry === 'object' && 'main' in cciEntry
                  ? (cciEntry as { main: { update: (p: any) => void } }).main
                  : cciEntry;
              const cciVal =
                lastIndicator.cci !== undefined && lastIndicator.cci !== null
                  ? lastIndicator.cci
                  : (lastIndicator as any).cci13;
              if (
                cciMain &&
                typeof (cciMain as any).update === 'function' &&
                cciVal !== undefined &&
                cciVal !== null &&
                Number.isFinite(Number(cciVal))
              ) {
                const v = Number(cciVal);
                if (safeUpdate(cciMain, v, 'CCI')) {
                  const ext = cciEntry as {
                    overbought?: { update: (p: any) => void };
                    oversold?: { update: (p: any) => void };
                    signal?: { update: (p: any) => void };
                  };
                  if (ext && typeof ext === 'object') {
                    if (ext.overbought && typeof ext.overbought.update === 'function') {
                      safeUpdate(ext.overbought, v, 'CCI Overbought');
                    }
                    if (ext.oversold && typeof ext.oversold.update === 'function') {
                      safeUpdate(ext.oversold, v, 'CCI Oversold');
                    }
                    const sigRaw = lastIndicator.cciSignal;
                    if (
                      ext.signal &&
                      typeof ext.signal.update === 'function' &&
                      sigRaw !== undefined &&
                      sigRaw !== null &&
                      Number.isFinite(Number(sigRaw))
                    ) {
                      safeUpdate(ext.signal, Number(sigRaw), 'CCI Signal');
                    }
                  }
                  updatedCount++;
                  console.log(`✅ [CCI] update() 완료: ${v.toFixed(2)}`);
                }
              }
              
              // ADX 업데이트
              const adxSeries = subSeriesRef.current['adx'];
              if (adxSeries && lastIndicator.adx !== undefined) {
                if (safeUpdate(adxSeries, lastIndicator.adx, 'ADX')) {
                  updatedCount++;
                  console.log(`✅ [ADX] update() 완료: ${lastIndicator.adx.toFixed(2)}`);
                }
              }
              
              // OBV 업데이트 (시그널 라인이 있으면 ref가 { main, signal })
              try {
                const obvEntry = subSeriesRef.current['obv'];
                if (obvEntry && lastIndicator.obv !== undefined && Number.isFinite(lastIndicator.obv)) {
                  const obvMain =
                    obvEntry && typeof obvEntry === 'object' && 'main' in obvEntry ? obvEntry.main : obvEntry;
                  if (safeUpdate(obvMain, lastIndicator.obv, 'OBV')) {
                    updatedCount++;
                    console.log(`✅ [OBV] update() 완료: ${lastIndicator.obv.toFixed(0)}`);
                  }
                  if (
                    obvEntry &&
                    typeof obvEntry === 'object' &&
                    obvEntry.signal &&
                    lastIndicator.obvSignal !== undefined &&
                    lastIndicator.obvSignal !== null &&
                    Number.isFinite(Number(lastIndicator.obvSignal))
                  ) {
                    safeUpdate(obvEntry.signal, Number(lastIndicator.obvSignal), 'OBV Signal');
                    console.log(`✅ [OBV Signal] update() 완료: ${Number(lastIndicator.obvSignal).toFixed(4)}`);
                  }
                }
              } catch (obvErr) {
                console.error('❌ [OBV] 실시간 업데이트 오류:', obvErr);
              }
              
              // DMI 업데이트
              try {
                const dmiSeries = subSeriesRef.current['dmi'];
                if (dmiSeries) {
                  let dmiUpdated = false;
                  if (dmiSeries.plusDI && lastIndicator.plusDI !== undefined && Number.isFinite(lastIndicator.plusDI)) {
                    if (safeUpdate(dmiSeries.plusDI, lastIndicator.plusDI, 'DMI +DI')) {
                      dmiUpdated = true;
                      console.log(`✅ [DMI +DI] update() 완료: ${lastIndicator.plusDI.toFixed(2)}`);
                    }
                  }
                  if (dmiSeries.minusDI && lastIndicator.minusDI !== undefined && Number.isFinite(lastIndicator.minusDI)) {
                    if (safeUpdate(dmiSeries.minusDI, lastIndicator.minusDI, 'DMI -DI')) {
                      dmiUpdated = true;
                      console.log(`✅ [DMI -DI] update() 완료: ${lastIndicator.minusDI.toFixed(2)}`);
                    }
                  }
                  if (dmiUpdated) {
                    updatedCount++;
                  }
                }
              } catch (dmiErr) {
                console.error('❌ [DMI] 실시간 업데이트 오류:', dmiErr);
              }
              
              // Williams %R 업데이트
              try {
                const williamsRSeries = subSeriesRef.current['williamsR'];
                if (williamsRSeries && lastIndicator.williamsR !== undefined && Number.isFinite(lastIndicator.williamsR)) {
                  if (safeUpdate(williamsRSeries, lastIndicator.williamsR, 'Williams %R')) {
                    updatedCount++;
                    console.log(`✅ [Williams %R] update() 완료: ${lastIndicator.williamsR.toFixed(2)}`);
                  }
                }
              } catch (wrErr) {
                console.error('❌ [Williams %R] 실시간 업데이트 오류:', wrErr);
              }
              
              // Momentum 업데이트
              try {
                const momentumSeries = subSeriesRef.current['momentum'];
                if (momentumSeries && lastIndicator.momentum !== undefined && Number.isFinite(lastIndicator.momentum)) {
                  if (safeUpdate(momentumSeries, lastIndicator.momentum, 'Momentum')) {
                    updatedCount++;
                    console.log(`✅ [Momentum] update() 완료: ${lastIndicator.momentum.toFixed(2)}`);
                  }
                }
              } catch (momErr) {
                console.error('❌ [Momentum] 실시간 업데이트 오류:', momErr);
              }
              
              // TRIX 업데이트
              try {
                const trixSeriesData = subSeriesRef.current['trix'];
                if (trixSeriesData) {
                  let trixUpdated = false;
                  if (trixSeriesData.trix && lastIndicator.trix !== undefined && Number.isFinite(lastIndicator.trix)) {
                    if (safeUpdate(trixSeriesData.trix, lastIndicator.trix, 'TRIX')) {
                      trixUpdated = true;
                      console.log(`✅ [TRIX] update() 완료: ${lastIndicator.trix.toFixed(4)}`);
                    }
                  }
                  if (trixSeriesData.signal && lastIndicator.trixSignal !== undefined && Number.isFinite(lastIndicator.trixSignal)) {
                    if (safeUpdate(trixSeriesData.signal, lastIndicator.trixSignal, 'TRIX Signal')) {
                      trixUpdated = true;
                      console.log(`✅ [TRIX Signal] update() 완료: ${lastIndicator.trixSignal.toFixed(4)}`);
                    }
                  }
                  if (trixUpdated) {
                    updatedCount++;
                  }
                }
              } catch (trixErr) {
                console.error('❌ [TRIX] 실시간 업데이트 오류:', trixErr);
              }
              
              // VR 업데이트
              try {
                const vrSeriesData = subSeriesRef.current['vr'];
                if (vrSeriesData) {
                  let vrUpdated = false;
                  if (vrSeriesData.vr && lastIndicator.vr !== undefined && Number.isFinite(lastIndicator.vr)) {
                    if (safeUpdate(vrSeriesData.vr, lastIndicator.vr, 'VR')) {
                      vrUpdated = true;
                      console.log(`✅ [VR] update() 완료: ${lastIndicator.vr.toFixed(2)}`);
                    }
                  }
                  if (vrSeriesData.vr2 && lastIndicator.vr2 !== undefined && Number.isFinite(lastIndicator.vr2)) {
                    if (safeUpdate(vrSeriesData.vr2, lastIndicator.vr2, 'VR2')) {
                      vrUpdated = true;
                      console.log(`✅ [VR2] update() 완료: ${lastIndicator.vr2.toFixed(2)}`);
                    }
                  }
                  if (vrUpdated) {
                    updatedCount++;
                  }
                }
              } catch (vrErr) {
                console.error('❌ [VR] 실시간 업데이트 오류:', vrErr);
              }
              
              // BWI 업데이트
              try {
                const bwiSeries = subSeriesRef.current['bwi'];
                if (bwiSeries && lastIndicator.bwi !== undefined && Number.isFinite(lastIndicator.bwi)) {
                  if (safeUpdate(bwiSeries, lastIndicator.bwi, 'BWI')) {
                    updatedCount++;
                    console.log(`✅ [BWI] update() 완료: ${lastIndicator.bwi.toFixed(2)}`);
                  }
                }
              } catch (bwiErr) {
                console.error('❌ [BWI] 실시간 업데이트 오류:', bwiErr);
              }
              
              // Volume OSC 업데이트
              try {
                const volumeOscSeries = subSeriesRef.current['volumeOsc'];
                if (volumeOscSeries && lastIndicator.volumeOsc !== undefined && Number.isFinite(lastIndicator.volumeOsc)) {
                  if (safeUpdate(volumeOscSeries, lastIndicator.volumeOsc, 'Volume OSC')) {
                    updatedCount++;
                    console.log(`✅ [Volume OSC] update() 완료: ${lastIndicator.volumeOsc.toFixed(2)}`);
                  }
                }
              } catch (volOscErr) {
                console.error('❌ [Volume OSC] 실시간 업데이트 오류:', volOscErr);
              }
              
              // New Psychological 업데이트 (서브차트 생성 시 ref가 { newPsy } 형태)
              try {
                const newPsyEntry = subSeriesRef.current['newPsychological'];
                const newPsySeries =
                  newPsyEntry && typeof newPsyEntry === 'object' && 'newPsy' in newPsyEntry
                    ? (newPsyEntry as { newPsy: { update: (p: any) => void } }).newPsy
                    : newPsyEntry;
                const newPsyValue = lastIndicator.newPsychological ?? lastIndicator.newPsy;
                if (
                  newPsySeries &&
                  typeof (newPsySeries as any).update === 'function' &&
                  newPsyValue !== undefined &&
                  Number.isFinite(newPsyValue)
                ) {
                  if (safeUpdate(newPsySeries as any, newPsyValue, 'New Psychological')) {
                    updatedCount++;
                    console.log(`✅ [New Psychological] update() 완료: ${newPsyValue.toFixed(2)}`);
                  }
                }
              } catch (newPsyErr) {
                console.error('❌ [New Psychological] 실시간 업데이트 오류:', newPsyErr);
              }
              
              // Invest Psychological 업데이트 (서브차트 생성 시 ref가 { investPsy } 형태)
              try {
                const investPsyEntry = subSeriesRef.current['investPsychological'];
                const investPsySeries =
                  investPsyEntry && typeof investPsyEntry === 'object' && 'investPsy' in investPsyEntry
                    ? (investPsyEntry as { investPsy: { update: (p: any) => void } }).investPsy
                    : investPsyEntry;
                const investPsyValue = lastIndicator.investPsychological ?? lastIndicator.investPsy;
                if (
                  investPsySeries &&
                  typeof (investPsySeries as any).update === 'function' &&
                  investPsyValue !== undefined &&
                  Number.isFinite(investPsyValue)
                ) {
                  if (safeUpdate(investPsySeries as any, investPsyValue, 'Invest Psychological')) {
                    updatedCount++;
                    console.log(`✅ [Invest Psychological] update() 완료: ${investPsyValue.toFixed(2)}`);
                  }
                }
              } catch (investPsyErr) {
                console.error('❌ [Invest Psychological] 실시간 업데이트 오류:', investPsyErr);
              }
              
              // Disparity 업데이트
              try {
                const disparitySeriesData = subSeriesRef.current['disparity'];
                if (disparitySeriesData) {
                  let disparityUpdated = false;
                  const DISPARITY_OFFSET = 95;
                  
                  const dUS = (lastIndicator as any).disparityUltraShort;
                  if (disparitySeriesData.ultraShort && dUS !== undefined && Number.isFinite(dUS)) {
                    if (safeUpdate(disparitySeriesData.ultraShort, dUS - DISPARITY_OFFSET, 'Disparity UltraShort')) {
                      disparityUpdated = true;
                      console.log(`✅ [Disparity UltraShort] update() 완료: ${dUS.toFixed(2)}`);
                    }
                  }
                  const dS = (lastIndicator as any).disparityShort;
                  if (disparitySeriesData.short && dS !== undefined && Number.isFinite(dS)) {
                    if (safeUpdate(disparitySeriesData.short, dS - DISPARITY_OFFSET, 'Disparity Short')) {
                      disparityUpdated = true;
                      console.log(`✅ [Disparity Short] update() 완료: ${dS.toFixed(2)}`);
                    }
                  }
                  const dM = (lastIndicator as any).disparityMid;
                  if (disparitySeriesData.mid && dM !== undefined && Number.isFinite(dM)) {
                    if (safeUpdate(disparitySeriesData.mid, dM - DISPARITY_OFFSET, 'Disparity Mid')) {
                      disparityUpdated = true;
                      console.log(`✅ [Disparity Mid] update() 완료: ${dM.toFixed(2)}`);
                    }
                  }
                  const dL = (lastIndicator as any).disparityLong;
                  if (disparitySeriesData.long && dL !== undefined && Number.isFinite(dL)) {
                    if (safeUpdate(disparitySeriesData.long, dL - DISPARITY_OFFSET, 'Disparity Long')) {
                      disparityUpdated = true;
                      console.log(`✅ [Disparity Long] update() 완료: ${dL.toFixed(2)}`);
                    }
                  }
                  if (disparityUpdated) {
                    updatedCount++;
                  }
                }
              } catch (dispErr) {
                console.error('❌ [Disparity] 실시간 업데이트 오류:', dispErr);
              }
            }

            console.log(`✅ [서브패널 실시간 업데이트] ${updatedCount}개 지표 차트 업데이트 완료`);
            console.log(`🔍 [subSeriesRef 상태] 등록된 지표:`, Object.keys(subSeriesRef.current));
          }

          console.log(`✅ [updateLastIndicators] 실시간 지표 업데이트 완료`);
        } catch (error) {
          console.error(`❌ [updateLastIndicators] 오류:`, error);
        }
      },
    }));

    // 차트 준비 완료 상태 추적
    const [chartReady, setChartReady] = useState(false);

    // ✅ 차트가 준비되면 pending 크로스헤어 자동 적용
    useEffect(() => {
      if (!chartReady || !chartRef.current || !candleSeriesRef.current || !pendingCrosshairRef.current || data.length === 0) {
        return;
      }

      const timeToApply = pendingCrosshairRef.current;
      const timeStr = typeof timeToApply === 'number' ? new Date(timeToApply * 1000).toISOString().slice(11, 19) : new Date(timeToApply as string).toISOString().slice(11, 19);
      console.log(`🔄 [CandlestickChart] 차트 준비 완료 - pending 크로스헤어 적용 시도: ${timeStr}`);

      try {
        const targetTimeInSeconds = typeof timeToApply === 'number' ? timeToApply : Math.floor(new Date(timeToApply as string).getTime() / 1000);
        let closestDataPoint: CandleData | null = null;
        let minTimeDiff = Infinity;

        for (const d of data) {
          const dataTime = Math.floor(new Date(d.time).getTime() / 1000);
          const timeDiff = Math.abs((targetTimeInSeconds as number) - dataTime);
          if (timeDiff < minTimeDiff) {
            minTimeDiff = timeDiff;
            closestDataPoint = d;
          }
        }

        if (closestDataPoint && chartRef.current && candleSeriesRef.current) {
          isSettingExternalCrosshairRef.current = true;
          chartRef.current.setCrosshairPosition(closestDataPoint.close, targetTimeInSeconds as Time, candleSeriesRef.current);
          console.log(`✅ [CandlestickChart] pending 크로스헤어 적용 성공: time=${new Date((targetTimeInSeconds as number) * 1000).toISOString().slice(11, 19)}`);
          setTimeout(() => {
            isSettingExternalCrosshairRef.current = false;
          }, 16);
        }
      } catch (error) {
        console.debug('[CandlestickChart] pending 크로스헤어 적용 실패 (무시):', error);
      } finally {
        pendingCrosshairRef.current = null;
      }
    }, [chartReady, data]);

    // ✅ 캔들차트에서 마우스 휠 스크롤 시 차트 좌우 이동
    // 단, y축 라벨 영역에서는 페이지 스크롤 허용
    useEffect(() => {
      const container = chartContainerRef.current;
      if (!container) return;

      let lastScrollTime = 0;
      const SCROLL_DEBOUNCE_MS = 50; // 50ms 디바운싱
      const MIN_DELTA_THRESHOLD = 15; // 최소 휠 이동량 (민감도 낮춤)

      const handleWheel = (e: WheelEvent) => {
        // 마우스 위치 확인 (y축 라벨 영역 감지)
        const rect = container.getBoundingClientRect();
        const mouseX = e.clientX - rect.left;
        const containerWidth = rect.width;
        
        // y축 라벨은 우측 80px 영역에 위치
        const Y_AXIS_WIDTH = 80;
        const isOnYAxis = mouseX > containerWidth - Y_AXIS_WIDTH;
        
        if (isOnYAxis) {
          // y축 라벨 영역: Y축 스케일 조정
          e.preventDefault();
          e.stopPropagation();
          
          // deltaY < 0: 스크롤 업 (위로) -> Y축 간격 늘림 (축소)
          // deltaY > 0: 스크롤 다운 (아래로) -> Y축 간격 줄임 (확대)
          const zoomDelta = e.deltaY < 0 ? 0.1 : -0.1; // 10% 단위 조정
          setYAxisScaleFactor(prev => {
            const newFactor = prev + zoomDelta;
            // 스케일 팩터 범위 제한: 0.3 (최대 축소) ~ 3.0 (최대 확대)
            const clampedFactor = Math.max(0.3, Math.min(3.0, newFactor));
            console.log(`📏 [Y축 스케일] 팩터 변경: ${prev.toFixed(2)} → ${clampedFactor.toFixed(2)}`);
            return clampedFactor;
          });
          return;
        }
        
        // ✅ 작은 휠 이동 무시 (민감도 낮춤)
        if (Math.abs(e.deltaY) < MIN_DELTA_THRESHOLD) {
          return;
        }
        
        // ✅ 디바운싱: 마지막 스크롤 이후 일정 시간 경과 확인
        const now = Date.now();
        if (now - lastScrollTime < SCROLL_DEBOUNCE_MS) {
          return;
        }
        lastScrollTime = now;
        
        // 차트 영역: 좌우 이동
        e.preventDefault();
        e.stopPropagation();

        // deltaY < 0: 스크롤 업 (위로) -> 좌측으로 이동 (과거 데이터)
        // deltaY > 0: 스크롤 다운 (아래로) -> 우측으로 이동 (최신 데이터)
        if (e.deltaY < 0) {
          handleScrollLeft();
        } else if (e.deltaY > 0) {
          handleScrollRight();
        }
      };

      // passive: false로 설정하여 preventDefault가 동작하도록 함
      container.addEventListener('wheel', handleWheel, { passive: false });

      return () => {
        container.removeEventListener('wheel', handleWheel);
      };
    }, [handleScrollLeft, handleScrollRight]);

    // ✅ 보조지표 차트에서 마우스 휠 스크롤 시 차트 좌우 이동
    // 단, y축 라벨 영역에서는 페이지 스크롤 허용
    useEffect(() => {
      let lastScrollTime = 0;
      const SCROLL_DEBOUNCE_MS = 50; // 50ms 디바운싱
      const MIN_DELTA_THRESHOLD = 15; // 최소 휠 이동량 (민감도 낮춤)

      const handleWheelForIndicator = (panelType: string) => (e: WheelEvent) => {
        const target = e.currentTarget as HTMLElement;
        const rect = target.getBoundingClientRect();
        const mouseX = e.clientX - rect.left;
        const containerWidth = rect.width;
        
        // y축 라벨은 우측 80px 영역에 위치
        const Y_AXIS_WIDTH = 80;
        const isOnYAxis = mouseX > containerWidth - Y_AXIS_WIDTH;
        
        if (isOnYAxis) {
          // y축 라벨 영역: Y축 스케일 조정
          e.preventDefault();
          e.stopPropagation();
          
          // deltaY < 0: 스크롤 업 (위로) -> Y축 간격 늘림 (축소)
          // deltaY > 0: 스크롤 다운 (아래로) -> Y축 간격 줄임 (확대)
          const zoomDelta = e.deltaY < 0 ? 0.1 : -0.1; // 10% 단위 조정
          setYAxisScaleFactor(prev => {
            const newFactor = prev + zoomDelta;
            // 스케일 팩터 범위 제한: 0.3 (최대 축소) ~ 3.0 (최대 확대)
            const clampedFactor = Math.max(0.3, Math.min(3.0, newFactor));
            console.log(`📏 [Y축 스케일 - ${panelType}] 팩터 변경: ${prev.toFixed(2)} → ${clampedFactor.toFixed(2)}`);
            return clampedFactor;
          });
          return;
        }
        
        // ✅ 작은 휠 이동 무시 (민감도 낮춤)
        if (Math.abs(e.deltaY) < MIN_DELTA_THRESHOLD) {
          return;
        }
        
        // ✅ 디바운싱: 마지막 스크롤 이후 일정 시간 경과 확인
        const now = Date.now();
        if (now - lastScrollTime < SCROLL_DEBOUNCE_MS) {
          return;
        }
        lastScrollTime = now;
        
        // 차트 영역: 좌우 이동
        e.preventDefault();
        e.stopPropagation();

        if (e.deltaY < 0) {
          handleScrollLeft();
        } else if (e.deltaY > 0) {
          handleScrollRight();
        }
      };

      // 모든 보조지표 컨테이너에 이벤트 리스너 추가
      const containers = Object.entries(subChartContainersRef.current).filter(([_, c]) => c) as [string, HTMLDivElement][];
      containers.forEach(([panelType, container]) => {
        container.addEventListener('wheel', handleWheelForIndicator(panelType), { passive: false });
      });

      return () => {
        containers.forEach(([panelType, container]) => {
          container.removeEventListener('wheel', handleWheelForIndicator(panelType));
        });
      };
    }, [handleScrollLeft, handleScrollRight, subPanels]);

    // 외부 호버 시간 동기화 (보조지표 → 캔들차트) - setCrosshairPosition API 사용
    useEffect(() => {
      const chart = chartRef.current;
      const series = candleSeriesRef.current;

      // externalHoverTime이 null이면 크로스헤어 위치 초기화
      if (!externalHoverTime || data.length === 0) {
        if (chart) {
          chart.clearCrosshairPosition();
        }
        setExternalCrosshair(null);
        return;
      }

      // ✅ 마우스가 차트 위에 있어도 외부 크로스헤어 동기화 허용 (다른 차트와의 동기화를 위해)

      // ✅ 차트가 아직 준비되지 않았으면 대기 큐에 저장하고 리턴
      if (!isChartReadyRef.current) {
        pendingCrosshairRef.current = externalHoverTime;
        const timeStr = typeof externalHoverTime === 'number' ? new Date(externalHoverTime * 1000).toISOString().slice(11, 19) : new Date(externalHoverTime as string).toISOString().slice(11, 19);
        console.log(`⏳ [CandlestickChart] 차트 준비 중 - 크로스헤어 대기: ${timeStr}`);
        return;
      }

      // 차트가 준비될 때까지 대기
      if (!chart || !series) {
        return;
      }

      // ✅ 이미 설정 중이면 무시 (떨림 방지)
      if (isSettingExternalCrosshairRef.current) {
        return;
      }

      try {
        // ✅ 원본 externalHoverTime을 Unix timestamp (초 단위)로 변환
        const targetTimeInSeconds = typeof externalHoverTime === 'number' 
          ? externalHoverTime 
          : Math.floor(new Date(externalHoverTime as string).getTime() / 1000);

        // ✅ 데이터 범위 확인 (데이터 범위 밖이면 조용히 무시)
        if (data.length === 0) {
          return;
        }

        const firstDataTime = Math.floor(new Date(data[0].time).getTime() / 1000);
        const lastDataTime = Math.floor(new Date(data[data.length - 1].time).getTime() / 1000);

        // ✅ 요청된 시간이 데이터 범위 밖이면 조용히 무시 (에러 없이)
        if (targetTimeInSeconds < firstDataTime || targetTimeInSeconds > lastDataTime) {
          return;
        }

        // ✅ 가장 가까운 데이터 포인트 찾기 (정확한 시간 매칭)
        let closestDataPoint: CandleData | null = null;
        let minTimeDiff = Infinity;

        for (const d of data) {
          try {
            const dataTimeInSeconds = Math.floor(new Date(d.time).getTime() / 1000);
            const timeDiff = Math.abs(dataTimeInSeconds - targetTimeInSeconds);

            // 1분(60초) 이내에서 가장 가까운 포인트 찾기
            if (timeDiff < 60 && timeDiff < minTimeDiff) {
              minTimeDiff = timeDiff;
              closestDataPoint = d;
            }
          } catch {
            continue;
          }
        }

        if (closestDataPoint) {
          // ★ 외부 크로스헤어 설정 중 플래그 설정 (subscribeCrosshairMove 이벤트 무시용)
          isSettingExternalCrosshairRef.current = true;

          // ✅ 핵심 수정: 원본 externalHoverTime의 시간 사용 (데이터포인트 시간이 아닌)
          // 이렇게 해야 모든 차트가 정확히 같은 X축 위치에 크로스헤어 표시
          chart.setCrosshairPosition(closestDataPoint.close, targetTimeInSeconds as Time, series);

          // ✅ 빠른 반응을 위해 짧은 대기 시간 (1프레임)
          setTimeout(() => {
            isSettingExternalCrosshairRef.current = false;
          }, 16);

          // 오버레이는 제거
          setExternalCrosshair(null);
        } else {
          chart.clearCrosshairPosition();
          setExternalCrosshair(null);
        }
      } catch (e) {
        // 오류 발생 시 크로스헤어 초기화
        if (chart) {
          chart.clearCrosshairPosition();
        }
        setExternalCrosshair(null);
      }
    }, [externalHoverTime, chartReady, isMouseOverChart]); // ✅ chartReady 추가 - 차트 준비 완료 시 현재 externalHoverTime 재적용

    useEffect(() => {
      if (!chartContainerRef.current || data.length === 0) return;

      // ✅ 선행스팬만 활성화 시 컨테이너 0 크기로 차트 미렌더링 방지 (차트 제거 전 체크)
      const cw = chartContainerRef.current.clientWidth;
      const ch = chartContainerRef.current.clientHeight;
      // clientHeight가 0이어도 height prop이 있으면 차트 생성 (레이아웃 타이밍 이슈 시 차트 사라짐 방지)
      const effectiveHeight = (ch > 0 ? ch : height) || height;
      if (cw <= 0 || effectiveHeight <= 0) {
        console.warn(`⏸️ [CandlestickChart] 컨테이너 크기 0 - 차트 생성 건너뜀 (width=${cw}, height=${effectiveHeight}), 100ms 후 재시도`);
        const retryTimer = setTimeout(() => setForceUpdateCounter((c) => c + 1), 100);
        return () => clearTimeout(retryTimer);
      }

      // ✅ 리사이즈 중에는 메인 차트도 재생성 중지
      if (isResizingRef.current || isDraggingRef.current) {
        console.log(`⏸️ [메인차트] 리사이즈/드래그 중 - 차트 재생성 건너뜀`);
        return;
      }

      // ✅ 기존 차트가 있으면 먼저 제거
      if (chartRef.current) {
        console.log(`🗑️ [메인차트] 기존 차트 제거 후 재생성`);
        try {
          // ✅ 재생성 직전 현재 뷰(논리 범위 + scrollPosition) 저장
          try {
            const currentLogicalRange = chartRef.current.timeScale().getVisibleLogicalRange();
            const currentScrollPosition = chartRef.current.timeScale().scrollPosition();
            if (
              currentLogicalRange &&
              Number.isFinite(currentLogicalRange.from) &&
              Number.isFinite(currentLogicalRange.to) &&
              logicalBarsVisible(currentLogicalRange) >= MIN_LOGICAL_BARS_TO_PERSIST
            ) {
              preservedViewRangeRef.current = {
                from: currentLogicalRange.from,
                to: currentLogicalRange.to,
                scrollPosition: Number.isFinite(currentScrollPosition) ? currentScrollPosition : 0,
              };
              console.log(
                `💾 [재생성 전 뷰 저장] logical: ${currentLogicalRange.from.toFixed(2)} ~ ${currentLogicalRange.to.toFixed(2)}, scroll: ${currentScrollPosition.toFixed(2)}`
              );
            } else if (currentLogicalRange && logicalBarsVisible(currentLogicalRange) < MIN_LOGICAL_BARS_TO_PERSIST) {
              preservedViewRangeRef.current = null;
              console.log(
                `⏸️ [재생성 전 뷰 저장 스킵] 논리봉 ${logicalBarsVisible(currentLogicalRange).toFixed(1)}개 < ${MIN_LOGICAL_BARS_TO_PERSIST} (극단 줌 복원 방지)`
              );
            }
          } catch (e) {
            console.warn('⚠️ [재생성 전 뷰 저장 실패]:', e);
          }

          // disposing 플래그 설정
          isChartDisposingRef.current = true;
          
          // fitContent timeout 취소
          fitContentTimeoutsRef.current.forEach(timeoutId => {
            window.clearTimeout(timeoutId);
          });
          fitContentTimeoutsRef.current = [];
          
          // 차트 제거
          chartRef.current.remove();
          chartRef.current = null;
          candleSeriesRef.current = null;
          
          // ✅ 실시간 업데이트용 series refs 초기화
          maSeriesMapRef.current.clear();
          bollingerSeriesRef.current = {};
          
          // 플래그 초기화
          isChartReadyRef.current = false;
          setChartReady(false);
          pendingCrosshairRef.current = null;
        } catch (e) {
          console.warn(`⚠️ [메인차트] 기존 차트 제거 실패:`, e);
        }
      }

      // ✅ 차트 재생성 시작 - 현재 externalHoverTime을 대기 큐에 저장하고 준비 플래그 false로 설정
      if (externalHoverTime && !pendingCrosshairRef.current) {
        pendingCrosshairRef.current = externalHoverTime;
        const timeStr = typeof externalHoverTime === 'number' ? new Date(externalHoverTime * 1000).toISOString().slice(11, 19) : new Date(externalHoverTime as string).toISOString().slice(11, 19);
        console.log(`⏳ [CandlestickChart] 차트 재생성 시작 - 크로스헤어 대기 큐 저장: ${timeStr}`);
      }
      isChartReadyRef.current = false;
      setChartReady(false); // ✅ state도 업데이트
      isChartDisposingRef.current = false; // ✅ 차트 재생성 시작 - disposing 플래그 해제

      // 중복 제거 - data prop 사용 (멀티차트는 ChartItem에서 data={maCalculationData||chartData} 전달)
      const uniqueData: CandleData[] = [];
      const timeSet = new Set<string>();

      // ✅ data prop 샘플 로그
      if (data.length > 0) {
        const firstData = data[0];
        const lastData = data[data.length - 1];
        console.log('[CandlestickChart] data prop 샘플 (첫 번째):', {
          time: firstData.time,
          timeType: typeof firstData.time,
          timeJSON: JSON.stringify(firstData.time),
        });
        console.log('[CandlestickChart] data prop 샘플 (마지막):', {
          time: lastData.time,
          timeType: typeof lastData.time,
          timeJSON: JSON.stringify(lastData.time),
        });
      }

      for (const item of data) {
        const t = item.time != null ? String(item.time) : '';
        if (t && !timeSet.has(t)) {
          // ✅ CRITICAL: uniqueData에 추가하기 전에 time이 객체가 아닌지 확인
          const timeValue = item.time as any;
          if (typeof timeValue === 'object' && timeValue !== null) {
            const isDate = timeValue instanceof Date || (typeof timeValue.getTime === 'function');
            if (!isDate) {
              console.error('[CandlestickChart] ❌ data prop에 객체 형식 time 발견:', {
                time: timeValue,
                timeType: typeof timeValue,
                timeConstructor: timeValue.constructor?.name,
                timeKeys: Object.keys(timeValue),
              });
            }
          }
          uniqueData.push(item);
          timeSet.add(t);
        }
      }

      console.log(`🔍 [CandlestickChart] data.length=${data.length}, uniqueData.length=${uniqueData.length}`);

      // ✅ useEffect 재실행 시 dispose 플래그 리셋
      isDisposedRef.current = false;

      // ✅ 컨테이너 크기 및 DPR 초기화
      const containerWidth = chartContainerRef.current.clientWidth;
      const containerHeight = chartContainerRef.current.clientHeight;
      const currentDPR = window.devicePixelRatio || 1;
      
      containerSizeRef.current = { width: containerWidth, height: containerHeight };
      devicePixelRatioRef.current = currentDPR;

      console.log(`🎨 [CandlestickChart] 차트 생성`, { width: containerWidth, height: containerHeight, dpr: currentDPR });

      // 차트 생성
      // ✅ 일목 구름 표시 시: 차트 배경을 transparent로 해서 구름 오버레이가 보이도록 함 (투자분석/멀티차트 공통)
      const hasIchimokuCloud = ichimokuSettings?.senkouA && ichimokuSettings?.senkouB;
      const chart = createChart(chartContainerRef.current, {
        autoSize: true,  // ✅ 서브차트 및 IndicatorChart와 완전히 일치 (자동 크기 조정)
        layout: {
          background: { type: ColorType.Solid, color: hasIchimokuCloud ? 'transparent' : chartColors.background },
          textColor: chartColors.textColor,
          attributionLogo: false,
        },
        width: chartContainerRef.current.clientWidth,
        height: effectiveHeight,
        // 가격 포맷터 - 원화 표시 (간결한 형식)
        localization: {
          locale: 'en-US',  // 기본 로케일을 영어로 고정하여 tickMarkFormatter가 일관되게 작동하도록 함
          priceFormatter: (price: number) => {
            // 가격대별 소수점 자릿수 결정
            if (price >= 1000) {
              // 1,000원 이상: 정수로 표시
              return Math.round(price).toLocaleString('en-US');
            } else if (price >= 100) {
              // 100~999원: 소수점 2자리
              return price.toFixed(2);
            } else if (price >= 1) {
              // 1~99원: 소수점 6자리
              return price.toFixed(6);
            } else {
              // 1원 미만: 소수점 6자리
              return price.toFixed(6);
            }
          },
          // ✅ 크로스헤어 레이블의 시간 포맷터 (X축과 동일한 형식으로 통일)
          timeFormatter: (time: number) => {
            const date = new Date(time * 1000);
            const month = String(date.getMonth() + 1).padStart(2, '0');
            const day = String(date.getDate()).padStart(2, '0');
            const hours = String(date.getHours()).padStart(2, '0');
            const minutes = String(date.getMinutes()).padStart(2, '0');
            return `${month}/${day} ${hours}:${minutes}`;
          },
        },
        grid: {
          vertLines: { visible: true, color: chartColors.gridColor },
          horzLines: { visible: true, color: chartColors.gridColor },
        },
        crosshair: {
          mode: isMobileMode ? 1 : 0, // 모바일: Magnet 모드, PC: Normal 모드
          vertLine: {
            visible: isMobileMode ? true : false, // 모바일: 수직선 표시, PC: 업비트 스타일 (전역 수직선 사용)
            width: 1,
            color: chartColors.crosshairColor,
            style: isMobileMode ? 3 : 0, // 모바일: 점선, PC: 실선
            labelVisible: true, // ✅ PC/모바일 모두 세로선 날짜 레이블 표시
            labelBackgroundColor: chartColors.crosshairLabelBg,
          },
          horzLine: {
            visible: !hideNativeCrosshair, // hideNativeCrosshair가 true면 수평선도 숨김
            width: 1,
            color: chartColors.crosshairColor,
            style: isMobileMode ? 3 : 0, // 모바일: 점선, PC: 실선
            labelBackgroundColor: chartColors.crosshairLabelBg,
            labelVisible: true, // 가로선 레이블 표시
          },
        },
        leftPriceScale: {
          visible: false,  // ✅ 왼쪽 Y축 숨김
          minimumWidth: 0,  // ✅ 왼쪽 여백 완전 제거
          mode: 0,  // ✅ PriceScaleMode.Normal
        },
        rightPriceScale: {
          visible: true,  // ✅ 오른쪽 Y축 표시 (업비트 스타일)
          borderVisible: false,  // ✅ Y축 경계선 숨김 (서브차트 및 IndicatorChart와 완전히 일치)
          borderColor: 'transparent',  // ✅ 경계선 색상도 투명으로
          minimumWidth: stackedPanelsRightPriceScaleMinWidth(subPanels, isMobileMode),
          entireTextOnly: false,  // ✅ 라벨이 잘려도 괜찮음 (서브차트 및 IndicatorChart와 일치)
          autoScale: true,
          scaleMargins: {
            top: 0.1,
            bottom: 0.15,  // ✅ 0.1 → 0.15로 증가 (X축 라벨 영역과 겹치지 않도록)
          },
        },
        timeScale: {
          visible: true,  // ✅ 업비트 스타일: 캔들차트 하단에 X축 표시
          borderVisible: true,
          borderColor: chartColors.gridColor,
          timeVisible: true,
          secondsVisible: false,
          // ✅ 일목균형표 선행스팬이 활성화되어 있으면 우측 여백 확대 (미래 시점 표시용)
          // ✅ 기본 5: 투자분석 화면과 동일하게 설정하여 보조지표와 X축 위치 일치 (IndicatorChart.tsx와 동일)
          rightOffset: (ichimokuSettings?.senkouA || ichimokuSettings?.senkouB) ? 30 : 5,  // 선행스팬 ON: 30, OFF: 5
          barSpacing: 8,    // 기본 캔들 간격 (초기 로드 시)
          minBarSpacing: 5, // ✅ 최소 캔들 간격 (줌인 한계 - 200개 데이터가 화면에 다 보이는 수준)
          fixLeftEdge: (ichimokuSettings?.senkouA || ichimokuSettings?.senkouB) ? true : false,  // ✅ 일목균형표 활성화 시 좌측 기준 정렬
          fixRightEdge: false,  // ✅ 수동 스크롤 허용: 마지막 캔들 우측에 빈 공간 생성 가능 (업비트 스타일)
          lockVisibleTimeRangeOnResize: true,  // ✅ 리사이즈 시에도 범위 유지 (스케일 고정)
          rightBarStaysOnScroll: false,  // ✅ 스크롤 시 차트가 자유롭게 이동 (업비트 스타일)
          shiftVisibleRangeOnNewBar: false, // ✅ 수동 스크롤 모드: 자동 롤링 비활성화 (사용자가 과거 데이터 볼 때 방해되지 않도록)
          allowShiftVisibleRangeOnWhitespaceReplacement: false,  // ✅ 자동 롤링 비활성화
          // 시간 포맷터 커스터마이징
          tickMarkFormatter: (time: number) => {
            const date = new Date(time * 1000);
            const month = String(date.getMonth() + 1).padStart(2, '0');
            const day = String(date.getDate()).padStart(2, '0');
            const hours = String(date.getHours()).padStart(2, '0');
            const minutes = String(date.getMinutes()).padStart(2, '0');
            return `${month}/${day} ${hours}:${minutes}`;
          },
        },
        // ✅ 차트 스크롤 동작: PC는 기본 설정, 모바일은 터치 최적화
        handleScroll: {
          mouseWheel: false,      // 마우스 휠 스크롤 비활성화
          pressedMouseMove: isMobileMode ? false : false, // PC: 마우스 드래그 스크롤 비활성화
          horzTouchDrag: isMobileMode ? true : false,     // 모바일: 터치 가로 드래그 활성화 (좌우 이동)
          vertTouchDrag: isMobileMode ? true : false,     // 모바일: 터치 세로 드래그 활성화 (확대/축소)
        },
        // ✅ 차트 줌 동작: PC와 모바일 모두 지원
        handleScale: {
          mouseWheel: isMobileMode ? true : false,       // 모바일: 휠 줌 활성화, PC: 줌 컨트롤만 사용
          pinch: isMobileMode ? true : false,            // 모바일: 핀치 줌 활성화
          axisPressedMouseMove: {
            time: isMobileMode ? true : false,           // 모바일: X축 드래그로 줌 활성화
            price: isMobileMode ? true : false,          // 모바일: Y축 드래그로 줌 활성화
          },
          axisDoubleClickReset: isMobileMode ? false : false, // 축 더블클릭 리셋 비활성화
        },
        kineticScroll: {
          mouse: false,  // 마우스 관성 스크롤 비활성화
          touch: isMobileMode ? true : false,   // 모바일: 터치 관성 스크롤 활성화
        },
      });

      chartRef.current = chart;

      // ✅ 메인 차트의 시간 범위를 ref에 실시간 저장 (서브차트 동기화용)
      const timeScale = chart.timeScale();
      const updateMainTimeRange = () => {
        const timeRange = timeScale.getVisibleRange();
        if (timeRange) {
          mainChartTimeRangeRef.current = timeRange;
          console.log(`📊 [메인 차트] 시간 범위 ref 업데이트: ${new Date((timeRange.from as number) * 1000).toISOString()} ~ ${new Date((timeRange.to as number) * 1000).toISOString()}`);
        }
      };
      // 초기 범위 저장 (데이터 설정 후 여러 번 시도)
      setTimeout(() => updateMainTimeRange(), 0);
      setTimeout(() => updateMainTimeRange(), 50);
      setTimeout(() => updateMainTimeRange(), 100);
      // 범위 변경 시마다 업데이트
      timeScale.subscribeVisibleTimeRangeChange(updateMainTimeRange);

      // 캔들스틱 시리즈 (왼쪽 Y축 사용)
      // 차트 뷰 모드에 따라 캔들 색상 변경
      const candleColors = chartViewMode === 'basic'
        ? {
          upColor: '#EF5350',      // 밝은 빨간색 (양봉)
          downColor: '#42A5F5',    // 밝은 파란색 (음봉)
          wickUpColor: '#EF5350',
          wickDownColor: '#42A5F5',
        }
        : {
          upColor: chartColors.candleRising,
          downColor: chartColors.candleFalling,
          wickUpColor: chartColors.candleRising,
          wickDownColor: chartColors.candleFalling,
        };

      // ✅ candleData를 먼저 생성 → autoscaleInfoProvider가 ref에서 즉시 사용 가능 (타이밍 이슈 방지)
      const toUnixSeconds = (v: string | number | undefined): number | null => {
        if (v == null) return null;
        if (typeof v === 'number') {
          if (!isFinite(v)) return null;
          return Math.floor(v >= 1e12 ? v / 1000 : v); // ms(13자리) → 초, 이미 초(10자리)면 그대로, 소수점 제거
        }
        if (typeof v === 'string') {
          const parsed = new Date(v).getTime();
          return isFinite(parsed) ? Math.floor(parsed / 1000) : null;
        }
        // v가 객체인 경우 (Date 객체 등)
        console.error('[toUnixSeconds] ❌ 예상치 못한 타입:', typeof v, v);
        return null;
      };

      const buildCandleDataRaw = (source: CandleData[]) => {
        // ✅ 소스 데이터 샘플 로그
        if (source.length > 0) {
          const firstItem = source[0];
          console.log('[buildCandleDataRaw] 소스 데이터 샘플 (첫 번째):', {
            time: firstItem.time,
            timeType: typeof firstItem.time,
            timeJSON: JSON.stringify(firstItem.time),
          });
        }
        
        return source
          .filter((item): item is CandleData => item != null && item.open !== undefined && item.close !== undefined)
          .map((item) => {
            const t = toUnixSeconds(item.time ?? (item as any).timestamp);
            if (t == null || t <= 0) {
              console.warn('[buildCandleDataRaw] time 변환 실패:', {
                original: item.time,
                timestamp: (item as any).timestamp,
                converted: t,
              });
              return null;
            }
            return {
              time: Math.floor(t),
              open: item.open,
              high: item.high,
              low: item.low,
              close: item.close,
              volume: item.volume || 0,
            };
          })
          .filter((c): c is NonNullable<typeof c> => c != null)
          .filter((c) => c.time != null && !isNaN(c.time) && isFinite(c.time) && c.time > 0);
      };

      let candleDataRaw = buildCandleDataRaw(uniqueData);
      // ✅ 멀티차트 전용: data(uniqueData)에서 캔들이 없을 때 maCalculationData fallback (투자분석은 chartData에 캔들 있음)
      if (candleDataRaw.length === 0 && maCalculationData && maCalculationData.length > 0) {
        candleDataRaw = buildCandleDataRaw(maCalculationData);
        console.log(`📊 [멀티차트] data에 캔들 없음 - maCalculationData ${maCalculationData.length}개로 fallback`);
      }

      candleDataRaw.sort((a, b) => a.time - b.time);

      const candleDataMap = new Map<number, typeof candleDataRaw[0]>();
      for (const candle of candleDataRaw) {
        candleDataMap.set(candle.time, candle);
      }
      const candleData = Array.from(candleDataMap.values()).sort((a, b) => a.time - b.time);
      
      // ✅ 디버그: candleData의 time 타입 확인
      if (candleData.length > 0) {
        const lastCandle = candleData[candleData.length - 1];
        console.log('[CandlestickChart] 🔍 candleData 마지막 캔들:', {
          time: lastCandle.time,
          timeType: typeof lastCandle.time,
          isNumber: typeof lastCandle.time === 'number',
          close: lastCandle.close,
        });
      }

      // ✅ ref를 먼저 설정 (autoscaleInfoProvider에서 사용)
      // ⚠️ candleData는 플레이스홀더 제외한 실제 캔들만 포함
      candleDataRawRef.current = candleData;
      console.log(`📊 [candleDataRawRef] 설정 완료: ${candleData.length}개 (일목균형표 플레이스홀더 제외)`);

      const candleSeries = chart.addCandlestickSeries({
        ...candleColors,
        borderVisible: false,
        priceScaleId: 'right',
        lastValueVisible: true,
        priceFormat: {
          type: 'price',
          precision: 6,
          minMove: 0.000001,
        },
        autoscaleInfoProvider: () => {
          const visibleLogicalRange = chart.timeScale().getVisibleLogicalRange();
          const currentCandleData = candleDataRawRef.current;
          if (!visibleLogicalRange || currentCandleData.length === 0) return null;
          const startIndex = Math.max(0, Math.floor(visibleLogicalRange.from));
          const endIndex = Math.min(currentCandleData.length - 1, Math.ceil(visibleLogicalRange.to));
          let minPrice = Infinity;
          let maxPrice = -Infinity;
          for (let i = startIndex; i <= endIndex; i++) {
            const candle = currentCandleData[i];
            if (candle) {
              minPrice = Math.min(minPrice, candle.low);
              maxPrice = Math.max(maxPrice, candle.high);
            }
          }
          if (!isFinite(minPrice) || !isFinite(maxPrice)) return null;
          const priceCenter = (minPrice + maxPrice) / 2;
          const priceRange = maxPrice - minPrice;
          const scaledRange = priceRange / yAxisScaleFactorRef.current;
          return {
            priceRange: {
              minValue: priceCenter - scaledRange / 2,
              maxValue: priceCenter + scaledRange / 2,
            },
          };
        },
      });

      candleSeriesRef.current = candleSeries;
      
      // ✅ 일목 선행스팬 ON 시: lightweight-charts가 캔들 범위 밖 미래 시간을 표시하지 않으므로, 26봉 플레이스홀더 추가
      const senkouEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;
      let candleDataToSet = candleData;
      let realCandleCount = candleData.length; // ✅ 실제 캔들 개수 추적 (플레이스홀더 제외)
      
      // ✅ 실제 캔들 개수를 ref에 저장 (일목균형표 플레이스홀더 제외)
      (candleSeriesRef.current as any).__realCandleCount = realCandleCount;
      ichimokuFlatCandleTimesRef.current = [];
      if (senkouEnabled && candleData.length >= 2) {
        const lastCandle = candleData[candleData.length - 1];
        const prevCandle = candleData[candleData.length - 2];
        const lastTime = typeof lastCandle.time === 'number' ? lastCandle.time : new Date(lastCandle.time).getTime() / 1000;
        const prevTime = typeof prevCandle.time === 'number' ? prevCandle.time : new Date(prevCandle.time).getTime() / 1000;
        const intervalSec = Math.max(60, Math.floor(lastTime - prevTime));
        const lastClose = lastCandle.close;
        const placeholders: typeof candleData = [];
        for (let i = 1; i <= 26; i++) {
          placeholders.push({
            time: Math.floor(lastTime + i * intervalSec),
            open: lastClose,
            high: lastClose,
            low: lastClose,
            close: lastClose,
            volume: 0,
          });
        }
        ichimokuFlatCandleTimesRef.current = placeholders.map((p) =>
          typeof p.time === 'number' ? (p.time as number) : Math.floor(new Date(p.time as string).getTime() / 1000)
        );
        candleDataToSet = [...candleData, ...placeholders];
        console.log(`📏 [Ichimoku] 선행스팬 미래 영역: 캔들 시리즈에 26봉 플레이스홀더 추가 (interval=${intervalSec}s)`);
        console.log(`📏 [Ichimoku] 실제 캔들: ${realCandleCount}개, 플레이스홀더: 26개, 전체: ${candleDataToSet.length}개`);
      }
      
      // ✅ 디버그 & Validation: setData 전 모든 캔들 time 타입 확인 및 수정
      if (candleDataToSet.length > 0) {
        const firstCandle = candleDataToSet[0];
        const lastCandle = candleDataToSet[candleDataToSet.length - 1];
        const middleCandle = candleDataToSet[Math.floor(candleDataToSet.length / 2)];
        
        console.log('[CandlestickChart] 🔍 setData 직전 데이터 샘플:');
        console.log('  첫 번째:', {
          time: firstCandle.time,
          timeType: typeof firstCandle.time,
          timeConstructor: (firstCandle.time as any)?.constructor?.name,
          isNumber: typeof firstCandle.time === 'number',
        });
        console.log('  중간:', {
          time: middleCandle.time,
          timeType: typeof middleCandle.time,
          timeConstructor: (middleCandle.time as any)?.constructor?.name,
        });
        console.log('  마지막:', {
          time: lastCandle.time,
          timeType: typeof lastCandle.time,
          timeConstructor: (lastCandle.time as any)?.constructor?.name,
        });
        
        // ✅ CRITICAL: 모든 캔들의 time 검증 및 강제 변환
        let invalidCount = 0;
        let fixedCount = 0;
        
        candleDataToSet = candleDataToSet.map((c, idx) => {
          const timeValue = c.time as any;
          
          // number이고 유효한 경우 그대로 사용
          if (typeof timeValue === 'number' && isFinite(timeValue) && !isNaN(timeValue)) {
            return c;
          }
          
          // 그 외의 경우 모두 변환 시도
          invalidCount++;
          let fixedTime: number | null = null;
          
          if (typeof timeValue === 'string') {
            const parsed = new Date(timeValue).getTime() / 1000;
            if (isFinite(parsed)) {
              fixedTime = Math.floor(parsed);
              fixedCount++;
            }
          } else if (typeof timeValue === 'object' && timeValue !== null) {
            // Date 객체 또는 Date-like 객체인 경우
            if (timeValue instanceof Date || (typeof timeValue.getTime === 'function')) {
              try {
                fixedTime = Math.floor(timeValue.getTime() / 1000);
                fixedCount++;
              } catch (e) {
                console.error(`[CandlestickChart] ❌ [${idx}] getTime() 실패:`, e);
              }
            } else {
              console.error(`[CandlestickChart] ❌ [${idx}] 처리 불가능한 객체:`, {
                timeValue,
                keys: Object.keys(timeValue),
                constructor: timeValue.constructor?.name,
              });
            }
          }
          
          if (fixedTime !== null) {
            return { ...c, time: fixedTime };
          } else {
            console.error(`[CandlestickChart] ❌ [${idx}] 변환 실패, 제외됨:`, timeValue);
            return null;
          }
        }).filter((c): c is NonNullable<typeof c> => c !== null);
        
        if (invalidCount > 0) {
          console.error(`[CandlestickChart] ⚠️ 잘못된 time 형식: ${invalidCount}개, 변환 성공: ${fixedCount}개, 최종: ${candleDataToSet.length}개`);
        } else {
          console.log(`[CandlestickChart] ✅ 모든 캔들 time 검증 통과: ${candleDataToSet.length}개`);
        }
        
        // 변환 후 재확인
        const finalFirst = candleDataToSet[0];
        const finalLast = candleDataToSet[candleDataToSet.length - 1];
        console.log('[CandlestickChart] 🔍 setData 직전 최종 데이터:', {
          count: candleDataToSet.length,
          firstTime: finalFirst?.time,
          firstTimeType: typeof finalFirst?.time,
          lastTime: finalLast?.time,
          lastTimeType: typeof finalLast?.time,
        });
      }
      
      // ✅ CRITICAL: 데이터를 완전히 복사하여 라이브러리가 원본을 참조하지 못하도록 함
      const candleDataForChart = candleDataToSet.map(c => ({
        time: c.time,
        open: c.open,
        high: c.high,
        low: c.low,
        close: c.close,
        volume: c.volume,
      }));
      
      console.log('[CandlestickChart] 🔍 setData 호출 직전 - 복사된 데이터:', {
        count: candleDataForChart.length,
        firstTime: candleDataForChart[0]?.time,
        firstTimeType: typeof candleDataForChart[0]?.time,
      });
      
      candleSeries.setData(candleDataForChart as any);

      try {
        mainCandleDisplayTimesRef.current = candleDataForChart.map((c) => {
          const raw = c.time as number | string;
          if (typeof raw === 'number' && Number.isFinite(raw)) {
            return raw > 1e12 ? Math.floor(raw / 1000) : Math.floor(raw);
          }
          const ms = new Date(String(raw)).getTime();
          return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
        }).filter((s) => s > 0);
      } catch {
        mainCandleDisplayTimesRef.current = [];
      }

      // ✅ setData 후 실제 저장된 데이터 확인
      setTimeout(() => {
        try {
          const storedData = candleSeries.data();
          if (storedData && storedData.length > 0) {
            const firstStored = storedData[0];
            const middleStored = storedData[Math.floor(storedData.length / 2)];
            const lastStored = storedData[storedData.length - 1];
            
            console.log('[CandlestickChart] ✅ setData 후 실제 저장된 데이터:');
            console.log('  첫 번째:', {
              time: firstStored.time,
              timeType: typeof firstStored.time,
              timeConstructor: (firstStored.time as any)?.constructor?.name,
              timeJSON: JSON.stringify(firstStored.time),
            });
            console.log('  중간:', {
              time: middleStored.time,
              timeType: typeof middleStored.time,
              timeConstructor: (middleStored.time as any)?.constructor?.name,
            });
            console.log('  마지막:', {
              time: lastStored.time,
              timeType: typeof lastStored.time,
              timeConstructor: (lastStored.time as any)?.constructor?.name,
              timeJSON: JSON.stringify(lastStored.time),
            });
            
            // ✅ 잘못 저장된 데이터가 있는지 검사
            const invalidStored = [];
            for (let i = 0; i < storedData.length; i++) {
              const item = storedData[i];
              if (typeof item.time !== 'number') {
                invalidStored.push({ index: i, time: item.time, type: typeof item.time });
                if (invalidStored.length >= 5) break; // 최대 5개만 로그
              }
            }
            
            if (invalidStored.length > 0) {
              console.error('[CandlestickChart] ❌ setData 후에도 잘못된 time 형식 발견:', invalidStored);
              console.error('[CandlestickChart] ❌ 차트 라이브러리가 데이터를 변형했을 가능성 있음');
            } else {
              console.log('[CandlestickChart] ✅ setData 후 모든 저장된 데이터 정상');
            }
          }
        } catch (e) {
          console.error('[CandlestickChart] setData 후 검증 실패:', e);
        }
      }, 100);
      
      // ✅ candleData 유효성 체크
      if (!candleData || candleData.length === 0) {
        console.warn('⚠️ [MA/Ichimoku] candleData가 비어있음 - 렌더링 스킵');
        return;
      }

      // ✅ MA/Ichimoku 계산용 데이터 (allIndicatorData가 있으면 사용, 없으면 maCalculationData 또는 candleData 사용)
      const indicatorForCalculation = allIndicatorData && allIndicatorData.length > 0 ? allIndicatorData : (maCalculationData && maCalculationData.length > 0 ? maCalculationData : candleData);
      const dataForCalculation = maCalculationData && maCalculationData.length > 0 ? maCalculationData : candleData;

      if (indicatorForCalculation.length === 0) {
        console.warn('⚠️ [MA/Ichimoku] 계산용 데이터가 비어있음 - 렌더링 스킵');
        return;
      }

      console.log(`📊 [MA/Ichimoku] 렌더링 시작: candleData ${candleData.length}개, calculationData ${indicatorForCalculation.length}개`);
      console.log(`📊 [MA/Ichimoku] 데이터 소스: allIndicatorData=${allIndicatorData?.length || 0}개, maCalculationData=${maCalculationData?.length || 0}개`);

      // ✅ candleData를 Unix timestamp로 변환하여 Set 생성 (정확한 필터링을 위해)
      const candleTimesUnix = candleData
        .filter((d): d is (typeof candleData)[0] => d != null && d.time != null)
        .map((d) => {
          const time = typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : d.time;
          return Math.floor(time); // 소수점 제거하여 정확한 비교
        });
      const candleTimeSet = new Set(candleTimesUnix);

      // ✅ allIndicatorData 행은 지표만 있고 close 가 비어 있을 수 있음 → 같은 인덱스 dataForCalculation(캔들) 종가로 보강 (일목·MA 공통)
      const sortedDataForMA = (indicatorForCalculation as any[])
        .map((d: any, i: number) => {
          if (d == null || typeof d !== 'object') return null;
          const fallbackCandle = dataForCalculation[i];
          const close =
            d.close !== undefined && d.close !== null && !isNaN(Number(d.close))
              ? Number(d.close)
              : fallbackCandle != null && fallbackCandle.close != null && !isNaN(Number(fallbackCandle.close))
                ? Number(fallbackCandle.close)
                : null;
          if (close === null || !isFinite(close)) return null;
          const timeValue = d.timestamp || d.time || d.date || fallbackCandle?.time;
          if (timeValue == null && fallbackCandle?.time == null) return null;
          const tv = timeValue ?? fallbackCandle?.time;
          const tSec = Math.floor(typeof tv === 'string' ? new Date(tv).getTime() / 1000 : Number(tv));
          if (!isFinite(tSec)) return null;
          return { time: tSec, close };
        })
        .filter((x): x is { time: number; close: number } => x != null);
      
      console.log(`📊 [MA/Ichimoku] sortedDataForMA 길이: ${sortedDataForMA.length}개`);

      // ✅ candleData의 시간 범위 (범위 필터링용)
      const candleTimeRange = {
        start: typeof candleData[0].time === 'string' ? new Date(candleData[0].time).getTime() / 1000 : candleData[0].time,
        end: typeof candleData[candleData.length - 1].time === 'string' ? new Date(candleData[candleData.length - 1].time).getTime() / 1000 : candleData[candleData.length - 1].time,
      };

      // maLines가 있으면 사용자 정의 이동평균선 표시  
      if (maLines && maLines.length > 0) {
        console.log(`📊 [MA] ${maLines.filter(ma => ma.enabled).length}개 MA 렌더링 시작`);

        maLines.forEach(ma => {
          if (ma.enabled) {
            try {
              console.log(`🔍 [MA ${ma.id}] 렌더링 시작 - period: ${ma.period}, sortedDataForMA: ${sortedDataForMA.length}개`);

              // ✅ 데이터 부족 경고
              if (sortedDataForMA.length < ma.period) {
                console.warn(`⚠️ [MA ${ma.id}] 데이터 부족! 필요: ${ma.period}개, 현재: ${sortedDataForMA.length}개 - MA 라인이 그려지지 않습니다`);
                return;
              }

              // ✅ 프론트엔드에서 직접 SMA 계산 (기간값 변경 시 즉시 반영)
              const maDataFull = calculateSMA(sortedDataForMA, ma.period);
              console.log(`🔍 [MA ${ma.id}] SMA 계산 결과: ${maDataFull.length}개 (null 포함)`);
              
              // ✅ 1단계: null 값 제거
              const maDataValidated = validateChartData(maDataFull);
              console.log(`🔍 [MA ${ma.id}] null 제거 후: ${maDataValidated.length}개`);
              
              // ✅ 2단계: candleTimeSet 필터링
              const maDataFiltered = maDataValidated.filter(d => candleTimeSet.has(d.time));
              console.log(`🔍 [MA ${ma.id}] candleTimeSet 필터링 후: ${maDataFiltered.length}개`);
              
              // ✅ 3단계: 최종 정렬 (lightweight-charts 요구사항)
              const maDataSorted = maDataFiltered.sort((a, b) => a.time - b.time);
              // ✅ 4단계: 동일 time 중복 제거 (lightweight-charts는 time 오름차순·유일 필요)
              const byTime = new Map<number, { time: number; value: number }>();
              maDataSorted.forEach(d => byTime.set(d.time, d));
              const maData = Array.from(byTime.values()).sort((a, b) => a.time - b.time);
              const dupCount = maDataSorted.length - maData.length;
              if (dupCount > 0) console.log(`🔍 [MA ${ma.id}] 동일 time 중복 제거: ${dupCount}개`);
              console.log(`🔍 [MA ${ma.id}] 최종 정렬 후: ${maData.length}개`);

              console.log(`📊 [MA ${ma.id}] 프론트엔드 SMA 계산 완료: ${maData.length}개`);

              if (maData.length === 0) {
                console.warn(`⚠️ [MA ${ma.id}] 계산된 데이터 없음 (period: ${ma.period}, sortedDataForMA: ${sortedDataForMA.length}개)`);
                return;
              }

              // ✅ 데이터 검증 로그 (처음 3개, 마지막 3개)
              console.log(`🔍 [MA ${ma.id}] 데이터 샘플 (처음 3개):`, JSON.stringify(maData.slice(0, 3)));
              console.log(`🔍 [MA ${ma.id}] 데이터 샘플 (마지막 3개):`, JSON.stringify(maData.slice(-3)));

              if (maData.length === 0) {
                console.warn(`⚠️ [MA ${ma.id}] 최종 데이터 없음`);
                return;
              }

              const maSeries = chart.addLineSeries({
                color: ma.color,
                lineWidth: ma.width as any,
                lastValueVisible: false,
                priceLineVisible: false,
                priceScaleId: 'right',
                priceFormat: {
                  type: 'price',
                  precision: 6,
                  minMove: 0.000001,
                },
              });

              maSeries.setData(maData as any);
              // ✅ 실시간 업데이트용 ref에 저장
              maSeriesMapRef.current.set(ma.id, maSeries);
              console.log(`✅ [MA ${ma.id}] 시리즈 추가 완료 (${maData.length}개 데이터), ref 저장됨`);
            } catch (error) {
              console.error(`❌ [MA ${ma.id}] 렌더링 오류:`, error);
            }
          }
        });
      } else if (false) { // 기본 MA도 비활성화
        // 기본 이동평균선 (하위 호환성)
        const ma12DataFull = calculateSMA(sortedDataForMA, 12);
        const ma12DataFiltered = ma12DataFull.filter(d => candleTimeSet.has(d.time));
        const ma12Data = validateChartData(ma12DataFiltered);
        if (ma12Data.length > 0) {
          const ma12Series = chart.addLineSeries({
            color: chartColors.ma12,
            lineWidth: 1 as any,
            lastValueVisible: false,
            priceLineVisible: false,
            priceScaleId: 'right',
            priceFormat: {
              type: 'price',
              precision: 6,
              minMove: 0.000001,
            },
          });
          ma12Series.setData(ma12Data as any);
          // ✅ 실시간 업데이트용 ref에 저장
          maSeriesMapRef.current.set(12, ma12Series);
        }

        const ma26DataFull = calculateSMA(sortedDataForMA, 26);
        const ma26DataFiltered = ma26DataFull.filter(d => candleTimeSet.has(d.time));
        const ma26Data = validateChartData(ma26DataFiltered);
        if (ma26Data.length > 0) {
          const ma26Series = chart.addLineSeries({
            color: chartColors.ma26,
            lineWidth: 1 as any,
            lastValueVisible: false,
            priceLineVisible: false,
            priceScaleId: 'right',
            priceFormat: {
              type: 'price',
              precision: 6,
              minMove: 0.000001,
            },
          });
          ma26Series.setData(ma26Data as any);
          // ✅ 실시간 업데이트용 ref에 저장
          maSeriesMapRef.current.set(26, ma26Series);
        }

        const ma60DataFull = calculateSMA(sortedDataForMA, 60);
        const ma60DataFiltered = ma60DataFull.filter(d => candleTimeSet.has(d.time));
        const ma60Data = validateChartData(ma60DataFiltered);
        if (ma60Data.length > 0) {
          const ma60Series = chart.addLineSeries({
            color: chartColors.ma60,
            lineWidth: 1 as any,
            lastValueVisible: false,
            priceLineVisible: false,
            priceScaleId: 'right',
            priceFormat: {
              type: 'price',
              precision: 6,
              minMove: 0.000001,
            },
          });
          ma60Series.setData(ma60Data as any);
          // ✅ 실시간 업데이트용 ref에 저장
          maSeriesMapRef.current.set(60, ma60Series);
        }

        setMaValues({
          ma12: ma12Data.length > 0 ? ma12Data[ma12Data.length - 1].value : null,
          ma26: ma26Data.length > 0 ? ma26Data[ma26Data.length - 1].value : null,
          ma60: ma60Data.length > 0 ? ma60Data[ma60Data.length - 1].value : null,
        });
      }

      // ✅ 볼린저 밴드 (Bollinger Bands) - overlaySettingsOverride 우선, 없으면 context settings 사용
      const effectiveBollinger = overlaySettingsOverride ?? (settings ? {
        bollingerUpper: settings.bollingerUpper,
        bollingerMiddle: settings.bollingerMiddle,
        bollingerLower: settings.bollingerLower,
        bollingerPeriod: settings.bollingerPeriod ?? 20,
        bollingerStdDev: settings.bollingerStdDev ?? 2,
      } : null);
      if (effectiveBollinger && chartColors) {
        const { bollingerUpper, bollingerMiddle, bollingerLower } = effectiveBollinger;
        const bollingerPeriod = effectiveBollinger.bollingerPeriod ?? 20;
        const bollingerStdDev = effectiveBollinger.bollingerStdDev ?? 2;
        const isBollingerEnabled = bollingerUpper || bollingerMiddle || bollingerLower;
        
        console.log('📊 [Bollinger] 설정 확인:', { 
          bollingerUpper, 
          bollingerMiddle, 
          bollingerLower,
          isBollingerEnabled,
          backgroundColor: chartColors?.bollingerBandBackgroundColor
        });
        
        const hasBollingerData = uniqueData.some((d: any) =>
          d.bollingerUpper !== undefined || d.bollingerMiddle !== undefined || d.bollingerLower !== undefined
        );
        
        // 샘플 데이터 3개 확인
        const sampleData = uniqueData.slice(-3).map((d: any) => ({
          time: d.time,
          bollingerUpper: d.bollingerUpper,
          bollingerMiddle: d.bollingerMiddle,
          bollingerLower: d.bollingerLower
        }));
        
        console.log('📊 [Bollinger] 데이터 확인:', { 
          hasBollingerData, 
          totalDataCount: uniqueData.length,
          sampleData 
        });
        
        if (!isBollingerEnabled) {
          console.warn('⚠️ [Bollinger] 볼린저밴드가 비활성화되어 있습니다. 상한선/중간선/하한선 중 하나를 활성화해주세요.');
        }

        // ✅ 데이터에 볼린저 값이 없으면 클라이언트에서 계산 (멀티차트 등)
        let bollingerDataToUse: typeof uniqueData = uniqueData;
        if (!hasBollingerData && isBollingerEnabled) {
          const closes = uniqueData
            .filter((d: any) => d != null && typeof d === 'object' && d.close != null && !isNaN(d.close))
            .map((d: any) => ({ time: typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : d.time, close: d.close }));
          if (closes.length >= bollingerPeriod) {
            const calc: Array<{ time: number; upper: number; middle: number; lower: number }> = [];
            for (let i = bollingerPeriod - 1; i < closes.length; i++) {
              const slice = closes.slice(i - bollingerPeriod + 1, i + 1);
              const avg = slice.reduce((s, c) => s + c.close, 0) / bollingerPeriod;
              const variance = slice.reduce((s, c) => s + Math.pow(c.close - avg, 2), 0) / bollingerPeriod;
              const std = Math.sqrt(variance) || 0;
              calc.push({ time: Math.floor(closes[i].time), upper: avg + bollingerStdDev * std, middle: avg, lower: avg - bollingerStdDev * std });
            }
            const bollingerByTime = new Map(calc.map((c) => [c.time, c]));
            bollingerDataToUse = uniqueData.map((d: any) => {
              const t = Math.floor(typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : d.time);
              const b = bollingerByTime.get(t);
              return b ? { ...d, bollingerUpper: b.upper, bollingerMiddle: b.middle, bollingerLower: b.lower } : d;
            });
          }
        }

        if (hasBollingerData || (isBollingerEnabled && bollingerDataToUse.some((d: any) => d.bollingerUpper != null))) {
          if (!hasBollingerData) console.log('✅ [Bollinger] 클라이언트 계산 데이터 사용');
          else console.log('✅ [Bollinger] uniqueData에서 볼린저밴드 데이터 사용');

          // 상한선 (Upper Band)
          if (bollingerUpper) {
            try {
              const upperData = bollingerDataToUse
                .filter((d: any) => d.bollingerUpper != null && !isNaN(d.bollingerUpper))
                .map((d: any) => ({
                  time: Math.floor(typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : d.time),
                  value: d.bollingerUpper,
                }))
                .sort((a, b) => a.time - b.time);

              if (upperData.length > 0) {
                const upperSeries = chart.addLineSeries({
                  color: chartColors.bollingerUpperColor || '#FF9800',
                  lineWidth: (chartColors.bollingerUpperWidth || 1.5) as any,
                  lineStyle: chartColors.bollingerUpperLineStyle === 'dashed' ? 2 : chartColors.bollingerUpperLineStyle === 'dotted' ? 3 : 0,
                  lastValueVisible: false,
                  priceLineVisible: false,
                  priceScaleId: 'right',
                  priceFormat: {
                    type: 'price',
                    precision: 6,
                    minMove: 0.000001,
                  },
                });
                upperSeries.setData(upperData as any);
                // ✅ 실시간 업데이트용 ref에 저장
                bollingerSeriesRef.current.upper = upperSeries;
                console.log(`✅ [Bollinger Upper] 상한선 추가 완료 (${upperData.length}개 데이터), ref 저장됨`);
              }
            } catch (error) {
              console.error('❌ [Bollinger Upper] 렌더링 오류:', error);
            }
          }

          // 중간선 (Middle Band)
          if (bollingerMiddle) {
            try {
              const middleData = bollingerDataToUse
                .filter((d: any) => d.bollingerMiddle != null && !isNaN(d.bollingerMiddle))
                .map((d: any) => ({
                  time: Math.floor(typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : d.time),
                  value: d.bollingerMiddle,
                }))
                .sort((a, b) => a.time - b.time);

              if (middleData.length > 0) {
                const middleSeries = chart.addLineSeries({
                  color: chartColors.bollingerMiddleColor || '#2196F3',
                  lineWidth: (chartColors.bollingerMiddleWidth || 1.5) as any,
                  lineStyle: chartColors.bollingerMiddleLineStyle === 'dashed' ? 2 : chartColors.bollingerMiddleLineStyle === 'dotted' ? 3 : 0,
                  lastValueVisible: false,
                  priceLineVisible: false,
                  priceScaleId: 'right',
                  priceFormat: {
                    type: 'price',
                    precision: 6,
                    minMove: 0.000001,
                  },
                });
                middleSeries.setData(middleData as any);
                // ✅ 실시간 업데이트용 ref에 저장
                bollingerSeriesRef.current.middle = middleSeries;
                console.log(`✅ [Bollinger Middle] 중간선 추가 완료 (${middleData.length}개 데이터), ref 저장됨`);
              }
            } catch (error) {
              console.error('❌ [Bollinger Middle] 렌더링 오류:', error);
            }
          }

          // 하한선 (Lower Band)
          if (bollingerLower) {
            try {
              const lowerData = bollingerDataToUse
                .filter((d: any) => d.bollingerLower != null && !isNaN(d.bollingerLower))
                .map((d: any) => ({
                  time: Math.floor(typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : d.time),
                  value: d.bollingerLower,
                }))
                .sort((a, b) => a.time - b.time);

              if (lowerData.length > 0) {
                const lowerSeries = chart.addLineSeries({
                  color: chartColors.bollingerLowerColor || '#4CAF50',
                  lineWidth: (chartColors.bollingerLowerWidth || 1.5) as any,
                  lineStyle: chartColors.bollingerLowerLineStyle === 'dashed' ? 2 : chartColors.bollingerLowerLineStyle === 'dotted' ? 3 : 0,
                  lastValueVisible: false,
                  priceLineVisible: false,
                  priceScaleId: 'right',
                  priceFormat: {
                    type: 'price',
                    precision: 6,
                    minMove: 0.000001,
                  },
                });
                lowerSeries.setData(lowerData as any);
                // ✅ 실시간 업데이트용 ref에 저장
                bollingerSeriesRef.current.lower = lowerSeries;
                console.log(`✅ [Bollinger Lower] 하한선 추가 완료 (${lowerData.length}개 데이터), ref 저장됨`);
              }
            } catch (error) {
              console.error('❌ [Bollinger Lower] 렌더링 오류:', error);
            }
          }

          // 볼린저밴드 배경 데이터 준비 (상한선과 하한선 사이 영역)
          // ✅ 배경색은 선 활성화 여부와 관계없이 데이터만 있으면 표시
          try {
            const bandData = bollingerDataToUse
              .filter((d: any) => d.bollingerUpper != null && d.bollingerLower != null && 
                      !isNaN(d.bollingerUpper) && !isNaN(d.bollingerLower))
              .map((d: any) => ({
                time: Math.floor(typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : d.time),
                upper: d.bollingerUpper,
                lower: d.bollingerLower,
              }))
              .sort((a, b) => a.time - b.time);

            console.log(`📊 [Bollinger Band 배경] 데이터 필터링 결과: ${bandData.length}개`);
            console.log(`📊 [Bollinger Band 배경] 샘플:`, bandData.slice(0, 3));

            if (bandData.length > 0) {
              setBollingerBandData(bandData);
              console.log(`✅ [Bollinger Band] 배경 데이터 설정 완료 (${bandData.length}개 데이터)`);
              console.log(`🎨 [Bollinger Band] 배경색: ${chartColors?.bollingerBandBackgroundColor}`);
              console.log(`🎨 [Bollinger Band] chartColors 객체:`, chartColors);
            } else {
              console.warn('⚠️ [Bollinger Band] bandData가 비어있음 - uniqueData에 볼린저밴드 값이 없습니다');
              setBollingerBandData([]);
            }
          } catch (error) {
            console.error('❌ [Bollinger Band] 배경 데이터 준비 오류:', error);
          }
        } else {
          console.warn('⚠️ [Bollinger] uniqueData에 볼린저밴드 데이터 없음');
          console.log('📊 [Bollinger] uniqueData 샘플:', uniqueData.slice(0, 3));
          setBollingerBandData([]);
        }
      }

      // ✅ displacement 적용 시 visible range 확장용 (멀티차트 등)
      let ichimokuDisplacementTimeRange: { from: number; to: number } | null = null;

      // ✅ 일목균형표 (사용자 정의 설정 기반)
      if (ichimokuSettings && ichimokuColors) {
        try {
        // ✅ 선행스팬(구름) ON 시: allIndicatorData/uniqueData 대신 항상 calculateIchimoku 사용
        // - 보조지표 추가 시 API가 ichimoku를 반환하지 않거나 null/부분 데이터 반환 시 구름 미렌더링 방지
        const senkouEnabled = ichimokuSettings.senkouA || ichimokuSettings.senkouB;
        const useCalculatedIchimoku = senkouEnabled;

        // ✅ 선행스팬 OFF인 경우에만 API/uniqueData 기반 일목 데이터 사용 검토
        const hasIchimokuInUniqueData = !useCalculatedIchimoku && uniqueData.some((d: any) =>
          d.ichimokuTenkan !== undefined ||
          d.ichimokuSenkouA !== undefined ||
          d.ichimokuSenkouB !== undefined
        );
        const hasIchimokuInAllIndicator = !useCalculatedIchimoku && (allIndicatorData?.some((d: any) =>
          d.ichimokuTenkan !== undefined ||
          d.ichimokuSenkouA !== undefined ||
          d.ichimokuSenkouB !== undefined
        ) ?? false);
        const hasIchimokuData = !useCalculatedIchimoku && (hasIchimokuInUniqueData || hasIchimokuInAllIndicator);

        let ichimokuData: Array<{
          time: number;
          tenkan?: number;
          kijun?: number;
          chikou?: number;
          senkouA?: number;
          senkouB?: number;
        }>;

        if (hasIchimokuData) {
          // uniqueData 또는 allIndicatorData에서 일목균형표 데이터 사용 (투자분석/멀티차트 공통)
          const sourceData = hasIchimokuInUniqueData ? uniqueData : (allIndicatorData || []);
          console.log(`✅ [Ichimoku] ${hasIchimokuInUniqueData ? 'uniqueData' : 'allIndicatorData'}에서 일목균형표 데이터 사용 (미래 영역 포함)`);
          ichimokuData = sourceData.map((d: any) => {
            const timeVal = d.time ?? d.timestamp ?? d.date;
            return {
              time: timeVal != null ? (typeof timeVal === 'string' ? new Date(timeVal).getTime() / 1000 : timeVal) : 0,
              tenkan: d.ichimokuTenkan,
              kijun: d.ichimokuKijun,
              chikou: d.ichimokuChikou,
              senkouA: d.ichimokuSenkouA,
              senkouB: d.ichimokuSenkouB,
            };
          }).filter(d => d.time > 0);

          console.log(`🔍 [Ichimoku] ichimokuData.length=${ichimokuData.length}`);
          const firstFewItems = ichimokuData.slice(0, 3);
          const lastFewItems = ichimokuData.slice(-3);
          console.log(`🔍 [Ichimoku] 처음 3개:`, firstFewItems);
          console.log(`🔍 [Ichimoku] 마지막 3개:`, lastFewItems);
        } else {
          // uniqueData에 일목균형표 데이터가 없으면 계산
          console.log('🔧 [Ichimoku] 일목균형표 데이터 계산');
          // ✅ high/low/close 유효성 검사 - 선행스팬만 활성화 시 차트 미렌더링 방지
          type CalcItem = { time?: string | number; high?: number; low?: number; close?: number };
          const validForIchimoku = (dataForCalculation as CalcItem[])
            .filter((d) => {
              const t = typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : d.time;
              const h = d.high;
              const l = d.low;
              const c = d.close;
              return t != null && !isNaN(Number(t)) && isFinite(Number(t)) &&
                h != null && !isNaN(h) && isFinite(h) &&
                l != null && !isNaN(l) && isFinite(l) &&
                c != null && !isNaN(c) && isFinite(c);
            })
            .map((d: CalcItem) => ({
              time: typeof d.time === 'string' ? new Date(d.time).getTime() / 1000 : (d.time as number),
              high: d.high!,
              low: d.low!,
              close: d.close!
            }));
          const requiredLength = Math.max(
            ichimokuSettings.tenkanPeriod ?? 9,
            ichimokuSettings.kijunPeriod ?? 26,
            ichimokuSettings.senkouBPeriod ?? 52
          );
          
          if (validForIchimoku.length < requiredLength) {
            console.warn(`⚠️ [Ichimoku] 계산용 데이터 부족: ${validForIchimoku.length}개 (필요: ${requiredLength}개 이상) - 계산 중단`);
            // ✅ 데이터 부족 시 빈 배열 반환하여 비정상 렌더링 방지
            ichimokuData = [];
          } else {
            ichimokuData = calculateIchimoku(
              validForIchimoku,
              ichimokuSettings.tenkanPeriod,
              ichimokuSettings.kijunPeriod,
              ichimokuSettings.senkouBPeriod
            );
          }
        }

        // ✅ 구름/선행스팬: 백엔드 데이터도 senkou 값은 "현재 시점+26"에 표시되므로 displacement 항상 적용
        const useSenkouDisplacement = (ichimokuSettings.senkouA || ichimokuSettings.senkouB) && ichimokuData.length >= 2;
        const senkouDisplacement = ichimokuSettings.kijunPeriod ?? 26;
        const ichimokuTimes = ichimokuData.map(d => (typeof d.time === 'number' ? d.time : new Date(d.time).getTime() / 1000) as number);
        const lastIchimokuTime = ichimokuTimes[ichimokuTimes.length - 1];
        const ichimokuIntervalSec = ichimokuTimes.length >= 2 ? ichimokuTimes[ichimokuTimes.length - 1] - ichimokuTimes[ichimokuTimes.length - 2] : 60;

        // 전환선 (Tenkan-sen) - Canvas Overlay로 렌더 (addLineSeries "Value is null" 회피)
        if (ichimokuSettings.tenkan) {
          const tenkanDataFull = ichimokuData
            .filter(d => d.tenkan !== undefined && d.tenkan !== null && !isNaN(d.tenkan) && isFinite(d.tenkan))
            .map(d => ({ time: d.time, value: d.tenkan! }));
          const tenkanDataFiltered = tenkanDataFull.filter(d => candleTimeSet.has(d.time));
          const tenkanData = validateChartData(tenkanDataFiltered);
          setTenkanLineData(tenkanData.length > 0 ? tenkanData : []);
        } else {
          setTenkanLineData([]);
        }

        // 기준선 (Kijun-sen) - Canvas Overlay로 렌더 (addLineSeries "Value is null" 회피)
        if (ichimokuSettings.kijun) {
          const kijunDataFull = ichimokuData
            .filter(d => d.kijun !== undefined && d.kijun !== null && !isNaN(d.kijun) && isFinite(d.kijun))
            .map(d => ({ time: d.time, value: d.kijun! }));
          const kijunDataFiltered = kijunDataFull.filter(d => candleTimeSet.has(d.time));
          const kijunData = validateChartData(kijunDataFiltered);
          setKijunLineData(kijunData.length > 0 ? kijunData : []);
        } else {
          setKijunLineData([]);
        }

        // ✅ 차트 가격 범위 (전환선/기준선/선행스팬 Canvas 오버레이용 - 일목 하나라도 ON이면 설정)
        if (ichimokuSettings.tenkan || ichimokuSettings.kijun || ichimokuSettings.senkouA || ichimokuSettings.senkouB) {
          const allPrices: number[] = [];
          uniqueData.forEach((candle) => {
            if (!candle) return;
            if (candle.high !== undefined && candle.high > 0) allPrices.push(candle.high);
            if (candle.low !== undefined && candle.low > 0) allPrices.push(candle.low);
          });
          ichimokuData.forEach(d => {
            if (d.tenkan !== undefined && !isNaN(d.tenkan) && d.tenkan > 0) allPrices.push(d.tenkan);
            if (d.kijun !== undefined && !isNaN(d.kijun) && d.kijun > 0) allPrices.push(d.kijun);
            if (d.chikou !== undefined && !isNaN(d.chikou) && d.chikou > 0) allPrices.push(d.chikou);
            if (d.senkouA !== undefined && !isNaN(d.senkouA) && d.senkouA > 0) allPrices.push(d.senkouA);
            if (d.senkouB !== undefined && !isNaN(d.senkouB) && d.senkouB > 0) allPrices.push(d.senkouB);
          });
          if (allPrices.length > 0) {
            let minPrice = Math.min(...allPrices);
            let maxPrice = Math.max(...allPrices);
            if (!isNaN(minPrice) && !isNaN(maxPrice) && maxPrice > minPrice) {
              const pad = (maxPrice - minPrice) * 0.02;
              minPrice -= pad;
              maxPrice += pad;
            }
            if (!isNaN(minPrice) && !isNaN(maxPrice)) {
              setChartPriceRange({ min: minPrice, max: maxPrice });
            }
          }
        }

        // 후행스팬 (Chikou Span) - 26일 후행 처리
        // 백엔드에서 이미 chikou[i] = closes[i + 26]로 계산되어 있음
        // 프론트엔드에서는 그대로 각 시간에 표시하면 됨 (추가 시간 이동 불필요)
        if (ichimokuSettings.chikou) {
          const chikouDataFull = ichimokuData
            .filter(d => d.chikou !== undefined && d.chikou !== null && !isNaN(d.chikou) && isFinite(d.chikou))
            .map(d => ({
              time: d.time, // ✅ 각 데이터의 원래 시간 사용 (백엔드에서 이미 26일 후행 처리됨)
              value: d.chikou!
            }));
          const chikouDataFiltered = chikouDataFull.filter(d => candleTimeSet.has(d.time));
          const chikouData = validateChartData(chikouDataFiltered);

          if (chikouData.length > 0) {
            const chikouSeries = chart.addLineSeries({
              color: ichimokuColors.chikou,
              lineWidth: (ichimokuColors.chikouWidth || 1) as any,
              lastValueVisible: false,
              priceLineVisible: false,
              priceScaleId: 'right',
              priceFormat: {
                type: 'price',
                precision: 6,
                minMove: 0.000001,
              },
            });
            chikouSeries.setData(chikouData as any);
            console.log(`✅ [일목균형표] 후행스팬 렌더링 완료: ${chikouData.length}개 데이터`);
          }
        }

        // 선행스팬1 (Senkou Span A) - Canvas Overlay로 렌더 (addLineSeries 사용 시 "Value is null" 회피)
        if (ichimokuSettings.senkouA) {
          const senkouADataFull = useSenkouDisplacement
            ? ichimokuData
                .map((d, i) => {
                  if (d.senkouA === undefined || d.senkouA === null || isNaN(d.senkouA) || !isFinite(d.senkouA)) return null;
                  const targetTime = i + senkouDisplacement < ichimokuData.length
                    ? ichimokuTimes[i + senkouDisplacement]
                    : lastIchimokuTime + (i + senkouDisplacement - ichimokuData.length + 1) * ichimokuIntervalSec;
                  return { time: targetTime, value: d.senkouA };
                })
                .filter((x): x is { time: number; value: number } => x !== null)
            : ichimokuData
                .filter(d => d.senkouA !== undefined && d.senkouA !== null && !isNaN(d.senkouA) && isFinite(d.senkouA))
                .map(d => ({ time: d.time, value: d.senkouA! }));

          const senkouAData = validateChartData(senkouADataFull);
          setSenkouALineData(senkouAData.length > 0 ? senkouAData : []);
          if (senkouAData.length > 0) {
            console.log(`✅ [Ichimoku] 선행스팬1 렌더링 (Canvas): ${senkouAData.length}개 포인트 (백엔드 displacement 적용됨, 색상: ${ichimokuColors.senkouA}, 굵기: ${ichimokuColors.senkouAWidth || 1})`);
          } else {
            console.warn(`⚠️ [Ichimoku] 선행스팬1 데이터가 비어있거나 유효하지 않음`);
          }
        } else {
          setSenkouALineData([]);
        }

        // 선행스팬2 (Senkou Span B) - Canvas Overlay로 렌더 (addLineSeries 사용 시 "Value is null" 회피)
        if (ichimokuSettings.senkouB) {
          const senkouBDataFull = useSenkouDisplacement
            ? ichimokuData
                .map((d, i) => {
                  if (d.senkouB === undefined || d.senkouB === null || isNaN(d.senkouB) || !isFinite(d.senkouB)) return null;
                  const targetTime = i + senkouDisplacement < ichimokuData.length
                    ? ichimokuTimes[i + senkouDisplacement]
                    : lastIchimokuTime + (i + senkouDisplacement - ichimokuData.length + 1) * ichimokuIntervalSec;
                  return { time: targetTime, value: d.senkouB };
                })
                .filter((x): x is { time: number; value: number } => x !== null)
            : ichimokuData
                .filter(d => d.senkouB !== undefined && d.senkouB !== null && !isNaN(d.senkouB) && isFinite(d.senkouB))
                .map(d => ({ time: d.time, value: d.senkouB! }));

          const senkouBData = validateChartData(senkouBDataFull);
          setSenkouBLineData(senkouBData.length > 0 ? senkouBData : []);
          if (senkouBData.length > 0) {
            console.log(`✅ [Ichimoku] 선행스팬2 렌더링 (Canvas): ${senkouBData.length}개 포인트 (백엔드 displacement 적용됨, 색상: ${ichimokuColors.senkouB}, 굵기: ${ichimokuColors.senkouBWidth || 1})`);
          } else {
            console.warn(`⚠️ [Ichimoku] 선행스팬2 데이터가 비어있거나 유효하지 않음`);
          }
        } else {
          setSenkouBLineData([]);
        }

        // ✅ 구름 영역 (Kumo/Cloud) 데이터 준비 - Canvas Overlay로 렌더링
        // 백엔드에서 이미 displacement를 적용하여 보내므로 그대로 사용
        console.log(`☁️ [구름 조건 체크] senkouA=${ichimokuSettings.senkouA}, senkouB=${ichimokuSettings.senkouB}, ichimokuData.length=${ichimokuData.length}`);

        if (ichimokuSettings.senkouA && ichimokuSettings.senkouB) {
          console.log(`☁️ [구름] 조건 만족 - 구름 데이터 준비 시작`);
          try {
            const cloudDataRaw = useSenkouDisplacement
              ? ichimokuData
                  .map((d, i) => {
                    if (d.senkouA == null || isNaN(d.senkouA) || !isFinite(d.senkouA) || d.senkouB == null || isNaN(d.senkouB) || !isFinite(d.senkouB)) return null;
                    const targetTime = i + senkouDisplacement < ichimokuData.length
                      ? ichimokuTimes[i + senkouDisplacement]
                      : lastIchimokuTime + (i + senkouDisplacement - ichimokuData.length + 1) * ichimokuIntervalSec;
                    return { time: targetTime, senkouA: d.senkouA, senkouB: d.senkouB };
                  })
                  .filter((x): x is { time: number; senkouA: number; senkouB: number } => x !== null)
              : ichimokuData
                  .filter(d =>
                    d.time !== undefined &&
                    d.senkouA !== undefined && d.senkouA !== null && !isNaN(d.senkouA) && isFinite(d.senkouA) &&
                    d.senkouB !== undefined && d.senkouB !== null && !isNaN(d.senkouB) && isFinite(d.senkouB)
                  )
                  .map(d => ({
                    time: d.time,
                    senkouA: d.senkouA!,
                    senkouB: d.senkouB!
                  }));

            console.log(`☁️ [Ichimoku Cloud] 구름 데이터 준비: ${cloudDataRaw.length}개 포인트${useSenkouDisplacement ? ' (displacement 적용)' : ''}`);

            if (cloudDataRaw.length === 0) {
              console.warn(`⚠️ [구름] cloudDataRaw가 비어있음! ichimokuData 샘플:`, ichimokuData.slice(0, 3));
            }

            // 시간순 정렬 + 동일 타임스탬프 병합 (역순/중복 시 x축 꺾임·수직 스파이크 방지, 모든 봉주기 공통)
            const sortedCloud = [...cloudDataRaw].sort((a, b) => a.time - b.time);
            const cloudMerged: Array<{ time: number; senkouA: number; senkouB: number }> = [];
            for (const row of sortedCloud) {
              if (cloudMerged.length === 0) {
                cloudMerged.push({ ...row });
                continue;
              }
              const last = cloudMerged[cloudMerged.length - 1];
              if (row.time === last.time) {
                last.senkouA = row.senkouA;
                last.senkouB = row.senkouB;
              } else {
                cloudMerged.push({ ...row });
              }
            }

            // ✅ displacement 적용 시 visible range 확장용 시간 범위 저장 (멀티차트 미래 영역 표시)
            // ✅ 반드시 candleData 범위를 포함하여 캔들이 보이도록 함
            if (useSenkouDisplacement && cloudMerged.length > 0 && candleData.length > 0) {
              const cloudTimes = cloudMerged.map(d => d.time);
              const candleStart = candleData[0].time;
              const candleEnd = candleData[candleData.length - 1].time;
              const from = Math.min(candleStart, Math.min(...cloudTimes));
              const to = Math.max(candleEnd, Math.max(...cloudTimes));
              ichimokuDisplacementTimeRange = { from, to };
              console.log(`☁️ [구름] displacement visible range (캔들 포함): ${from} ~ ${to}`);
            }

            setIchimokuCloudData(cloudMerged);

            console.log(`☁️ [Ichimoku] 구름 데이터 저장: ${cloudMerged.length}개 포인트 (원본 ${cloudDataRaw.length})`);
            console.log(`🎨 [Ichimoku] 전달된 색상:`, {
              cloudBullish: ichimokuColors?.cloudBullish,
              cloudBearish: ichimokuColors?.cloudBearish,
              전체ichimokuColors: ichimokuColors
            });

            console.log('✅ [Ichimoku] 구름 영역 데이터 준비 완료 (미래 포함, Canvas Overlay)', cloudDataRaw.length, '포인트');

            // 📊 일목균형표 판단 로직 실행 (최신 데이터 기준)
            if (ichimokuData.length > 0 && uniqueData.length > 0) {
              // ✅ close가 정의된 실제 캔들을 찾음 (미래 데이터 제외)
              let latestCandle = uniqueData[uniqueData.length - 1];
              let latestCandleIndex = uniqueData.length - 1;

              // close가 undefined면 역순으로 실제 캔들 찾기
              if (latestCandle.close === undefined) {
                console.warn('⚠️ [Ichimoku 분석] 마지막 캔들의 close가 undefined - 실제 캔들 탐색');
                for (let i = uniqueData.length - 1; i >= 0; i--) {
                  if (uniqueData[i].close !== undefined) {
                    latestCandle = uniqueData[i];
                    latestCandleIndex = i;
                    console.log(`✅ [Ichimoku 분석] 실제 마지막 캔들 발견: index=${i}, close=${latestCandle.close}`);
                    break;
                  }
                }
              }

              const latestIchimoku = ichimokuData[latestCandleIndex];
              const previous26Candle = latestCandleIndex >= 26 ? uniqueData[latestCandleIndex - 26] : undefined;

              // close가 여전히 undefined면 분석 스킵
              if (latestCandle.close !== undefined && latestIchimoku) {
                const analysis = analyzeIchimoku(
                  latestCandle.close,
                  latestIchimoku.tenkan,
                  latestIchimoku.kijun,
                  latestIchimoku.senkouA,
                  latestIchimoku.senkouB,
                  latestIchimoku.chikou,
                  previous26Candle?.close
                );

                console.log('📊 [일목균형표 분석]', {
                  현재가: latestCandle.close.toLocaleString('ko-KR'),
                  구름위치: analysis.cloudPosition,
                  TK관계: analysis.tkCross,
                  후행스팬: analysis.chikouTrend,
                  구름타입: analysis.cloudType,
                  구름두께: analysis.cloudThickness.toFixed(0),
                  종합신호: analysis.signal
                });
              } else {
                console.warn('⚠️ [Ichimoku 분석] 유효한 캔들 데이터 없음 - 분석 스킵');
              }
            }
          } catch (error) {
            console.error('❌ [Ichimoku] 구름 영역 데이터 준비 오류:', error);
          }
        } else {
          // 선행스팬이 OFF되면 구름 데이터 초기화
          console.log(`☁️ [구름] 선행스팬1 또는 2가 OFF - 구름 초기화`);
          setIchimokuCloudData([]);
        }
        } catch (e) {
          console.error('❌ [Ichimoku] 일목균형표 블록 오류 (차트는 캔들만 표시):', e);
          setIchimokuCloudData([]);
          setSenkouALineData([]);
          setSenkouBLineData([]);
          setTenkanLineData([]);
          setKijunLineData([]);
        }
      } else {
        // Ichimoku 비활성화 시 일목 데이터 초기화
        setSenkouALineData([]);
        setSenkouBLineData([]);
        setTenkanLineData([]);
        setKijunLineData([]);
      }

      // ======= 보조지표 오버레이 (indicatorData 기반) - showIndicatorOverlay가 true일 때만 =======
      if (showIndicatorOverlay && indicatorData && indicatorData.length > 0) {
        // 지표 데이터를 time 기준으로 매핑
        const indicatorMap = new Map<number, IndicatorData>();
        indicatorData.forEach(ind => {
          const timeVal = new Date(ind.time).getTime() / 1000;
          indicatorMap.set(Math.floor(timeVal), ind);
        });

        // Stochastic Slow 오버레이 (0-100 스케일)
        if (settings.stochasticSlow) {
          const stochKData = indicatorData
            .filter(d => d.stochK !== undefined && d.stochK !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.stochK! }))
            .sort((a, b) => a.time - b.time);

          const stochDData = indicatorData
            .filter(d => d.stochD !== undefined && d.stochD !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.stochD! }))
            .sort((a, b) => a.time - b.time);

          if (stochKData.length > 0) {
            const stochKSeries = chart.addLineSeries({
              color: '#FF9500',
              lineWidth: 1 as any,
              title: 'Stoch %K',
              priceScaleId: 'oscillator',
              lastValueVisible: true,
              priceLineVisible: false,
            });
            stochKSeries.setData(stochKData as any);
          }

          if (stochDData.length > 0) {
            const stochDSeries = chart.addLineSeries({
              color: '#2196F3',
              lineWidth: 1 as any,
              title: 'Stoch %D',
              priceScaleId: 'oscillator',
              lastValueVisible: true,
              priceLineVisible: false,
            });
            stochDSeries.setData(stochDData as any);
          }
        }

        // MACD 라인 오버레이
        if (settings.macdLine) {
          const macdLineData = indicatorData
            .filter(d => d.macdLine !== undefined && d.macdLine !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.macdLine! }))
            .sort((a, b) => a.time - b.time);

          const signalLineData = indicatorData
            .filter(d => d.signalLine !== undefined && d.signalLine !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.signalLine! }))
            .sort((a, b) => a.time - b.time);

          if (macdLineData.length > 0) {
            const macdSeries = chart.addLineSeries({
              color: '#e91e63',
              lineWidth: 1 as any,
              title: 'MACD',
              priceScaleId: 'macd',
              lastValueVisible: true,
              priceLineVisible: false,
            });
            macdSeries.setData(macdLineData as any);
          }

          if (signalLineData.length > 0) {
            const signalSeries = chart.addLineSeries({
              color: '#26c6da',
              lineWidth: 1 as any,
              title: 'Signal',
              priceScaleId: 'macd',
              lastValueVisible: true,
              priceLineVisible: false,
            });
            signalSeries.setData(signalLineData as any);
          }
        }

        // ADX 오버레이 (0-100 스케일)
        if (settings.adx) {
          const adxData = indicatorData
            .filter(d => d.adx !== undefined && d.adx !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.adx! }))
            .sort((a, b) => a.time - b.time);

          const plusDIData = indicatorData
            .filter(d => d.plusDI !== undefined && d.plusDI !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.plusDI! }))
            .sort((a, b) => a.time - b.time);

          const minusDIData = indicatorData
            .filter(d => d.minusDI !== undefined && d.minusDI !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.minusDI! }))
            .sort((a, b) => a.time - b.time);

          if (adxData.length > 0) {
            const adxSeries = chart.addLineSeries({
              color: '#9C27B0',
              lineWidth: 1 as any,
              title: 'ADX',
              priceScaleId: 'oscillator',
              lastValueVisible: true,
              priceLineVisible: false,
            });
            adxSeries.setData(adxData as any);
          }

          if (plusDIData.length > 0) {
            const plusDISeries = chart.addLineSeries({
              color: '#4CAF50',
              lineWidth: 1 as any,
              title: '+DI',
              priceScaleId: 'oscillator',
              lastValueVisible: true,
              priceLineVisible: false,
              lineStyle: 2, // Dashed
            });
            plusDISeries.setData(plusDIData as any);
          }

          if (minusDIData.length > 0) {
            const minusDISeries = chart.addLineSeries({
              color: '#f44336',
              lineWidth: 1 as any,
              title: '-DI',
              priceScaleId: 'oscillator',
              lastValueVisible: true,
              priceLineVisible: false,
              lineStyle: 2, // Dashed
            });
            minusDISeries.setData(minusDIData as any);
          }
        }

        // RSI 오버레이 (0-100 스케일)
        if (settings.rsi) {
          const rsiData = indicatorData
            .filter(d => d.rsi !== undefined && d.rsi !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.rsi! }))
            .sort((a, b) => a.time - b.time);

          if (rsiData.length > 0) {
            const rsiSeries = chart.addLineSeries({
              color: '#4CAF50',
              lineWidth: 1 as any,
              title: 'RSI',
              priceScaleId: 'oscillator',
              lastValueVisible: true,
              priceLineVisible: false,
            });
            rsiSeries.setData(rsiData as any);
          }
        }

        // CCI 오버레이 (-200~200 스케일)
        if (settings.cci) {
          const cciData = indicatorData
            .filter(d => d.cci !== undefined && d.cci !== null)
            .map(d => ({ time: Math.floor(new Date(d.time).getTime() / 1000), value: d.cci! }))
            .sort((a, b) => a.time - b.time);

          if (cciData.length > 0) {
            const cciSeries = chart.addLineSeries({
              color: '#ff5722',
              lineWidth: 1 as any,
              title: 'CCI',
              priceScaleId: 'cci',
              lastValueVisible: true,
              priceLineVisible: false,
            });
            cciSeries.setData(cciData as any);
          }
        }

        // 보조 Y축 설정 (oscillator: 0-100) - 하단 30% 영역 사용
        if (settings.stochasticSlow || settings.adx || settings.rsi) {
          chart.priceScale('oscillator').applyOptions({
            autoScale: true,
            scaleMargins: { top: 0.7, bottom: 0.0 },
            borderVisible: true,
            borderColor: '#FF9500',
          });
        }

        // MACD Y축 설정 - 하단 30% 영역 사용
        if (settings.macdLine) {
          chart.priceScale('macd').applyOptions({
            autoScale: true,
            scaleMargins: { top: 0.7, bottom: 0.0 },
            borderVisible: true,
            borderColor: '#e91e63',
          });
        }

        // CCI Y축 설정 (-200~200) - 하단 30% 영역 사용
        if (settings.cci) {
          chart.priceScale('cci').applyOptions({
            autoScale: true,
            scaleMargins: { top: 0.7, bottom: 0.0 },
            borderVisible: true,
            borderColor: '#ff5722',
          });
        }
      }

      // 매수/매도 마커
      // 🔍 디버깅: 2025-12-24 데이터 확인
      const debugData = uniqueData.filter(d => d.time.startsWith('2025-12-24T02:') || d.time.startsWith('2025-12-24T03:') || d.time.startsWith('2025-12-24T04:'));
      if (debugData.length > 0) {
        console.log('🔍 [CandlestickChart] uniqueData에서 2025-12-24 데이터:', debugData.map(d => ({
          time: d.time,
          isBuyPoint: d.isBuyPoint,
          isSellPoint: d.isSellPoint
        })));
      }

      const buyMarkers = uniqueData
        .filter((item) => item.isBuyPoint)
        .map((item) => ({
          time: new Date(item.time).getTime() / 1000,
          position: 'belowBar' as const,
          color: '#2196F3',
          shape: 'arrowUp' as const,
          text: '', // 텍스트는 HTML 오버레이로만 표시
        }));

      const sellMarkers = uniqueData
        .filter((item) => item.isSellPoint)
        .map((item) => ({
          time: new Date(item.time).getTime() / 1000,
          position: 'aboveBar' as const,
          color: '#f44336',
          shape: 'arrowDown' as const,
          text: '', // 텍스트는 HTML 오버레이로만 표시
        }));

      const normalizeMarkerTimeSec = (t: Time): number => {
        if (typeof t === 'number') return Math.floor(t >= 1e12 ? t / 1000 : t);
        if (typeof t === 'string') {
          const ms = new Date(t).getTime();
          return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
        }
        return 0;
      };

      const propAlertMarkers = (alertMarkers || []).map((m) => ({
        time: normalizeMarkerTimeSec(m.time),
        position: m.position,
        color: m.color,
        shape: m.shape,
        text: m.text ?? '',
      }));

      // ✅ 정렬: 시간순으로 정렬하여 마커 순서 보장 (백테스트 매수·매도 + 알림목록 등 prop 마커 병합)
      const allMarkers = [...buyMarkers, ...sellMarkers, ...propAlertMarkers].sort((a, b) => a.time - b.time);

      // ✅ Deep copy로 저장하여 참조 문제 방지
      markersRef.current = JSON.parse(JSON.stringify(allMarkers));

      tradeMarkerOverlaySpecsRef.current = [
        ...buyMarkers.map((m) => ({ timeSec: m.time as number, type: 'BUY' as const })),
        ...sellMarkers.map((m) => ({ timeSec: m.time as number, type: 'SELL' as const })),
      ];
      uniqueDataForTradeOverlayRef.current = uniqueData;

      console.log('🔍 [CandlestickChart] 마커 생성 및 저장:', {
        buyCount: buyMarkers.length,
        sellCount: sellMarkers.length,
        totalCount: allMarkers.length,
        buyTimes: buyMarkers.map(m => new Date(m.time * 1000).toISOString()),
        sellTimes: sellMarkers.map(m => new Date(m.time * 1000).toISOString())
      });

      candleSeries.setMarkers(markersRef.current as any);

      // ✅ 매수/매도 마커의 화면 좌표 계산 (차트 컨테이너 기준) — ref 기준으로 백테스트 이후에도 줌 시 최신 유지
      const updateTradeMarkerPositions = () => {
        if (!chart || !candleSeriesRef.current || !chartContainerRef.current) return;

        const specs = tradeMarkerOverlaySpecsRef.current;
        const uniqueOverlay = uniqueDataForTradeOverlayRef.current;
        const markers: Array<{ x: number; y: number; type: 'BUY' | 'SELL'; time: string }> = [];
        const timeScale = chart.timeScale();
        const candleSeries = candleSeriesRef.current;

        for (const spec of specs) {
          const x = timeScale.timeToCoordinate(spec.timeSec as any);
          if (x === null || x < 0) continue;

          const candle = uniqueOverlay.find(
            (d) => Math.abs(new Date(d.time).getTime() / 1000 - spec.timeSec) < 60
          );
          if (!candle) continue;

          if (spec.type === 'BUY') {
            const y = (candleSeries as any).priceToCoordinate(candle.low);
            if (y === null || y === undefined || y < 0) continue;
            markers.push({
              x,
              y: y + 28,
              type: 'BUY',
              time: candle.time,
            });
          } else {
            const y = (candleSeries as any).priceToCoordinate(candle.high);
            if (y === null || y === undefined || y < 0) continue;
            markers.push({
              x,
              y: y - 28,
              type: 'SELL',
              time: candle.time,
            });
          }
        }

        setTradeMarkers(markers);
      };

      // 초기 위치 계산
      setTimeout(updateTradeMarkerPositions, 100);

      // 차트 스크롤/줌 시 위치 업데이트
      const visibleRangeChangeHandler = () => {
        updateTradeMarkerPositions();
        
        // ✅ 사용자 인터랙션 감지: 범위 변경 시 플래그 설정
        isUserInteractingRef.current = true;
        
        // 1초 후 플래그 해제 (인터랙션 종료로 간주)
        setTimeout(() => {
          isUserInteractingRef.current = false;
        }, 1000);
      };
      chart.timeScale().subscribeVisibleTimeRangeChange(visibleRangeChangeHandler);

      // crosshair 이벤트
      chart.subscribeCrosshairMove((param) => {
        // ★ 외부 크로스헤어 설정 중이면 이벤트 무시 (무한 루프 방지)
        if (isSettingExternalCrosshairRef.current) {
          return;
        }

        // ✅ 업비트 스타일 크로스헤어: 전역 수직선 위치 업데이트
        if (param.point) {
          setGlobalCrosshairX(param.point.x);
          setActiveCrosshairChart('main');
        } else {
          setGlobalCrosshairX(null);
          setActiveCrosshairChart(null);
        }

        if (onCrosshairMoveRef.current) {
          onCrosshairMoveRef.current(param.time || null);
        }

        // 🔍 돋보기 모드: 확대 상태에서 마우스 이동 시 실시간 차트 갱신
        if (activeToolRef.current === 'magnifier') {
          const showingChart = magnifierShowChartRef.current;
          const justClicked = magnifierJustClickedRef.current;
          
          // 확대 차트가 표시 중이고, 클릭 직후가 아니며, 현재 시간이 있으면
          if (showingChart && !justClicked && param.time) {
            const newTime = param.time as number;
            console.log('🔍 [돋보기] 마우스 이동 - 차트 갱신:', {
              newTime: new Date(newTime * 1000).toISOString()
            });
            
            // 새로운 위치의 확대 차트로 갱신
            setMagnifierCenterTime(newTime);
          }
        }

        if (param.time && param.point && chartContainerRef.current) {
          const targetTime = param.time as number;
          const candleInfo = candleData.find(d => Math.floor(d.time) === Math.floor(targetTime));

          if (candleInfo) {
            const prevCandle = candleData.find((d, i) =>
              i < candleData.length - 1 && Math.floor(candleData[i + 1].time) === Math.floor(targetTime)
            );
            const prevClose = prevCandle ? prevCandle.close : candleInfo.open;
            const change = candleInfo.close - prevClose;
            const changePercent = prevClose !== 0 ? (change / prevClose) * 100 : 0;

            // MacdChart와 동일한 날짜 형식 사용: MM/dd HH:mm
            const date = new Date(targetTime * 1000);
            const month = (date.getMonth() + 1).toString().padStart(2, '0');
            const day = date.getDate().toString().padStart(2, '0');
            const hours = date.getHours().toString().padStart(2, '0');
            const minutes = date.getMinutes().toString().padStart(2, '0');
            const timeStr = `${month}/${day} ${hours}:${minutes}`;

            setTooltipData({
              visible: showTooltip, // ✅ showTooltip prop에 따라 툴팁 표시 여부 제어
              x: param.point.x,
              y: param.point.y,
              time: timeStr,
              open: candleInfo.open,
              high: candleInfo.high,
              low: candleInfo.low,
              close: candleInfo.close,
              change,
              changePercent,
              volume: candleInfo.volume || 0,
            });
          }
        } else {
          setTooltipData(null);
        }
      });

      // ✅ 차트 클릭 이벤트 (마커 클릭 + 일반 클릭)
      const clickHandler = (param: any) => {
        console.log(`🖱️ [차트 클릭 이벤트 발생]`, param);

        if (param.time) {
          const clickedTime = param.time as number;

          console.log(`🖱️ [차트 클릭] 시간: ${clickedTime}, activeTool: ${activeToolRef.current}`);

          // 🔍 돋보기 모드: 클릭으로 확대 차트 토글
          if (activeToolRef.current === 'magnifier') {
            const currentlyShowing = magnifierShowChartRef.current;
            
            console.log('🔍 [돋보기] 클릭 감지:', {
              clickedTime,
              timeStr: new Date(clickedTime * 1000).toISOString(),
              currentlyShowing,
              action: currentlyShowing ? '십자선 모드로 복귀' : '확대 차트 표시'
            });
            
            if (currentlyShowing) {
              // 확대 중일 때 클릭 → 십자선 모드로 복귀
              console.log('🔍 [돋보기] 십자선 모드로 복귀');
              setMagnifierShowChart(false);
              magnifierShowChartRef.current = false;
              magnifierClickPosRef.current = null;
              
              // 돋보기 차트 제거
              if (magnifierChartRef.current) {
                magnifierChartRef.current.chart?.remove();
                magnifierChartRef.current = null;
              }
              setMagnifierTradeMarkers([]);
            } else {
              // 십자선 모드일 때 클릭 → 확대 차트 표시
              console.log('🔍 [돋보기] 확대 차트 표시');
              setMagnifierCenterTime(clickedTime);
              setMagnifierShowChart(true);
              magnifierShowChartRef.current = true;
              
              // 클릭 시점의 마우스 위치 저장
              if (param.point) {
                magnifierClickPosRef.current = {
                  x: param.point.x,
                  y: param.point.y
                };
                console.log('🔍 [돋보기] 클릭 위치 저장:', magnifierClickPosRef.current);
              }
              
              // 클릭 직후 플래그 설정 (300ms 동안 마우스 이동으로 차트 갱신 안 함)
              magnifierJustClickedRef.current = true;
              setTimeout(() => {
                magnifierJustClickedRef.current = false;
                console.log('🔍 [돋보기] 마우스 이동으로 차트 갱신 활성화 (300ms 경과)');
              }, 300);
            }
            
            console.log('🔍 [돋보기] 처리 완료');
            return; // 돋보기 모드에서는 다른 클릭 동작 무시
          }

          // 1. 피보나치 타임존 등 일반 클릭 처리
          if (onChartClickRef.current) {
            console.log(`📞 [onChartClick 호출] 시간: ${clickedTime}`);
            onChartClickRef.current(clickedTime);
          } else {
            console.log(`⚠️ [onChartClick 없음]`);
          }

          // 2. 마커 더블클릭 처리 (매수/매도 신호)
          if (onMarkerClickRef.current) {
            // ✅ dataRef 디버깅
            console.log(`🔍 [dataRef 상태] 총 ${dataRef.current.length}개 항목`);
            if (dataRef.current.length > 0) {
              const firstItem = dataRef.current[0];
              console.log(`🔍 [dataRef 첫 항목]`, {
                time: firstItem.time,
                isBuyPoint: firstItem.isBuyPoint,
                isSellPoint: firstItem.isSellPoint,
                hasMarkerInfo: 'isBuyPoint' in firstItem || 'isSellPoint' in firstItem
              });
              
              // 마커가 있는 항목 찾기
              const markerItems = dataRef.current.filter(d => d.isBuyPoint || d.isSellPoint);
              console.log(`🔍 [마커 있는 항목] 총 ${markerItems.length}개`, markerItems.map(m => ({ time: m.time, buy: m.isBuyPoint, sell: m.isSellPoint })));
            }
            
            // ✅ 원본 data에서 클릭한 시간과 가까운 캔들 찾기 (1초 오차 허용)
            // dataRef.current를 사용하여 항상 최신 데이터 참조
            const clickedCandle = dataRef.current.find(d => {
              const dataTime = new Date(d.time).getTime() / 1000;
              return Math.abs(dataTime - clickedTime) < 1;
            });

            console.log(`🔍 [마커 클릭 검사] clickedCandle:`, clickedCandle);
            
            if (clickedCandle) {
              console.log(`🔍 [clickedCandle 상세]`, {
                time: clickedCandle.time,
                isBuyPoint: clickedCandle.isBuyPoint,
                isSellPoint: clickedCandle.isSellPoint,
                hasIsBuyPoint: 'isBuyPoint' in clickedCandle,
                hasIsSellPoint: 'isSellPoint' in clickedCandle,
                allKeys: Object.keys(clickedCandle)
              });
              
              // 해당 캔들에 매수/매도 신호가 있는지 확인
              const tradeType = clickedCandle.isBuyPoint ? 'BUY' : (clickedCandle.isSellPoint ? 'SELL' : null);
              
              console.log(`🔍 [마커 타입] tradeType: ${tradeType}, isBuyPoint: ${clickedCandle.isBuyPoint}, isSellPoint: ${clickedCandle.isSellPoint}`);
              
              if (tradeType) {
                const currentTime = Date.now();
                const lastClick = lastClickTimeRef.current;
                
                console.log(`🔍 [더블클릭 검사] lastClick:`, lastClick, `현재시간: ${currentTime}`);
                
                // 더블클릭 감지: 500ms 이내에 같은 마커를 다시 클릭
                if (lastClick && 
                    lastClick.timestamp === clickedCandle.time && 
                    lastClick.tradeType === tradeType &&
                    (currentTime - lastClick.time) < 500) {
                  // ✅ 더블클릭 - 팝업 표시
                  const timeDiff = currentTime - lastClick.time;
                  console.log(`🖱️🖱️ [마커 더블클릭 감지!] ${tradeType} at ${clickedCandle.time}, 시간차: ${timeDiff}ms`);
                  onMarkerClickRef.current(clickedCandle.time, tradeType);
                  lastClickTimeRef.current = null; // 초기화
                } else {
                  // ✅ 첫 번째 클릭 - 저장만 하고 대기
                  console.log(`🖱️ [마커 첫 클릭 저장] ${tradeType} at ${clickedCandle.time}`);
                  lastClickTimeRef.current = {
                    time: currentTime,
                    timestamp: clickedCandle.time,
                    tradeType: tradeType
                  };
                }
              } else {
                console.log(`⚠️ [마커 없음] 해당 캔들에 매수/매도 신호가 없습니다.`);
              }
            } else {
              console.log(`⚠️ [캔들 없음] 클릭한 시간에 해당하는 캔들을 찾을 수 없습니다.`);
            }
          }
        } else {
          console.log(`⚠️ [차트 클릭] param.time 없음`, param);
        }
      };

      chart.subscribeClick(clickHandler);
      console.log(`✅ [차트 생성] subscribeClick 이벤트 등록 완료`);

      // 시간축 변경 이벤트 (일목균형표 미래 영역 보호)
      const timeRangeChangeHandler = (range: any) => {
        if (range && onVisibleRangeChange && !isSettingRangeRef.current && !suppressExternalRangeSyncRef.current) {
          onVisibleRangeChange(range.from as number, range.to as number);
        }

        // 현재 visible range 저장 (스크롤바 업데이트용)
        if (range) {
          setCurrentVisibleRange({ from: range.from as number, to: range.to as number });
        }

        // ✅ 일목균형표 미래 영역 보호: 범위가 축소되어 미래 영역이 잘린 경우 복원
        const ichimokuEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;
        const hasFutureData = uniqueData.length > 0 && uniqueData.some(d => d.open === undefined && d.close === undefined);

        if (range && ichimokuEnabled && hasFutureData && !isSettingRangeRef.current && !isScrollingRef.current) {
          const allTimes = uniqueData.map(d => new Date(d.time).getTime() / 1000);
          const maxTime = Math.max(...allTimes);
          const currentMaxTime = range.to as number;

          // 미래 영역이 5% 이상 잘린 경우 경고
          if (currentMaxTime < maxTime * 0.95) {
            console.warn(`⚠️ [Ichimoku] 미래 영역 잘림 감지: ${currentMaxTime} < ${maxTime}`);
          }
        }
      };

      // ✅ Time Range 대신 Logical Range 변경 감지 (드래그 스크롤 시 깜빡임 방지)
      const logicalRangeChangeHandler = () => {
        if (isDisposedRef.current) return;
        if (!isSettingRangeRef.current) {
          // logical range에서 time range로 변환
          const visibleRange = chart.timeScale().getVisibleRange();
          if (visibleRange && onVisibleRangeChange && !suppressExternalRangeSyncRef.current) {
            onVisibleRangeChange(visibleRange.from as number, visibleRange.to as number);
          }
          
          // 현재 visible range 저장 (스크롤바 업데이트용)
          if (visibleRange) {
            setCurrentVisibleRange({ from: visibleRange.from as number, to: visibleRange.to as number });
          }
          
          // ✅ 외부 구독자들에게 logical range 변경 알림 (CandlestickTestPage의 독립 IndicatorChart 동기화)
          const logicalRange = chart.timeScale().getVisibleLogicalRange();
          if (logicalRange && externalLogicalRangeChangeCallbacksRef.current.size > 0 && !suppressExternalRangeSyncRef.current) {
            const rangeData = { from: logicalRange.from, to: logicalRange.to };
            externalLogicalRangeChangeCallbacksRef.current.forEach(callback => {
              try {
                callback(rangeData);
              } catch (e) {
                console.error('외부 logical range 콜백 오류:', e);
              }
            });
          }
        }
      };

      chart.timeScale().subscribeVisibleLogicalRangeChange(logicalRangeChangeHandler);

      // ❌ 줌 레벨 슬라이더 자동 업데이트 제거 (좌우 스크롤 시 스케일 변경 방지)
      // const updateZoomSlider = () => {
      //   if (isScrollingRef.current) return; // 슬라이더 조작 중에는 업데이트 안 함

      //   const logicalRange = chart.timeScale().getVisibleLogicalRange();
      //   if (logicalRange && uniqueData.length > 0) {
      //     const visibleBars = logicalRange.to - logicalRange.from;
      //     const totalBars = uniqueData.length;
      //     const minVisibleBars = 50;
      //     const maxVisibleBars = totalBars;

      //     // 현재 줌 레벨 계산 (0% = 전체, 100% = 최소)
      //     const zoomLevel = ((maxVisibleBars - visibleBars) / (maxVisibleBars - minVisibleBars)) * 100;
      //     const clampedZoom = Math.max(0, Math.min(100, zoomLevel));

      //     setScrollbarValue(clampedZoom);
      //   }
      // };

      // chart.timeScale().subscribeVisibleLogicalRangeChange(updateZoomSlider);

      // ✅ 일목균형표 미래 영역이 있으면 visible range 설정, 없으면 fitContent
      const ichimokuEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;
      const hasFutureData = uniqueData.length > 0 && uniqueData.some(d => d.open === undefined && d.close === undefined);
      // ✅ 멀티차트 등: calculateIchimoku + displacement 시 uniqueData에 미래 없음 → ichimokuDisplacementTimeRange 사용
      const hasDisplacementFuture = ichimokuDisplacementTimeRange !== null;

      // 🔍 돋보기 모드일 때는 자동 range 조정 건너뛰기
      const skipAutoRange = magnifierActive || isMagnifierZoomingRef.current;

      if (ichimokuEnabled && (hasFutureData || hasDisplacementFuture) && !skipAutoRange) {
        try {
          // 캔들 데이터와 미래 데이터 분리
          const candleOnlyData = uniqueData.filter(
            (d): d is CandleData => d != null && typeof d === 'object' && d.open !== undefined && d.close !== undefined
          );

          let candleStartTime: number;
          let lastDataTime: number;

          if (hasDisplacementFuture && ichimokuDisplacementTimeRange) {
            // ✅ 멀티차트 등: displacement로 계산된 구름 시간 범위 사용 (캔들 범위 포함됨)
            candleStartTime = ichimokuDisplacementTimeRange.from;
            lastDataTime = ichimokuDisplacementTimeRange.to;
            // ✅ 범위 유효성 검사 - NaN/역전 시 fitContent 사용
            if (candleData.length > 0 && (!isFinite(candleStartTime) || !isFinite(lastDataTime) || candleStartTime >= lastDataTime)) {
              const cs = candleData[0].time;
              const ce = candleData[candleData.length - 1].time;
              candleStartTime = cs;
              lastDataTime = ce;
              console.warn(`⚠️ [Ichimoku] visible range 유효하지 않음 - 캔들 범위로 대체: ${new Date(cs * 1000).toISOString()} ~ ${new Date(ce * 1000).toISOString()}`);
            } else {
              console.log(`📅 [최종] Ichimoku Displacement Visible Range: ${new Date(candleStartTime * 1000).toISOString()} ~ ${new Date(lastDataTime * 1000).toISOString()}`);
            }
          } else if (candleOnlyData.length > 0) {
            // ✅ 투자분석 등: uniqueData에 미래 데이터 포함
            console.log(`📅 [디버깅] uniqueData 처음 3개:`, uniqueData.slice(0, 3));
            console.log(`📅 [디버깅] uniqueData 마지막 10개:`, uniqueData.slice(-10));

            const allTimes = uniqueData
              .map((d, idx) => {
                if (!d) {
                  console.warn(`📅 [디버깅] uniqueData[${idx}]가 비어 있음`);
                  return 0;
                }
                const timeStr = d.time || (d as any).date;
                if (!timeStr) {
                  console.warn(`📅 [디버깅] uniqueData[${idx}]에 time/date 없음:`, d);
                  return 0;
                }
                const timestamp = new Date(timeStr).getTime() / 1000;
                if (idx >= uniqueData.length - 10) {
                  console.log(`📅 [디버깅] uniqueData[${idx}] time=${timeStr} → timestamp=${timestamp} (${new Date(timestamp * 1000).toISOString()})`);
                }
                return timestamp;
              })
              .filter(t => t > 0);

            candleStartTime = new Date(candleOnlyData[0].time).getTime() / 1000;
            lastDataTime = Math.max(...allTimes);

            console.log(`📅 [최종] allTimes.length=${allTimes.length}, min=${Math.min(...allTimes)} (${new Date(Math.min(...allTimes) * 1000).toISOString()}), max=${lastDataTime} (${new Date(lastDataTime * 1000).toISOString()})`);
            console.log(`📅 [최종] 전체 데이터 시간 범위: ${new Date(candleStartTime * 1000).toISOString()} ~ ${new Date(lastDataTime * 1000).toISOString()}`);
            console.log(`📅 [최종] 캔들=${candleOnlyData.length}개, 전체=${uniqueData.length}개, 미래=${uniqueData.length - candleOnlyData.length}개`);
          } else {
            candleStartTime = 0;
            lastDataTime = 0;
          }

          if (candleOnlyData.length > 0 || hasDisplacementFuture) {
            // ✅ 캔들 범위가 visible range에 포함되는지 최종 검증
            if (candleData.length > 0) {
              const cs = candleData[0].time;
              const ce = candleData[candleData.length - 1].time;
              if (candleStartTime > cs || lastDataTime < ce) {
                candleStartTime = Math.min(candleStartTime, cs);
                lastDataTime = Math.max(lastDataTime, ce);
                console.log(`📅 [최종] visible range 캔들 포함 보정: ${new Date(candleStartTime * 1000).toISOString()} ~ ${new Date(lastDataTime * 1000).toISOString()}`);
              }
            }
            if (isFinite(candleStartTime) && isFinite(lastDataTime) && candleStartTime < lastDataTime) {
              console.log(`📅 [최종] Ichimoku Visible Range 설정 (deferred): ${new Date(candleStartTime * 1000).toISOString()} ~ ${new Date(lastDataTime * 1000).toISOString()}`);
              isSettingRangeRef.current = true;
              // ✅ 두 프레임 뒤에 실행 - Line 시리즈(선행스팬)가 완전히 부착되고 컨테이너 크기가 안정된 후 setVisibleRange (Value is null 방지)
              requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                  if (isDisposedRef.current || !chartRef.current) return;
                  const cw = chartContainerRef.current?.clientWidth ?? 0;
                  if (cw <= 0) {
                    console.warn(`⏸️ [Ichimoku] setVisibleRange 스킵 - 컨테이너 width=0 (멀티차트 레이아웃 대기)`);
                    return;
                  }
                  try {
                    chartRef.current!.timeScale().setVisibleRange({
                      from: candleStartTime as Time,
                      to: lastDataTime as Time,
                    });
                  } catch (e) {
                    console.warn('⚠️ [Ichimoku] setVisibleRange deferred 오류:', e);
                  }
                });
              });
            } else {
              // ✅ 일목균형표 비활성 시: 전체 데이터 범위를 보이도록 logical range 설정 (fitContent 대신)
              console.log(`📊 [초기 로드] 전체 데이터 범위로 설정 (일목 비활성)`);
              requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                  if (isDisposedRef.current || !chartRef.current) return;
                  const cw = chartContainerRef.current?.clientWidth ?? 0;
                  if (cw <= 0) {
                    console.warn(`⏸️ [초기 로드] logical range 스킵 - 컨테이너 width=0`);
                    return;
                  }
                  try {
                    // ✅ 전체 데이터를 보이도록 logical range 설정
                    const dataLength = uniqueData.length;
                    if (dataLength > 0) {
                      chartRef.current!.timeScale().setVisibleLogicalRange({
                        from: 0,
                        to: Math.max(0, dataLength - 1)
                      });
                      console.log(`✅ [초기 로드] logical range 설정 완료: 0 ~ ${dataLength - 1}`);
                    }
                  } catch (e) {
                    console.warn('⚠️ [초기 로드] logical range 설정 오류:', e);
                  }
                });
              });
            }

            // ✅ 초기 logical range 및 barSpacing 저장 (재설정 기능용)
            setTimeout(() => {
              if (isDisposedRef.current || !chartRef.current) return;
              const initialLogicalRange = chartRef.current.timeScale().getVisibleLogicalRange();
              if (initialLogicalRange && !initialLogicalRangeRef.current) {
                initialLogicalRangeRef.current = {
                  from: initialLogicalRange.from as number,
                  to: initialLogicalRange.to as number
                };
                
                // ✅ 초기 barSpacing 저장
                const timeScaleOptions = chartRef.current!.timeScale().options();
                initialBarSpacingRef.current = timeScaleOptions.barSpacing || 8;
                
                console.log('💾 [초기 설정 저장 - Ichimoku]', {
                  logicalRange: initialLogicalRangeRef.current,
                  barSpacing: initialBarSpacingRef.current
                });
              }
            }, 100);

            // 전체 시간 범위 저장 (스크롤바용)
            setTotalTimeRange({ from: candleStartTime, to: lastDataTime });

            // ✅ 1000ms 후 플래그 해제 (Ichimoku 미래 영역이 안정화될 때까지)
            setTimeout(() => {
              isSettingRangeRef.current = false;
              console.log(`📅 [최종] Ichimoku 범위 변경 이벤트 재활성화 (1000ms 경과)`);
            }, 1000);
          } else if (!skipAutoRange) {
            // ✅ fitContent 대신 명시적 logical range 설정 (이동평균선 렌더링 전 호출 문제 해결)
            console.log(`📅 [최종 - ichimoku inner] Ichimoku OFF - 명시적 logical range 설정`);
            
            // 전체 데이터 범위 계산
            const logicalFrom = 0;
            const logicalTo = candleData.length - 1;
            
            // priceScale 강제 업데이트
            chart.applyOptions({
              rightPriceScale: {
                autoScale: true,
              }
            });
            
            // 명시적으로 전체 범위 설정
            try {
              chart.timeScale().setVisibleLogicalRange({
                from: logicalFrom,
                to: logicalTo
              });
              console.log(`📅 [최종 - ichimoku inner] logical range 설정 완료: ${logicalFrom} ~ ${logicalTo}`);
            } catch (e) {
              console.warn('⚠️ [최종 - ichimoku inner] logical range 설정 실패:', e);
            }
            
            // ✅ 초기 logical range 및 barSpacing 저장 (재설정 기능용)
            const initialLogicalRange = chart.timeScale().getVisibleLogicalRange();
            if (initialLogicalRange && !initialLogicalRangeRef.current) {
              initialLogicalRangeRef.current = {
                from: initialLogicalRange.from as number,
                to: initialLogicalRange.to as number
              };
              
              // ✅ 초기 barSpacing 저장
              const timeScaleOptions = chart.timeScale().options();
              initialBarSpacingRef.current = timeScaleOptions.barSpacing || 8;
              
              console.log('💾 [초기 설정 저장 - ichimoku inner]', {
                logicalRange: initialLogicalRangeRef.current,
                barSpacing: initialBarSpacingRef.current
              });
            }

            // 전체 시간 범위 저장 (time 또는 date 필드 확인)
            const allTimes = uniqueData
              .map(d => {
                const timeStr = d.time || (d as any).date;
                if (!timeStr) return 0;
                return new Date(timeStr).getTime() / 1000;
              })
              .filter(t => t > 0);
            if (allTimes.length > 0) {
              setTotalTimeRange({ from: Math.min(...allTimes), to: Math.max(...allTimes) });
            }
          }
        } catch (e) {
          console.error('❌ [최종] Visible Range 설정 오류:', e);
          if (!skipAutoRange) {
            // ✅ fitContent 대신 명시적 logical range 설정 (이동평균선 렌더링 전 호출 문제 해결)
            console.log(`📅 [최종 - catch] Ichimoku OFF - 명시적 logical range 설정`);
            
            // 전체 데이터 범위 계산
            const logicalFrom = 0;
            const logicalTo = candleData.length - 1;
            
            // priceScale 강제 업데이트
            chart.applyOptions({
              rightPriceScale: {
                autoScale: true,
              }
            });
            
            // 명시적으로 전체 범위 설정
            try {
              chart.timeScale().setVisibleLogicalRange({
                from: logicalFrom,
                to: logicalTo
              });
              console.log(`📅 [최종 - catch] logical range 설정 완료: ${logicalFrom} ~ ${logicalTo}`);
            } catch (e2) {
              console.warn('⚠️ [최종 - catch] logical range 설정 실패:', e2);
            }
            
            // ✅ 초기 logical range 및 barSpacing 저장 (재설정 기능용)
            const initialLogicalRange = chart.timeScale().getVisibleLogicalRange();
            if (initialLogicalRange && !initialLogicalRangeRef.current) {
              initialLogicalRangeRef.current = {
                from: initialLogicalRange.from as number,
                to: initialLogicalRange.to as number
              };
              
              // ✅ 초기 barSpacing 저장
              const timeScaleOptions = chart.timeScale().options();
              initialBarSpacingRef.current = timeScaleOptions.barSpacing || 8;
              
              console.log('💾 [초기 설정 저장 - catch]', {
                logicalRange: initialLogicalRangeRef.current,
                barSpacing: initialBarSpacingRef.current
              });
            }
          }
        }
      } else if (!skipAutoRange) {
        console.log(`📅 [최종] Ichimoku OFF 또는 미래 데이터 없음 - fitContent 사용`);
        
        // ✅ fitContent 대신 명시적 logical range 설정 (이동평균선 렌더링 전 호출 문제 해결)
        console.log(`📅 [최종] Ichimoku OFF - 명시적 logical range 설정`);
        
        // 전체 데이터 범위 계산
        const logicalFrom = 0;
        const logicalTo = candleData.length - 1;
        
        // priceScale 강제 업데이트
        chart.applyOptions({
          rightPriceScale: {
            autoScale: true,
          }
        });
        
        // 명시적으로 전체 범위 설정
        try {
          chart.timeScale().setVisibleLogicalRange({
            from: logicalFrom,
            to: logicalTo
          });
          console.log(`📅 [최종] logical range 설정 완료: ${logicalFrom} ~ ${logicalTo}`);
        } catch (e) {
          console.warn('⚠️ [최종] logical range 설정 실패:', e);
        }
        
        // ✅ 초기 logical range 및 barSpacing 저장 (재설정 기능용)
        const initialLogicalRange = chart.timeScale().getVisibleLogicalRange();
        if (initialLogicalRange && !initialLogicalRangeRef.current) {
          initialLogicalRangeRef.current = {
            from: initialLogicalRange.from as number,
            to: initialLogicalRange.to as number
          };
          
          // ✅ 초기 barSpacing 저장
          const timeScaleOptions = chart.timeScale().options();
          initialBarSpacingRef.current = timeScaleOptions.barSpacing || 8;
          
          console.log('💾 [초기 설정 저장]', {
            logicalRange: initialLogicalRangeRef.current,
            barSpacing: initialBarSpacingRef.current
          });
        }

        // 전체 시간 범위 저장 (time 또는 date 필드 확인)
        const allTimes = uniqueData
          .map(d => {
            const timeStr = d.time || (d as any).date;
            if (!timeStr) return 0;
            return new Date(timeStr).getTime() / 1000;
          })
          .filter(t => t > 0);
        if (allTimes.length > 0) {
          setTotalTimeRange({ from: Math.min(...allTimes), to: Math.max(...allTimes) });
        }
      }

      // ✅ 차트 재생성 후 저장된 사용자 뷰 우선 복원
      // 새 봉 수신/증분 업데이트 시 기본 범위(0~N)로 튕기는 문제 방지
      if (preservedViewRangeRef.current && !skipAutoRange) {
        const saved = preservedViewRangeRef.current;
        const savedSpan = saved.to - saved.from;
        if (savedSpan < MIN_LOGICAL_BARS_TO_PERSIST) {
          console.log(
            `⏸️ [재생성 후 뷰 복원 스킵] 저장 논리 범위 ${savedSpan.toFixed(1)}봉 < ${MIN_LOGICAL_BARS_TO_PERSIST} (극단 줌 고착 방지)`
          );
          preservedViewRangeRef.current = null;
        } else {
        const maxLogicalTo = Math.max(0, candleData.length - 1 + 220);
        const rangeSize = Math.max(1, saved.to - saved.from);

        let nextFrom = saved.from;
        let nextTo = saved.to;
        if (!Number.isFinite(nextFrom) || !Number.isFinite(nextTo) || nextTo <= nextFrom) {
          nextFrom = Math.max(0, maxLogicalTo - rangeSize);
          nextTo = nextFrom + rangeSize;
        }

        if (nextTo > maxLogicalTo) {
          nextTo = maxLogicalTo;
          nextFrom = nextTo - rangeSize;
        }
        if (nextFrom < 0) {
          nextFrom = 0;
          nextTo = nextFrom + rangeSize;
        }

        try {
          chart.timeScale().setVisibleLogicalRange({
            from: nextFrom,
            to: nextTo,
          });
          chart.timeScale().scrollToPosition(saved.scrollPosition, false);
          console.log(
            `✅ [재생성 후 뷰 복원] logical: ${nextFrom.toFixed(2)} ~ ${nextTo.toFixed(2)}, scroll: ${saved.scrollPosition.toFixed(2)}`
          );
        } catch (e) {
          console.warn('⚠️ [재생성 후 뷰 복원 실패]:', e);
        } finally {
          preservedViewRangeRef.current = null;
        }
        }
      }

      // 차트 준비 완료 상태 설정 (크로스헤어 동기화를 위해)
      setChartReady(true);

      // 초기 visible range 콜백 호출 (fitContent 후)
      if (candleData.length > 0) {
        setTimeout(() => {
          const visibleRange = chart.timeScale().getVisibleRange();
          if (visibleRange) {
            if (onTimeRangeChange) {
              onTimeRangeChange(visibleRange.from as number, visibleRange.to as number);
            }
            if (onVisibleRangeChangeRef.current) {
              onVisibleRangeChangeRef.current(visibleRange.from as number, visibleRange.to as number);
            }

            // ✅ visible range 변경 시 마커 재설정
            if (candleSeries && markersRef.current.length > 0) {
              console.log('🔄 [onVisibleRangeChange] 마커 재설정:', markersRef.current.length, '개');
              candleSeries.setMarkers(markersRef.current as any);
            }
          }
        }, 50);
      }

      // ✅ 마우스 휠 이벤트는 페이지 스크롤로 동작 (줌 기능 제거)
      // 별도의 줌 컨트롤 슬라이더를 하단에 제공

      // ✅ visible range 변경 시 마커 재설정 (줌인/줌아웃 시 마커 사라짐 방지)
      // ✅ Logical Range + Time Range 모두 구독 (더 강력한 마커 유지)
      const reapplyMarkers = (source: string) => {
        const series = candleSeriesRef.current;
        const markers = markersRef.current;

        if (!series) {
          console.warn(`⚠️ [${source}] candleSeries가 null입니다`);
          return;
        }

        if (!markers || markers.length === 0) {
          // 마커가 없는 것은 정상 (백테스트 미실행 시)
          return;
        }

        console.log(`🔄 [${source}] 마커 재설정 시작 - 마커 개수:`, markers.length, {
          buyCount: markers.filter((m: any) => m.position === 'belowBar').length,
          sellCount: markers.filter((m: any) => m.position === 'aboveBar').length
        });

        // ✅ Deep copy로 마커 배열 복사 (참조 문제 방지)
        const markersCopy = JSON.parse(JSON.stringify(markers));

        // 즉시 실행
        series.setMarkers(markersCopy as any);

        // requestAnimationFrame으로도 실행 (렌더링 사이클에 맞춰)
        requestAnimationFrame(() => {
          if (candleSeriesRef.current && markersRef.current.length > 0) {
            const markers2 = JSON.parse(JSON.stringify(markersRef.current));
            candleSeriesRef.current.setMarkers(markers2 as any);
            console.log(`✅ [${source}+RAF] 마커 재설정 완료`);
          }
        });

        // 100ms 후에도 재실행 (보험)
        setTimeout(() => {
          if (candleSeriesRef.current && markersRef.current.length > 0) {
            const markers3 = JSON.parse(JSON.stringify(markersRef.current));
            candleSeriesRef.current.setMarkers(markers3 as any);
            console.log(`✅ [${source}+100ms] 마커 재설정 완료`);
          }
        }, 100);
      };

      chart.timeScale().subscribeVisibleLogicalRangeChange(() => {
        reapplyMarkers('LogicalRangeChange');
      });

      chart.timeScale().subscribeVisibleTimeRangeChange(() => {
        reapplyMarkers('TimeRangeChange');
      });

      const handleResize = () => {
        if (isDisposedRef.current || !chartContainerRef.current || !chartRef.current) return;
        
        // 컨테이너가 숨겨진 상태(0px)이면 resize 적용 건너뜀 — 0px 적용 시 라이브러리 내부에서
        // 다시 resize 이벤트가 발생해 무한 루프가 생기는 것을 방지
        const newWidth = chartContainerRef.current.clientWidth;
        if (newWidth <= 0) return;

        try {
          chartRef.current.applyOptions({ width: newWidth });
          
          // ✅ 탭 복귀 시 저장된 범위 복원 (단, 한 번만 복원)
          if (savedVisibleRangeBeforeHiddenRef.current) {
            const savedRange = savedVisibleRangeBeforeHiddenRef.current;
            savedVisibleRangeBeforeHiddenRef.current = null; // ⚡ 즉시 초기화하여 중복 복원 방지
            
            setTimeout(() => {
              if (isDisposedRef.current || !chartRef.current) return; // ✅ 차트 파괴 여부 재확인
              
              try {
                const span = savedRange.logicalTo - savedRange.logicalFrom;
                const n = dataRef.current?.length ?? 0;
                if (span < MIN_LOGICAL_BARS_TO_PERSIST && n > MIN_LOGICAL_BARS_TO_PERSIST) {
                  chartRef.current.timeScale().setVisibleLogicalRange({
                    from: 0 as any,
                    to: (n - 1) as any,
                  });
                  console.log('🔄 [Resize 시 범위 복원] 극단 줌 감지 → 전체 논리봉으로 복원', { n, span });
                } else {
                  chartRef.current.timeScale().setVisibleRange({
                    from: savedRange.from as Time,
                    to: savedRange.to as Time,
                  });
                  console.log('🔄 [Resize 시 범위 복원]', savedRange);
                }
              } catch (e) {
                console.error('Resize 시 범위 복원 오류:', e);
              }
            }, 50);
          }
        } catch (e) {
          console.error('[CandlestickChart] handleResize 오류:', e);
        }
      };

      // ✅ 탭 전환 시 차트 범위 보존 (Page Visibility API)
      const handleVisibilityChange = () => {
        if (isDisposedRef.current || !chartRef.current) return;
        
        if (document.hidden) {
          // 탭이 숨겨질 때: 현재 범위 저장 (극단 줌은 저장하지 않음 → 복귀 시 망가진 뷰 고착 방지)
          try {
            const visibleRange = chartRef.current.timeScale().getVisibleRange();
            const visibleLogicalRange = chartRef.current.timeScale().getVisibleLogicalRange();
            const span = logicalBarsVisible(visibleLogicalRange);
            if (visibleRange && visibleLogicalRange && span >= MIN_LOGICAL_BARS_TO_PERSIST) {
              savedVisibleRangeBeforeHiddenRef.current = {
                from: visibleRange.from as number,
                to: visibleRange.to as number,
                logicalFrom: visibleLogicalRange.from,
                logicalTo: visibleLogicalRange.to,
              };
              console.log('💾 [탭 숨김] 차트 범위 저장:', savedVisibleRangeBeforeHiddenRef.current);
            } else {
              savedVisibleRangeBeforeHiddenRef.current = null;
              if (visibleLogicalRange) {
                console.log(
                  `⏸️ [탭 숨김] 범위 저장 스킵 — 논리봉 ${span.toFixed(1)}개 < ${MIN_LOGICAL_BARS_TO_PERSIST} (극단 줌)`
                );
              }
            }
          } catch (e) {
            console.error('탭 숨김 시 범위 저장 오류:', e);
          }
        } else {
          // 탭이 보일 때: 저장된 범위 복원
          if (savedVisibleRangeBeforeHiddenRef.current) {
            const savedRange = savedVisibleRangeBeforeHiddenRef.current;
            savedVisibleRangeBeforeHiddenRef.current = null; // ⚡ 즉시 초기화하여 중복 복원 방지
            
            setTimeout(() => {
              if (isDisposedRef.current || !chartRef.current) return; // ✅ 차트 파괴 여부 재확인
              
              try {
                const span = savedRange.logicalTo - savedRange.logicalFrom;
                const n = dataRef.current?.length ?? 0;
                if (span < MIN_LOGICAL_BARS_TO_PERSIST && n > MIN_LOGICAL_BARS_TO_PERSIST) {
                  chartRef.current.timeScale().setVisibleLogicalRange({
                    from: 0 as any,
                    to: (n - 1) as any,
                  });
                  console.log('🔄 [탭 복귀] 극단 줌 감지 → 전체 논리봉으로 복원', { n, span });
                } else {
                  chartRef.current.timeScale().setVisibleRange({
                    from: savedRange.from as Time,
                    to: savedRange.to as Time,
                  });
                  console.log('🔄 [탭 복귀] 차트 범위 복원:', savedRange);
                }
              } catch (e) {
                console.error('탭 복귀 시 범위 복원 오류:', e);
              }
            }, 150);
          }
        }
      };

      window.addEventListener('resize', handleResize);
      document.addEventListener('visibilitychange', handleVisibilityChange);

      // ✅ 차트 준비 완료 플래그 설정 (pending 크로스헤어는 별도 useEffect에서 처리)
      setTimeout(() => {
        isChartReadyRef.current = true;
        setChartReady(true); // ✅ state 업데이트 → 별도 useEffect에서 pending 크로스헤어 자동 적용
        console.log(`✅ [CandlestickChart] 차트 준비 완료`);
      }, 100); // 차트 완전 초기화 대기 시간 증가

      return () => {
        // ✅ cleanup 시작 - 모든 핸들러 즉시 무시
        isDisposedRef.current = true;
        ichimokuFlatCandleTimesRef.current = [];

        // ✅ Debounce 타이머 정리
        if (debounceTimerRef.current) {
          clearTimeout(debounceTimerRef.current);
          debounceTimerRef.current = null;
        }
        
        // ✅ ResizeObserver 정리
        if (resizeObserverRef.current) {
          resizeObserverRef.current.disconnect();
          resizeObserverRef.current = null;
        }
        
        // ✅ cleanup: 이벤트 리스너 제거
        window.removeEventListener('resize', handleResize);
        document.removeEventListener('visibilitychange', handleVisibilityChange);
        
        // ✅ fitContent 타이머 취소 (dispose 후 콜백 실행 방지)
        fitContentTimeoutsRef.current.forEach((id) => window.clearTimeout(id));
        fitContentTimeoutsRef.current = [];
        
        // ✅ cleanup: 차트 제거 (unmount 시 "Value is null" 방지 - lightweight-charts dispose)
        if (chartRef.current) {
          try {
            const ch = chartRef.current;
            chartRef.current = null;
            candleSeriesRef.current = null;
            // ✅ 실시간 업데이트용 series refs 초기화
            maSeriesMapRef.current.clear();
            bollingerSeriesRef.current = {};
            if (ch && typeof ch.remove === 'function') ch.remove();
          } catch (e) {
            const msg = e instanceof Error ? e.message : String(e);
            if (!msg.includes('disposed') && !msg.includes('null')) console.warn('[메인차트 cleanup] 차트 제거 중:', e);
          }
        }
      };
    // ✅ indicatorData/allIndicatorData 제거: 보조지표 API 도착 시 메인 차트 전체 dispose→재생성 방지
    // 메인 차트(캔들/MA/일목/볼린저)는 data/maCalculationData로 렌더링됨. indicatorData는 showIndicatorOverlay용이지만,
    // deps에 있으면 API 응답마다 chart.remove() → 서브차트까지 연쇄 dispose되는 문제 발생
    // ✅ data/maCalculationData 참조 제거: WebSocket 마지막 캔들만 갱신 시 setChartData→전체 차트 dispose→좌측 이동 버그 방지
    // → data?.length, data?.[0]?.time, maCalculationData?.length 로 구조 변경만 감지. 마지막 캔들 OHLC만 변경 시 effect 미실행.
    // ✅ externalForceUpdate 제외: 투자분석에서 WS마다 indicatorUpdateCounter 증가 시 매 틱 메인 차트 전체 재생성되어
    //    updateLastCandle()로 갱신한 캔들이 즉시 초기화되는 문제 발생 (실시간 캔들/지표가 멈춘 것처럼 보임).
    }, [
      data?.length,
      data?.[0]?.time,
      maCalculationData?.length,
      mainChartColorsStr,
      effectiveIndicatorSettingsKey,
      chartViewMode,
      maLinesKey,
      ichimokuSettingsKey,
      ichimokuColorsKey,
      settings?.bollingerUpper,
      settings?.bollingerMiddle,
      settings?.bollingerLower,
      settings?.bollingerPeriod,
      settings?.bollingerStdDev,
      overlaySettingsOverrideKey,
      forceUpdateCounter,
    ]);

    // ✅ alertMarkers만 변경된 경우(같은 종목·같은 캔들 길이) 메인 차트 effect가 돌지 않아 백테스트 마커에 알림 마커가 합쳐지지 않는 문제 보완
    // ✅ dataTradeMarkerSyncKey: 투자실행 후 캔들 개수·첫봉이 동일해도 매수·매도 플래그만 바뀌면 setMarkers·오버레이 갱신
    useEffect(() => {
      const series = candleSeriesRef.current;
      if (!series || isDisposedRef.current) return;
      const markerData = dataForMarkersRef.current;
      if (!markerData || markerData.length === 0) {
        tradeMarkerOverlaySpecsRef.current = [];
        uniqueDataForTradeOverlayRef.current = [];
        markersRef.current = [];
        try {
          series.setMarkers([]);
        } catch (e) {
          const msg = e instanceof Error ? e.message : String(e);
          if (!msg.includes('disposed')) console.warn('[CandlestickChart] 마커 초기화 setMarkers:', e);
        }
        setTradeMarkers([]);
        return;
      }

      const uniqueData: CandleData[] = [];
      const timeSet = new Set<string>();
      for (const item of markerData) {
        const t = item.time != null ? String(item.time) : '';
        if (t && !timeSet.has(t)) {
          uniqueData.push(item);
          timeSet.add(t);
        }
      }

      const buyMarkers = uniqueData
        .filter((item) => item.isBuyPoint)
        .map((item) => ({
          time: new Date(item.time).getTime() / 1000,
          position: 'belowBar' as const,
          color: '#2196F3',
          shape: 'arrowUp' as const,
          text: '',
        }));

      const sellMarkers = uniqueData
        .filter((item) => item.isSellPoint)
        .map((item) => ({
          time: new Date(item.time).getTime() / 1000,
          position: 'aboveBar' as const,
          color: '#f44336',
          shape: 'arrowDown' as const,
          text: '',
        }));

      const normalizeMarkerTimeSec = (t: Time): number => {
        if (typeof t === 'number') return Math.floor(t >= 1e12 ? t / 1000 : t);
        if (typeof t === 'string') {
          const ms = new Date(t).getTime();
          return Number.isFinite(ms) ? Math.floor(ms / 1000) : 0;
        }
        return 0;
      };

      const propAlertMarkers = (alertMarkers || []).map((m) => ({
        time: normalizeMarkerTimeSec(m.time),
        position: m.position,
        color: m.color,
        shape: m.shape,
        text: m.text ?? '',
      }));

      const allMarkers = [...buyMarkers, ...sellMarkers, ...propAlertMarkers].sort((a, b) => a.time - b.time);
      markersRef.current = JSON.parse(JSON.stringify(allMarkers));

      tradeMarkerOverlaySpecsRef.current = [
        ...buyMarkers.map((m) => ({ timeSec: m.time as number, type: 'BUY' as const })),
        ...sellMarkers.map((m) => ({ timeSec: m.time as number, type: 'SELL' as const })),
      ];
      uniqueDataForTradeOverlayRef.current = uniqueData;

      try {
        series.setMarkers(markersRef.current as any);
      } catch (e) {
        const msg = e instanceof Error ? e.message : String(e);
        if (!msg.includes('disposed')) console.warn('[CandlestickChart] alertMarkers 동기화 setMarkers:', e);
      }

      const chartApi = chartRef.current;
      const candleSeries = candleSeriesRef.current;
      if (chartApi && candleSeries && chartContainerRef.current) {
        const timeScale = chartApi.timeScale();
        const positions: Array<{ x: number; y: number; type: 'BUY' | 'SELL'; time: string }> = [];
        for (const spec of tradeMarkerOverlaySpecsRef.current) {
          const x = timeScale.timeToCoordinate(spec.timeSec as any);
          if (x === null || x < 0) continue;
          const candle = uniqueData.find(
            (d) => Math.abs(new Date(d.time).getTime() / 1000 - spec.timeSec) < 60
          );
          if (!candle) continue;
          if (spec.type === 'BUY') {
            const y = (candleSeries as any).priceToCoordinate(candle.low);
            if (y === null || y === undefined || y < 0) continue;
            positions.push({ x, y: y + 28, type: 'BUY', time: candle.time });
          } else {
            const y = (candleSeries as any).priceToCoordinate(candle.high);
            if (y === null || y === undefined || y < 0) continue;
            positions.push({ x, y: y - 28, type: 'SELL', time: candle.time });
          }
        }
        setTradeMarkers(positions);
      }
    }, [alertMarkersKey, data?.length, data?.[0]?.time, dataTradeMarkerSyncKey]);

    // ✅ height 변경 시 차트 재생성 대신 applyOptions로 리사이즈 (드래그 높이 조절 시 차트 사라짐 방지)
    useEffect(() => {
      const chart = chartRef.current;
      const container = chartContainerRef.current;
      if (!chart || !container || !candleSeriesRef.current || data.length === 0) return;
      const cw = container.clientWidth;
      const ch = container.clientHeight;
      const effectiveHeight = (ch > 0 ? ch : height) || height;
      if (cw <= 0 || effectiveHeight < 80) return;
      try {
        chart.applyOptions({ width: cw, height: Math.max(80, effectiveHeight) });
      } catch (e) {
        const msg = e instanceof Error ? e.message : String(e);
        if (!msg.includes('disposed')) console.warn('[CandlestickChart] height resize applyOptions:', e);
      }
    }, [height, data.length]);

    // ✅ 보조지표 종류 변경 시에도 메인 우측 가격축 너비를 스택과 맞춤 (메인 effect는 subPanels 미의존으로 재생성 안 함)
    useEffect(() => {
      const chart = chartRef.current;
      if (!chart || isDisposedRef.current) return;
      try {
        const w = stackedPanelsRightPriceScaleMinWidth(subPanels, isMobileMode);
        chart.applyOptions({
          rightPriceScale: { minimumWidth: w },
        });
      } catch {
        /* ignore */
      }
    }, [subPanels, isMobileMode]);

    // ✅ 서브차트 생성 및 업데이트 (TradingView 스타일)
    useEffect(() => {
      if (!subPanels || subPanels.length === 0 || !chartRef.current) {
        return;
      }

      const enabledPanels = subPanels.filter(p => p.enabled);
      if (enabledPanels.length === 0) {
        return;
      }

      // 드래그/리사이즈 중에는 차트 재생성 중지 (기존 차트 유지)
      if (isResizingRef.current || isDraggingRef.current) {
        console.log(`⏸️ [서브차트] 리사이즈/드래그 중 - 차트 재생성 건너뜀`);
        return;
      }

      console.log(`🎨 [서브차트] 생성 시작: ${enabledPanels.length}개 패널`);
      console.log(`🎨 [서브차트] data 샘플:`, data?.slice(0, 2));
      console.log(`🎨 [서브차트] indicatorData 샘플:`, indicatorData?.slice(0, 2));
      console.log(`🎨 [서브차트] indicatorData[0] 필드:`, indicatorData && indicatorData.length > 0 ? Object.keys(indicatorData[0]) : 'no data');

      // ✅ 컨테이너 체크를 dispose **이전에** 수행 (한번 그렸다가 바로 사라지는 현상 방지)
      // chart.remove() 후 빈 컨테이너가 flex에서 width=0으로 접히면, 이후 zeroWidth 체크에 걸려 생성이 스킵됨
      const missingContainers = enabledPanels.filter((p) => !subChartContainersRef.current[p.type]);
      const zeroWidthContainers = enabledPanels.filter((p) => {
        const c = subChartContainersRef.current[p.type];
        return c && c.clientWidth <= 0;
      });
      if (missingContainers.length > 0) {
        console.warn(`서브차트 컨테이너 대기: ${missingContainers.map((p) => p.type).join(', ')}`);
        const t1 = setTimeout(() => setForceUpdateCounter((c) => c + 1), 150);
        const t2 = setTimeout(() => setForceUpdateCounter((c) => c + 1), 500);
        return () => { clearTimeout(t1); clearTimeout(t2); };
      }
      if (zeroWidthContainers.length > 0) {
        console.warn(`서브차트 컨테이너 width=0 대기: ${zeroWidthContainers.map((p) => p.type).join(', ')}`);
        const t1 = setTimeout(() => setForceUpdateCounter((c) => c + 1), 150);
        const t2 = setTimeout(() => setForceUpdateCounter((c) => c + 1), 500);
        return () => { clearTimeout(t1); clearTimeout(t2); };
      }

      // 기존 서브차트 제거 (컨테이너 유효성 확인 후에만 수행)
      Object.keys(subChartsRef.current).forEach(key => {
        if (subChartsRef.current[key]) {
          try {
            const chart = subChartsRef.current[key];
            if (chart && typeof chart.remove === 'function') {
              chart.remove();
            }
          } catch (e) {
            const errorMessage = e instanceof Error ? e.message : String(e);
            if (!errorMessage.includes('disposed')) {
              console.warn(`서브차트 제거 실패 (${key}):`, e);
            }
          }
        }
      });
      subChartsRef.current = {};
      subSeriesRef.current = {};

      // ✅ 메인 차트 범위는 ref에서 가져옴 (실시간 업데이트됨)
      if (mainChartTimeRangeRef.current) {
        console.log(`🔒 [범위 고정] 메인 차트 시간 범위 ref: ${new Date((mainChartTimeRangeRef.current.from as number) * 1000).toISOString()} ~ ${new Date((mainChartTimeRangeRef.current.to as number) * 1000).toISOString()}`);
      }

      // ✅ 일목균형표 선행스팬 ON 시: fixLeftEdge로 인해 scrollToPosition이 서브차트 정렬을 깨뜨림 → setVisibleLogicalRange만 사용
      const isSenkouSpanEnabled = ichimokuSettings?.senkouA || ichimokuSettings?.senkouB;

      enabledPanels.forEach((panel, index) => {
        const container = subChartContainersRef.current[panel.type];
        if (!container) return;
        // ✅ lightweight-charts "Value is null": 컨테이너 width 0일 때 createChart/setData 호출 시 발생 - 건너뛰기
        if (container.clientWidth <= 0) {
          console.warn(`⏸️ [서브차트 ${panel.type}] 컨테이너 width=0 - 차트 생성 건너뜀`);
          return;
        }

        // ✅ 마지막 서브차트인지 확인 (x축 라벨 표시용)
        const isLastPanel = index === enabledPanels.length - 1;

        // ✅ Zebra striping: 짝수 인덱스는 옅은 회색, 홀수 인덱스는 기본 배경색
        const isEven = index % 2 === 0;
        const alternatingBgColor = themeMode === 'dark'
          ? (isEven ? 'rgba(255, 255, 255, 0.02)' : chartColors.background)
          : (isEven ? 'rgba(0, 0, 0, 0.02)' : chartColors.background);

        try {
          
          if (index === 0) {
            console.log(`📊 [서브차트 timeScale] 일목균형표 선행스팬 활성화: ${isSenkouSpanEnabled ? 'ON (좌측 기준 정렬)' : 'OFF (우측 기준 정렬)'}`);
          }
          
          // 서브차트 생성
          // ✅ 서브차트용 timeScale 설정 (메인 차트와 완전히 일치)
          // ✅ showSubPanelTimeScale이 true이고 마지막 패널일 때만 날짜 라벨 표시 (투자분석), 멀티차트는 false로 비표시
          const showTimeScaleOnThisPanel = showSubPanelTimeScale && isLastPanel;
          const subChartTimeScaleOptions = {
            borderColor: chartColors.gridColor,
            timeVisible: showTimeScaleOnThisPanel,  // ✅ 투자분석: 마지막 보조지표만 날짜 표시
            secondsVisible: false,
            visible: showTimeScaleOnThisPanel,  // ✅ 투자분석: 마지막 보조지표만 날짜 표시
            rightOffset: isSenkouSpanEnabled ? 30 : 5,  // ✅ 투자분석과 동일: 선행스팬 ON시 30, OFF시 5 (보조지표 미래 영역 확장 방지)
            barSpacing: 8,    // ✅ 메인 차트와 동일한 캔들 간격
            minBarSpacing: 5, // ✅ 메인 차트와 동일한 최소 캔들 간격
            // ✅ 일목균형표 선행스팬이 활성화되어 있으면 좌측 기준으로 정렬
            fixLeftEdge: isSenkouSpanEnabled ? true : false,  // 선행스팬 ON: 좌측 고정, OFF: 자유 스크롤
            // ✅ 메인 차트와 동일하게 유지 (업비트 스타일: 수동 스크롤 허용)
            fixRightEdge: false,
            lockVisibleTimeRangeOnResize: true, // ✅ 리사이즈 시 범위 고정
            // ✅ 수동 스크롤 모드: 자동 롤링 비활성화 (업비트 스타일)
            rightBarStaysOnScroll: false, // ✅ 스크롤 시 차트가 자유롭게 이동
            shiftVisibleRangeOnNewBar: false, // ✅ 수동 스크롤 모드: 자동 롤링 비활성화
            allowShiftVisibleRangeOnWhitespaceReplacement: false,  // ✅ 자동 롤링 비활성화
            // ✅ 시간 포맷터 커스터마이징 (메인 차트와 동일)
            tickMarkFormatter: (time: number) => {
              const date = new Date(time * 1000);
              const month = String(date.getMonth() + 1).padStart(2, '0');
              const day = String(date.getDate()).padStart(2, '0');
              const hours = String(date.getHours()).padStart(2, '0');
              const minutes = String(date.getMinutes()).padStart(2, '0');
              return `${month}/${day} ${hours}:${minutes}`;
            },
          };

          // ✅ 메인 차트의 실제 clientWidth 사용 (완벽한 동기화)
          const mainChartWidth = chartContainerRef.current?.clientWidth || container.clientWidth;
          
          const subChartOptions: Parameters<typeof createChart>[1] = {
            autoSize: true,  // ✅ 자동 크기 조정 활성화 (IndicatorChart와 동일)
            layout: {
              background: { type: ColorType.Solid, color: alternatingBgColor },
              textColor: chartColors.textColor,
              attributionLogo: false,
            },
            watermark: {
              visible: false,  // ✅ 워터마크 완전 비활성화 (IndicatorChart와 동일)
            },
            width: mainChartWidth,  // ✅ 메인 차트와 동일한 너비 사용
            height: panel.height || 120,
            localization: {
              locale: 'en-US',
              // ✅ Y축 라벨 포맷터: 이격도일 때는 변환된 값을 원래 값으로 복원
              priceFormatter: (price: number) => {
                let formatted: string;
                
                // ✅ 이격도 패널: 변환된 값(0~15)을 원래 값(95~110)으로 복원
                if (panel.type === 'disparity') {
                  const originalValue = price + 95; // 95 더하기 (DISPARITY_OFFSET)
                  formatted = originalValue.toFixed(1);
                }
                // 절대값이 1000000 이상이면 M 단위로 표시
                else if (Math.abs(price) >= 1000000) {
                  formatted = (price / 1000000).toFixed(1) + 'M';
                }
                // 절대값이 1000 이상이면 K 단위로 표시
                else if (Math.abs(price) >= 1000) {
                  formatted = (price / 1000).toFixed(1) + 'K';
                }
                // 절대값이 100 이상이면 정수로 표시
                else if (Math.abs(price) >= 100) {
                  formatted = price.toFixed(0);
                }
                // 그 외에는 소수점 2자리로 표시
                else {
                  formatted = price.toFixed(2);
                }
                // ★ 고정 길이 (8자리)로 패딩하여 모든 Y축 라벨의 너비를 동일하게
                return formatted.padStart(8, ' ');
              },
              // ✅ 크로스헤어 레이블의 시간 포맷터 (X축과 동일한 형식으로 통일)
              timeFormatter: (time: number) => {
                const date = new Date(time * 1000);
                const month = String(date.getMonth() + 1).padStart(2, '0');
                const day = String(date.getDate()).padStart(2, '0');
                const hours = String(date.getHours()).padStart(2, '0');
                const minutes = String(date.getMinutes()).padStart(2, '0');
                return `${month}/${day} ${hours}:${minutes}`;
              },
            },
            grid: {
              vertLines: { 
                visible: true,
                color: chartColors.gridColor,
              },
              horzLines: { 
                visible: false,  // ✅ 수평 그리드 라인 완전히 숨김 (IndicatorChart와 동일)
                color: 'transparent',  // ✅ 혹시 모를 경우를 대비해 투명 색상 설정
              },
            },
            leftPriceScale: {
              visible: false,  // ✅ 왼쪽 Y축 완전 숨김 (IndicatorChart와 동일)
              minimumWidth: 0,  // ✅ 왼쪽 여백 완전 제거 (IndicatorChart와 동일)
              mode: 0,  // ✅ PriceScaleMode.Normal (IndicatorChart와 동일)
              borderVisible: false,  // ✅ 경계선도 숨김 (IndicatorChart와 동일)
              ticksVisible: false,  // ✅ 눈금 숨김 (IndicatorChart와 동일)
            },
            rightPriceScale: {
              visible: true,  // ✅ 오른쪽 Y축 표시 (업비트 스타일)
              borderVisible: false,  // ✅ Y축 경계선 숨김 (IndicatorChart와 동일)
              borderColor: 'transparent',  // ✅ 경계선 색상도 투명으로 (IndicatorChart와 동일)
              // ✅ 메인과 동일 너비 — 패널별(112/128)로 다르면 전역 수직선·timeToCoordinate X가 캔들 vs 보조지표에서 불일치
              minimumWidth: stackedPanelsRightPriceScaleMinWidth(subPanels, isMobileMode),
              entireTextOnly: false,  // ✅ 라벨이 잘려도 괜찮음 (IndicatorChart와 동일)
              autoScale: true,  // ✅ 자동 스케일 (IndicatorChart와 동일)
              scaleMargins: {
                top: 0.1,
                bottom: 0.15,  // ✅ 0.1 → 0.15로 증가 (X축 라벨 영역과 겹치지 않도록)
              },  // ✅ 여백 설정 (IndicatorChart와 동일)
              ticksVisible: false,  // ✅ Y축 눈금 완전 제거 (IndicatorChart와 동일)
              // 여러 시리즈·가격선 라벨이 세로로 겹칠 때 분산 배치 (좁은 보조지표 패널 필수)
              alignLabels: true,
              mode: 0,  // ✅ PriceScaleMode.Normal (IndicatorChart와 동일)
            },
            timeScale: subChartTimeScaleOptions,
            crosshair: {
              mode: 0, // ✅ Normal 모드: 마우스 위치를 정확히 따라감
              vertLine: {
                visible: false, // ✅ 업비트 스타일: 수직선 숨김 (전역 수직선 사용)
                color: 'transparent',
                style: 0,
                labelVisible: true, // ✅ 세로선 날짜 레이블 표시
                labelBackgroundColor: chartColors.crosshairLabelBg || '#4c525e',
              },
              horzLine: {
                visible: true,
                color: chartColors.crosshairColor || '#758696',
                width: 1,
                style: 3,
                labelBackgroundColor: chartColors.crosshairLabelBg || '#4c525e',
              },
            },
            handleScroll: {
              mouseWheel: false,
              pressedMouseMove: false,
              horzTouchDrag: isMobileMode ? true : false,   // 모바일: 터치 가로 드래그 활성화
              vertTouchDrag: isMobileMode ? true : false,   // 모바일: 터치 세로 드래그 활성화
            },
            handleScale: {
              mouseWheel: isMobileMode ? true : false,      // 모바일: 휠 줌 활성화
              pinch: isMobileMode ? true : false,           // 모바일: 핀치 줌 활성화
              axisPressedMouseMove: {
                time: isMobileMode ? true : false,          // 모바일: X축 드래그로 줌 활성화
                price: isMobileMode ? true : false,         // 모바일: Y축 드래그로 줌 활성화
              },
              axisDoubleClickReset: false,
            },
            kineticScroll: {
              mouse: false,
              touch: isMobileMode ? true : false,           // 모바일만 터치 관성 스크롤 활성화
            },
            ...(panel.type === 'disparity' ? {
              overlayPriceScales: { mode: 0 },  // Normal (95, 100, 105 표시)
            } : {}),
          };
          const subChart = createChart(container, subChartOptions);

          subChartsRef.current[panel.type] = subChart;

          // ✅ 서브차트 생성 직후 timeScale 옵션 명시적 재적용 (확실한 동기화)
          subChart.applyOptions({
            timeScale: subChartTimeScaleOptions,
          });
          
          // ✅ setVisibleLogicalRange는 데이터 설정 **후에만** 호출 (lightweight-charts "Value is null" 방지)
          // 데이터 추가 전 호출 시 오류 발생 → "데이터 설정 후 logical range 재적용" 블록에서 처리
          console.log(`🔧 [서브차트 ${panel.type}] timeScale 옵션 재적용 완료: rightOffset=${subChartTimeScaleOptions.rightOffset}, fixLeftEdge=${subChartTimeScaleOptions.fixLeftEdge}`);

          // 시간 데이터를 안전하게 변환 (메인 캔들 buildCandleDataRaw / updateLastIndicators와 동일: 초 단위)
          const parseTime = (timeStr: string | number | undefined, index?: number): Time => {
            try {
              let raw: string | number | undefined = timeStr;
              if ((raw == null || raw === '') && index !== undefined && index < data.length) {
                raw = data[index].time as string | number | undefined;
              }
              if (raw == null || raw === '') {
                console.warn(`Invalid time: ${raw}`);
                return 0 as Time;
              }
              if (typeof raw === 'number') {
                if (!Number.isFinite(raw)) return 0 as Time;
                const sec = raw > 1e12 ? Math.floor(raw / 1000) : Math.floor(raw);
                return sec as Time;
              }
              const timestamp = new Date(raw as string).getTime();
              if (isNaN(timestamp)) {
                console.warn(`Invalid time: ${raw}`);
                return 0 as Time;
              }
              return Math.floor(timestamp / 1000) as Time;
            } catch (e) {
              console.warn(`Failed to parse time: ${timeStr}`, e);
              return 0 as Time;
            }
          };

          // ✅ 서브패널: 메인 차트 data와 시간 정렬된 보조지표 데이터 사용
          // - 투자분석/멀티차트 공통: data=chartData(표시용 200), indicatorData=200(1:1) → indicatorDataMatchesData로 1:1 사용
          const hasIndicatorFields = (arr: any[] | undefined) =>
            arr && arr.length > 0 && arr.some((d: any) => d.cci13 != null || d.cci != null || d.adx != null || d.macdLine != null);
          const hasFuturePlaceholders = data.length > (indicatorData?.length ?? 0);
          const canUseAllIndicator = hasIndicatorFields(allIndicatorData) && (allIndicatorData?.length ?? 0) >= data.length;
          // 투자분석+일목: data(200), indicatorData(200) → 1:1 정렬
          const isInvestmentFutureCase = hasFuturePlaceholders && isSenkouSpanEnabled && data.length < (allIndicatorData?.length ?? 0) && hasIndicatorFields(indicatorData) && (indicatorData?.length ?? 0) > 0;
          // ✅ 멀티차트: indicatorData가 data와 동일 길이·지표 필드 있으면 우선 사용 (시간축 1:1 정렬 보장)
          const indicatorDataMatchesData = indicatorData && indicatorData.length === data.length && hasIndicatorFields(indicatorData);
          let effectiveIndicatorData =
            isInvestmentFutureCase
              ? indicatorData!  // 투자분석+일목: 200+26미래 → indicatorData 기준으로 placeholder 추가 예정
              : indicatorDataMatchesData
                ? indicatorData!  // 멀티차트/투자분석: data와 1:1 정렬된 indicatorData 사용
                : canUseAllIndicator
                  ? allIndicatorData!
                  : hasIndicatorFields(indicatorData)
                    ? indicatorData!
                    : (allIndicatorData && allIndicatorData.length >= data.length ? allIndicatorData : indicatorData);

          // ✅ 일목 선행스팬 ON: 메인 data(chartData)에는 이미 미래 봉이 포함됨 → 목표 개수는 data.length (data.length+26 금지: 이중 가산으로 서브·메인 논리봉 불일치)
          // placeholder 시각은 메인 캔들 data[i].time과 동일하게 맞춰 setVisibleLogicalRange 동기화 유지
          const targetBarCount = data.length;
          const needsIndicatorPlaceholders =
            isSenkouSpanEnabled &&
            effectiveIndicatorData &&
            effectiveIndicatorData.length > 0 &&
            targetBarCount > effectiveIndicatorData.length;
          if (needsIndicatorPlaceholders && effectiveIndicatorData) {
            // ✅ 시간축 논리봉 개수만 메인 data에 맞춤. 마지막 행을 spread하면 스토캐스틱/윌리엄스/TRIX/이격도 등이
            // 미래 구간에 마지막 값으로 복제되어 구름 영역까지 선이 그려짐 → time만 두고 지표 필드는 비움.
            const futurePlaceholders: any[] = [];
            for (let i = effectiveIndicatorData.length; i < targetBarCount; i++) {
              const candle = data[i] as any;
              const t = candle?.time ?? candle?.date ?? candle?.timestamp;
              if (!t) {
                console.warn(`📏 [서브차트] 일목 placeholder 스킵: data[${i}]에 time 없음`);
                continue;
              }
              futurePlaceholders.push({
                time: t,
                date: t,
                timestamp: t,
              });
            }
            if (futurePlaceholders.length > 0) {
              effectiveIndicatorData = [...effectiveIndicatorData, ...futurePlaceholders];
              console.log(
                `📏 [서브차트] 일목 선행스팬: 보조지표 placeholder ${futurePlaceholders.length}개 → 총 ${effectiveIndicatorData.length}봉 (메인 data ${data.length}봉)`
              );
            }
          }

          // ✅ 메인 캔들 시리즈와 동일한 실봉 개수만 사용 (지표 행이 캔들보다 많거나 시간 불일치 시 미래 구간에 찍힘 방지)
          // 일목균형표 선행스팬이 활성화되지 않은 경우: 전체 data.length 사용
          // 일목균형표 선행스팬이 활성화된 경우: candleDataRawRef 또는 data.length 사용
          const realCandleLenForSub = isSenkouSpanEnabled
            ? (candleDataRawRef.current && candleDataRawRef.current.length > 0
                ? candleDataRawRef.current.length
                : data.length)
            : data.length; // 일목 비활성 시 전체 데이터 사용
          
          if (
            effectiveIndicatorData &&
            effectiveIndicatorData.length > 0 &&
            realCandleLenForSub > 0 &&
            effectiveIndicatorData.length > realCandleLenForSub
          ) {
            effectiveIndicatorData = effectiveIndicatorData.slice(0, realCandleLenForSub);
          }

          // 서브패널 각 행의 시각: 1:1 캔들과 반드시 동일하게 (d.time 우선 시 CCI가 일목 미래 축까지 직선으로 이어질 수 있음)
          const subChartTimeForRow = (d: any, i: number): string | number | undefined => {
            if (i < data.length && data[i]?.time != null && data[i].time !== '') {
              return data[i].time as string | number;
            }
            return d?.time ?? d?.timestamp ?? undefined;
          };

          /** 메인 캔들 setData와 동일한 UTCTimestamp(초). parseTime(ISO 문자열)은 브라우저 TZ로 달라져 일목 미래 구간까지 선이 뻗어 보일 수 있음 */
          const subChartSeriesTimeFromIndex = (i: number, d: any): Time | null => {
            const rawRef = candleDataRawRef.current;
            if (rawRef && i >= 0 && i < rawRef.length && rawRef[i]?.time != null) {
              const t = rawRef[i].time as number;
              if (Number.isFinite(t)) {
                const sec = Math.floor(t > 1e12 ? t / 1000 : t);
                if (sec > 0) return sec as Time;
              }
            }
            const timeValue = subChartTimeForRow(d, i);
            if (!timeValue) return null;
            const parsed = parseTime(timeValue);
            if (!parsed || (typeof parsed === 'number' && parsed <= 0)) return null;
            return parsed as Time;
          };

          // ✅ 메인은 일목 선행스팬으로 논리봉이 미래까지 확장되나, 보조지표는 실캔들 마지막 시각까지만 그린다.
          // - data 프롭 vs 메인 candleData dedupe 정렬 순서가 어긋나면 data[N] 시각이 지표 마지막 시각과 불일치할 수 있음(멀티차트).
          // - 메인과 동일하게 플레이스홀더 없이 채워지는 candleDataRawRef 마지막 봉을 최우선 기준으로 사용 (투자분석과 동일 안전).
          const toUnixSecForClamp = (t: number | string | undefined): number => {
            if (t == null || t === '') return Number.POSITIVE_INFINITY;
            if (typeof t === 'number') {
              if (!Number.isFinite(t)) return Number.POSITIVE_INFINITY;
              return t > 1e12 ? Math.floor(t / 1000) : Math.floor(t);
            }
            const ms = new Date(t as string).getTime();
            return Number.isFinite(ms) ? Math.floor(ms / 1000) : Number.POSITIVE_INFINITY;
          };
          const rawMax = candleDataRawRef.current;
          let maxIndicatorTimeSec =
            rawMax && rawMax.length > 0
              ? toUnixSecForClamp(rawMax[rawMax.length - 1].time as number | string)
              : Number.POSITIVE_INFINITY;
          const realCandleCountRef = (candleSeriesRef.current as any)?.__realCandleCount as number | undefined;
          const realNForClamp =
            typeof realCandleCountRef === 'number' && realCandleCountRef > 0
              ? realCandleCountRef
              : rawMax?.length ?? 0;
          if (realNForClamp > 0 && realNForClamp <= data.length && data[realNForClamp - 1]?.time != null && data[realNForClamp - 1]?.time !== '') {
            const tRow = toUnixSecForClamp(data[realNForClamp - 1].time as number | string);
            if (Number.isFinite(tRow)) {
              if (!Number.isFinite(maxIndicatorTimeSec) || maxIndicatorTimeSec >= Number.POSITIVE_INFINITY) {
                maxIndicatorTimeSec = tRow;
              } else {
                maxIndicatorTimeSec = Math.max(maxIndicatorTimeSec, tRow);
              }
            }
          }
          if (!Number.isFinite(maxIndicatorTimeSec) || maxIndicatorTimeSec >= Number.POSITIVE_INFINITY) {
            const lastRealIdx =
              typeof realCandleCountRef === 'number' && realCandleCountRef > 0
                ? Math.min(realCandleCountRef, data.length) - 1
                : data.length - 1;
            const lastRealCandleRow = lastRealIdx >= 0 ? data[lastRealIdx] : undefined;
            maxIndicatorTimeSec =
              lastRealCandleRow?.time != null && lastRealCandleRow.time !== ''
                ? toUnixSecForClamp(lastRealCandleRow.time as number | string)
                : Number.POSITIVE_INFINITY;
          }
          if (isSenkouSpanEnabled) {
            const mtEnd = mainCandleDisplayTimesRef.current;
            if (mtEnd.length > 0) {
              const tailSec = mtEnd[mtEnd.length - 1];
              if (Number.isFinite(tailSec) && tailSec > 0) {
                if (!Number.isFinite(maxIndicatorTimeSec) || maxIndicatorTimeSec >= Number.POSITIVE_INFINITY) {
                  maxIndicatorTimeSec = tailSec;
                } else {
                  maxIndicatorTimeSec = Math.max(maxIndicatorTimeSec, tailSec);
                }
              }
            }
          }

          const clampSubChartByMaxTime = <T extends { time: Time | number }>(pts: T[]): T[] => {
            if (!Number.isFinite(maxIndicatorTimeSec) || maxIndicatorTimeSec >= Number.POSITIVE_INFINITY) {
              return pts;
            }
            return pts.filter((pt) => {
              const tu = typeof pt.time === 'number' ? pt.time : Number(pt.time);
              return Number.isFinite(tu) && tu <= maxIndicatorTimeSec;
            });
          };

          // 보조지표 데이터를 차트 데이터로 변환하는 헬퍼 함수
          const mapIndicatorData = (valueExtractor: (d: any) => number | null | undefined) => {
            if (!effectiveIndicatorData) return [];

            // 실제 indicatorData 매핑
            const mappedData = effectiveIndicatorData
              .map((d, i) => {
                const value = valueExtractor(d);
                if (value === undefined || value === null || isNaN(value) || !isFinite(value)) return null;
                const time = subChartSeriesTimeFromIndex(i, d);
                if (time === null) return null;
                return { time, value };
              })
              .filter((d): d is NonNullable<typeof d> => d !== null && d.time !== 0);

            const timeFiltered = clampSubChartByMaxTime(mappedData);

            // ✅ lightweight-charts "Value is null" 방지: null/NaN/Infinity 제거
            const validated = validateChartData(timeFiltered as { time: number; value: number | null }[]);
            return validated as { time: Time; value: number }[];
          };

          // 보조지표별 데이터 추가 (volume은 data만 사용, 나머지는 effectiveIndicatorData 사용)
          const hasIndicatorData = effectiveIndicatorData && effectiveIndicatorData.length > 0;
          const hasVolumePanel = enabledPanels.some(p => p.type === 'volume');
          if (hasIndicatorData || (hasVolumePanel && data.length > 0)) {
            const indicatorDataForChart = effectiveIndicatorData ?? [];
            console.log(`📊 [서브차트 ${panel.type}] effectiveIndicatorData 길이: ${indicatorDataForChart.length}, data 길이: ${data.length}`);
            
            if (panel.type === 'volume') {
              // Volume 차트 (data에서 가져오기) - 설정 색상 적용
              const volumeData = data
                .filter(d => d.volume !== undefined && d.volume !== null && isFinite(d.volume) && d.volume >= 0)
                .map((d, i) => {
                  const isRising = d.close >= d.open;
                  const color = isRising
                    ? (colorSettings?.volumeRising || '#ef5350')
                    : (colorSettings?.volumeFalling || '#2196f3');
                  const time = subChartSeriesTimeFromIndex(i, d);
                  if (time === null || time === 0) return null;
                  // ✅ 다시 한번 volume 유효성 체크
                  if (!isFinite(d.volume!) || d.volume! < 0) return null;
                  return {
                    time,
                    value: d.volume!,
                    color,
                  };
                })
                .filter((d): d is NonNullable<typeof d> => d !== null && d.value !== null && isFinite(d.value));

              const volumeFinal = sanitizeHistogramSeriesData(volumeData as { time: Time; value: number; color: string }[]);
              
              // ✅ 데이터가 있을 때만 시리즈 생성
              if (volumeFinal.length > 0) {
                const histogramSeries = subChart.addHistogramSeries({
                  priceFormat: {
                    type: 'volume',
                  },
                  priceScaleId: 'right',
                });
                histogramSeries.setData(volumeFinal as any);
                subSeriesRef.current[panel.type] = histogramSeries;
                console.log(`✅ [서브차트] Volume 데이터 설정: ${volumeFinal.length}개`);
              } else {
                console.warn(`⚠️ [서브차트] Volume 데이터 없음 - 시리즈 생성 스킵`);
              }
            } else if (hasIndicatorData && panel.type === 'macd') {
              // MACD 차트 (MACD Line, Signal Line, Histogram) - 설정 색상/굵기/스타일 적용
              // Histogram 데이터 준비
              const histogramData = indicatorDataForChart
                .map((d, i) => {
                  const value = d.histogram ?? d.macdHistogram;
                  if (value === undefined || value === null || !isFinite(value)) return null;
                  
                  // ✅ 히스토그램 상승/하락 활성화 상태 확인
                  if (value > 0 && effectiveIndicatorSettings?.histogramPositiveEnabled === false) return null;
                  if (value < 0 && effectiveIndicatorSettings?.histogramNegativeEnabled === false) return null;

                  const t = subChartSeriesTimeFromIndex(i, d);
                  if (t === null || t === 0) return null;
                  
                  const color = value >= 0
                    ? (colorSettings?.histogramPositive || '#26a69a')
                    : (colorSettings?.histogramNegative || '#ef5350');

                  return {
                    time: t,
                    value: value,
                    color: color,
                  };
                })
                .filter((d): d is NonNullable<typeof d> => d !== null && d.value !== null && isFinite(d.value) && d.time !== 0);
              const histogramDataClampedRaw = clampSubChartByMaxTime(histogramData) as {
                time: Time;
                value: number;
                color: string;
              }[];
              const histogramDataClamped = sanitizeHistogramSeriesData(histogramDataClampedRaw);
              
              // ✅ Histogram 시리즈 생성 (데이터가 있을 때만)
              let histogramSeries = null;
              if (histogramDataClamped.length > 0) {
                histogramSeries = subChart.addHistogramSeries({
                  priceFormat: {
                    type: 'price',
                    precision: 4,
                    minMove: 0.0001,
                  },
                  priceScaleId: 'right',
                });
                histogramSeries.setData(histogramDataClamped as any);
              } else {
                console.warn(`⚠️ [서브차트] MACD Histogram 데이터 없음`);
              }

              // MACD Line
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              const macdLineSeries = subChart.addLineSeries({
                color: colorSettings?.macdLine || '#2962FF',
                lineWidth: (colorSettings?.macdLineWidth || 2) as any,
                lineStyle: colorSettings?.macdLineStyle ? getLineStyle(colorSettings.macdLineStyle) : 0,
                priceFormat: {
                  type: 'price',
                  precision: 4,
                  minMove: 0.0001,
                },
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
                const macdLineData = indicatorDataForChart
                  .map((d, i) => {
                    if (d.macdLine === undefined || d.macdLine === null) return null;
                    const t = subChartSeriesTimeFromIndex(i, d);
                    if (t === null) return null;
                    return { time: t, value: d.macdLine };
                  })
                  .filter((d): d is NonNullable<typeof d> => d !== null && d.time !== 0);
                const macdLineDataClamped = clampSubChartByMaxTime(macdLineData) as { time: Time; value: number }[];
                if (macdLineDataClamped.length > 0) {
                  macdLineSeries.setData(macdLineDataClamped as any);
                }

              // Signal Line
              const signalLineSeries = subChart.addLineSeries({
                color: colorSettings?.signalLine || '#FF6D00',
                lineWidth: (colorSettings?.signalLineWidth || 2) as any,
                lineStyle: colorSettings?.signalLineStyle ? getLineStyle(colorSettings.signalLineStyle) : 0,
                priceFormat: {
                  type: 'price',
                  precision: 4,
                  minMove: 0.0001,
                },
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
                const signalLineData = indicatorDataForChart
                  .map((d, i) => {
                    const value = d.signalLine ?? d.signal;
                    if (value === undefined || value === null) return null;
                    const t = subChartSeriesTimeFromIndex(i, d);
                    if (t === null) return null;
                    return { time: t, value };
                  })
                  .filter((d): d is NonNullable<typeof d> => d !== null && d.time !== 0);
                const signalLineDataClamped = clampSubChartByMaxTime(signalLineData) as { time: Time; value: number }[];
                if (signalLineDataClamped.length > 0) {
                  signalLineSeries.setData(signalLineDataClamped as any);
                }
                
                // MACD 0선 기준선 (설정에 따라)
                if (effectiveIndicatorSettings?.macdZeroLineEnabled) {
                  const macdZeroLineColor = colorSettings?.macdZeroLineColor || '#9e9e9e';
                  macdLineSeries.createPriceLine({
                    price: 0,
                    color: macdZeroLineColor,
                    lineWidth: (colorSettings?.macdZeroLineWidth || 1) as any,
                    lineStyle: colorSettings?.macdZeroLineStyle ? getLineStyle(colorSettings.macdZeroLineStyle) : 1,
                    axisLabelVisible: true,
                    axisLabelColor: macdZeroLineColor,
                    title: '',
                  });
                }

              const macdEntry: {
                histogram?: ISeriesApi<'Histogram'>;
                macdLine: typeof macdLineSeries;
                signalLine: typeof signalLineSeries;
              } = { macdLine: macdLineSeries, signalLine: signalLineSeries };
              if (histogramSeries) macdEntry.histogram = histogramSeries;
              subSeriesRef.current[panel.type] = macdEntry as any;
              console.log(
                `✅ [서브차트] MACD 데이터 설정: Histogram ${histogramDataClamped.length}개, Line ${macdLineDataClamped.length}개, Signal ${signalLineDataClamped.length}개`
              );
            } else if (hasIndicatorData && panel.type === 'rsi') {
              // RSI 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // ✅ RSI 과열 영역 (기준선 위만) - BaselineSeries 사용
              let rsiOverboughtAreaSeries = null;
              if (effectiveIndicatorSettings?.rsiOverboughtEnabled) {
                const rsiOverboughtLevel = effectiveIndicatorSettings.rsiOverbought || 70;
                rsiOverboughtAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: rsiOverboughtLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'rgba(239, 83, 80, 0.25)',
                  topFillColor2: 'rgba(239, 83, 80, 0.15)',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'transparent',
                  bottomFillColor2: 'transparent',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 RSI] 과열 영역 시리즈 생성 완료 (기준: ${rsiOverboughtLevel}) - 기준선 위만`);
              }
              
              // ✅ RSI 침체 영역 (기준선 아래만) - BaselineSeries 사용
              let rsiOversoldAreaSeries = null;
              if (effectiveIndicatorSettings?.rsiOversoldEnabled) {
                const rsiOversoldLevel = effectiveIndicatorSettings.rsiOversold || 30;
                rsiOversoldAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: rsiOversoldLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'transparent',
                  topFillColor2: 'transparent',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'rgba(33, 150, 243, 0.15)',
                  bottomFillColor2: 'rgba(33, 150, 243, 0.25)',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 RSI] 침체 영역 시리즈 생성 완료 (기준: ${rsiOversoldLevel}) - 기준선 아래만 (파란색)`);
              }
              
              const rsiSeries = subChart.addLineSeries({
                color: colorSettings?.rsiColor || '#2962FF',
                lineWidth: (colorSettings?.rsiWidth || 2) as any,
                lineStyle: colorSettings?.rsiLineStyle ? getLineStyle(colorSettings.rsiLineStyle) : 0,
                priceFormat: {
                  type: 'price',
                  precision: 2,
                  minMove: 0.01,
                },
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });

              const rsiData = mapIndicatorData(d => d.rsi);
              let rsiSignalSeries: any = null;

              if (rsiData.length > 0) {
                rsiSeries.setData(rsiData as any);
                
                // ✅ 영역 시리즈에도 동일한 데이터 설정
                if (rsiOverboughtAreaSeries) {
                  rsiOverboughtAreaSeries.setData(rsiData as any);
                  const overboughtCount = rsiData.filter(d => d.value > (effectiveIndicatorSettings?.rsiOverbought || 70)).length;
                  console.log(`✅ [CandlestickChart 서브차트 RSI] 과열 영역 데이터 설정 완료 (총: ${rsiData.length}개, 과열: ${overboughtCount}개)`);
                }
                if (rsiOversoldAreaSeries) {
                  rsiOversoldAreaSeries.setData(rsiData as any);
                  const oversoldCount = rsiData.filter(d => d.value < (effectiveIndicatorSettings?.rsiOversold || 30)).length;
                  console.log(`✅ [CandlestickChart 서브차트 RSI] 침체 영역 데이터 설정 완료 (총: ${rsiData.length}개, 침체: ${oversoldCount}개)`);
                }

                console.log(`✅ [서브차트] RSI 데이터 설정: ${rsiData.length}개`);

                // RSI 기준선 추가 (설정에 따라)
                if (effectiveIndicatorSettings) {
                  // 과매수선 (기본 70)
                  if (effectiveIndicatorSettings.rsiOverboughtEnabled) {
                    const rsiOverboughtColor = colorSettings?.rsiOverboughtColor || '#ef5350';
                    rsiSeries.createPriceLine({
                      price: effectiveIndicatorSettings.rsiOverbought || 70,
                      color: rsiOverboughtColor,
                      lineWidth: (colorSettings?.rsiOverboughtWidth || 1) as any,
                      lineStyle: colorSettings?.rsiOverboughtLineStyle ? getLineStyle(colorSettings.rsiOverboughtLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: rsiOverboughtColor,
                      title: '',
                    });
                  }
                  // 중앙선 (기본 50)
                  if (effectiveIndicatorSettings.rsiMiddleEnabled) {
                    const rsiMiddleColor = colorSettings?.rsiMiddleColor || '#9e9e9e';
                    rsiSeries.createPriceLine({
                      price: effectiveIndicatorSettings.rsiMiddle || 50,
                      color: rsiMiddleColor,
                      lineWidth: (colorSettings?.rsiMiddleWidth || 1) as any,
                      lineStyle: colorSettings?.rsiMiddleLineStyle ? getLineStyle(colorSettings.rsiMiddleLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: rsiMiddleColor,
                      title: '',
                    });
                  }
                  // 과매도선 (기본 30)
                  if (effectiveIndicatorSettings.rsiOversoldEnabled) {
                    const rsiOversoldColor = colorSettings?.rsiOversoldColor || '#26a69a';
                    rsiSeries.createPriceLine({
                      price: effectiveIndicatorSettings.rsiOversold || 30,
                      color: rsiOversoldColor,
                      lineWidth: (colorSettings?.rsiOversoldWidth || 1) as any,
                      lineStyle: colorSettings?.rsiOversoldLineStyle ? getLineStyle(colorSettings.rsiOversoldLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: rsiOversoldColor,
                      title: '',
                    });
                  }
                }
                
                // RSI 신호선 추가 (ON일 때만 표시)
                if (effectiveIndicatorSettings?.rsiSignalEnabled) {
                  rsiSignalSeries = subChart.addLineSeries({
                    color: colorSettings?.rsiSignalColor || '#FF9800',
                    lineWidth: (colorSettings?.rsiSignalWidth || 1) as any,
                    lineStyle: colorSettings?.rsiSignalLineStyle ? getLineStyle(colorSettings.rsiSignalLineStyle) : 0,
                    priceFormat: {
                      type: 'price',
                      precision: 2,
                      minMove: 0.01,
                    },
                    lastValueVisible: false,
                    priceLineVisible: false,
                  });
                  
                  // RSI 신호선 데이터 계산 (RSI의 N일 이동평균)
                  const signalPeriod = effectiveIndicatorSettings.rsiSignalPeriod || 9;
                  const rsiSignalData: Array<{ time: Time; value: number }> = [];
                  
                  for (let i = signalPeriod - 1; i < rsiData.length; i++) {
                    let sum = 0;
                    for (let j = 0; j < signalPeriod; j++) {
                      sum += rsiData[i - j].value;
                    }
                    rsiSignalData.push({
                      time: rsiData[i].time as Time,
                      value: sum / signalPeriod,
                    });
                  }

                  const rsiSignalDataClamped = clampSubChartByMaxTime(
                    rsiSignalData as { time: Time; value: number }[]
                  );
                  if (rsiSignalDataClamped.length > 0) {
                    rsiSignalSeries.setData(rsiSignalDataClamped as any);
                    console.log(`✅ [서브차트] RSI 신호선 데이터 설정: ${rsiSignalDataClamped.length}개 (기간: ${signalPeriod})`);
                  }
                }

                const rsiRef: {
                  main: typeof rsiSeries;
                  overbought?: NonNullable<typeof rsiOverboughtAreaSeries>;
                  oversold?: NonNullable<typeof rsiOversoldAreaSeries>;
                  signal?: typeof rsiSignalSeries;
                } = { main: rsiSeries };
                if (rsiOverboughtAreaSeries) rsiRef.overbought = rsiOverboughtAreaSeries;
                if (rsiOversoldAreaSeries) rsiRef.oversold = rsiOversoldAreaSeries;
                if (rsiSignalSeries) rsiRef.signal = rsiSignalSeries;
                subSeriesRef.current[panel.type] = rsiRef;
              }
            } else if (hasIndicatorData && panel.type === 'stochastic') {
              // Stochastic 차트 (%K, %D) - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => {
                if (style === 'solid') return 0;
                if (style === 'dotted') return 1;
                if (style === 'dashed') return 2;
                return 0; // 기본값
              };
              
              console.log('🎨 [Stochastic] 설정 적용:', {
                kColor: colorSettings?.stochasticK || '#2962FF',
                kWidth: colorSettings?.stochasticKWidth || 2,
                kLineStyle: colorSettings?.stochasticKLineStyle || 'solid',
                dColor: colorSettings?.stochasticD || '#FF6D00',
                dWidth: colorSettings?.stochasticDWidth || 2,
                dLineStyle: colorSettings?.stochasticDLineStyle || 'solid',
              });
              
              // ✅ Stochastic 과열 영역 (기준선 위만) - BaselineSeries 사용
              let stochOverboughtAreaSeries = null;
              if (effectiveIndicatorSettings?.stochasticOverboughtEnabled) {
                const stochOverboughtLevel = effectiveIndicatorSettings.stochasticOverbought || 80;
                stochOverboughtAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: stochOverboughtLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'rgba(239, 83, 80, 0.25)',
                  topFillColor2: 'rgba(239, 83, 80, 0.15)',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'transparent',
                  bottomFillColor2: 'transparent',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 Stochastic] 과열 영역 시리즈 생성 완료 (기준: ${stochOverboughtLevel}) - 기준선 위만`);
              }
              
              // ✅ Stochastic 침체 영역 (기준선 아래만) - BaselineSeries 사용
              let stochOversoldAreaSeries = null;
              if (effectiveIndicatorSettings?.stochasticOversoldEnabled) {
                const stochOversoldLevel = effectiveIndicatorSettings.stochasticOversold || 20;
                stochOversoldAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: stochOversoldLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'transparent',
                  topFillColor2: 'transparent',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'rgba(33, 150, 243, 0.15)',
                  bottomFillColor2: 'rgba(33, 150, 243, 0.25)',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 Stochastic] 침체 영역 시리즈 생성 완료 (기준: ${stochOversoldLevel}) - 기준선 아래만 (파란색)`);
              }
              
              const kLineSeries = subChart.addLineSeries({
                color: colorSettings?.stochasticK || '#2962FF',
                lineWidth: (colorSettings?.stochasticKWidth || 2) as any,
                lineStyle: getLineStyle(colorSettings?.stochasticKLineStyle || 'solid'),
                priceFormat: {
                  type: 'price',
                  precision: 2,
                  minMove: 0.01,
                },
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const kLineData = mapIndicatorData(d => d.stochasticK ?? d.stochK);
              if (kLineData.length > 0) {
                kLineSeries.setData(kLineData as any);
                
                // ✅ 영역 시리즈에도 동일한 데이터 설정 (K 라인 기준)
                if (stochOverboughtAreaSeries) {
                  stochOverboughtAreaSeries.setData(kLineData as any);
                  const overboughtCount = kLineData.filter(d => d.value > (effectiveIndicatorSettings?.stochasticOverbought || 80)).length;
                  console.log(`✅ [CandlestickChart 서브차트 Stochastic] 과열 영역 데이터 설정 완료 (총: ${kLineData.length}개, 과열: ${overboughtCount}개)`);
                }
                if (stochOversoldAreaSeries) {
                  stochOversoldAreaSeries.setData(kLineData as any);
                  const oversoldCount = kLineData.filter(d => d.value < (effectiveIndicatorSettings?.stochasticOversold || 20)).length;
                  console.log(`✅ [CandlestickChart 서브차트 Stochastic] 침체 영역 데이터 설정 완료 (총: ${kLineData.length}개, 침체: ${oversoldCount}개)`);
                }
              }

              const dLineSeries = subChart.addLineSeries({
                color: colorSettings?.stochasticD || '#FF6D00',
                lineWidth: (colorSettings?.stochasticDWidth || 2) as any,
                lineStyle: getLineStyle(colorSettings?.stochasticDLineStyle || 'solid'),
                priceFormat: {
                  type: 'price',
                  precision: 2,
                  minMove: 0.01,
                },
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const dLineData = mapIndicatorData(d => d.stochasticD ?? d.stochD);
              if (dLineData.length > 0) {
                dLineSeries.setData(dLineData as any);
              }

              if (kLineData.length > 0 || dLineData.length > 0) {

                subSeriesRef.current[panel.type] = { kLine: kLineSeries, dLine: dLineSeries };
                console.log(`✅ [서브차트] Stochastic 데이터 설정: K ${kLineData.length}개, D ${dLineData.length}개`);

                // Stochastic 기준선 추가 (설정에 따라)
                if (effectiveIndicatorSettings) {
                  console.log('📏 [Stochastic] 기준선 설정:', {
                    overbought: effectiveIndicatorSettings.stochasticOverboughtEnabled,
                    neutral: effectiveIndicatorSettings.stochasticNeutralEnabled,
                    oversold: effectiveIndicatorSettings.stochasticOversoldEnabled,
                  });
                  
                  // 과매수선 (기본 80)
                  if (effectiveIndicatorSettings.stochasticOverboughtEnabled) {
                    const stochOverboughtColor = colorSettings?.stochasticOverboughtColor || '#ef5350';
                    kLineSeries.createPriceLine({
                      price: effectiveIndicatorSettings.stochasticOverbought || 80,
                      color: stochOverboughtColor,
                      lineWidth: (colorSettings?.stochasticOverboughtWidth || 1) as any,
                      lineStyle: getLineStyle(colorSettings?.stochasticOverboughtLineStyle || 'dashed'),
                      axisLabelVisible: true,
                      axisLabelColor: stochOverboughtColor,
                      title: '',
                    });
                  }
                  // 중앙선 (기본 50)
                  if (effectiveIndicatorSettings.stochasticNeutralEnabled) {
                    const stochNeutralColor = colorSettings?.stochasticNeutralColor || '#9e9e9e';
                    kLineSeries.createPriceLine({
                      price: effectiveIndicatorSettings.stochasticNeutral || 50,
                      color: stochNeutralColor,
                      lineWidth: (colorSettings?.stochasticNeutralWidth || 1) as any,
                      lineStyle: getLineStyle(colorSettings?.stochasticNeutralLineStyle || 'dashed'),
                      axisLabelVisible: true,
                      axisLabelColor: stochNeutralColor,
                      title: '',
                    });
                  }
                  // 과매도선 (기본 20)
                  if (effectiveIndicatorSettings.stochasticOversoldEnabled) {
                    const stochOversoldColor = colorSettings?.stochasticOversoldColor || '#26a69a';
                    kLineSeries.createPriceLine({
                      price: effectiveIndicatorSettings.stochasticOversold || 20,
                      color: stochOversoldColor,
                      lineWidth: (colorSettings?.stochasticOversoldWidth || 1) as any,
                      lineStyle: getLineStyle(colorSettings?.stochasticOversoldLineStyle || 'dashed'),
                      axisLabelVisible: true,
                      axisLabelColor: stochOversoldColor,
                      title: '',
                    });
                  }
                }
              }
            } else if (hasIndicatorData && panel.type === 'cci') {
              // CCI 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // ✅ CCI 과열 영역 (기준선 위만) - BaselineSeries 사용
              let cciOverboughtAreaSeries = null;
              if (effectiveIndicatorSettings?.cciOverboughtEnabled) {
                const cciOverboughtLevel = effectiveIndicatorSettings.cciOverbought || 100;
                cciOverboughtAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: cciOverboughtLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'rgba(239, 83, 80, 0.25)',
                  topFillColor2: 'rgba(239, 83, 80, 0.15)',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'transparent',
                  bottomFillColor2: 'transparent',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 CCI] 과열 영역 시리즈 생성 완료 (기준: ${cciOverboughtLevel}) - 기준선 위만`);
              }
              
              // ✅ CCI 침체 영역 (기준선 아래만) - BaselineSeries 사용
              let cciOversoldAreaSeries = null;
              if (effectiveIndicatorSettings?.cciOversoldEnabled) {
                const cciOversoldLevel = effectiveIndicatorSettings.cciOversold || -100;
                cciOversoldAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: cciOversoldLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'transparent',
                  topFillColor2: 'transparent',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'rgba(33, 150, 243, 0.15)',
                  bottomFillColor2: 'rgba(33, 150, 243, 0.25)',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 CCI] 침체 영역 시리즈 생성 완료 (기준: ${cciOversoldLevel}) - 기준선 아래만 (파란색)`);
              }
              
              const cciSeries = subChart.addLineSeries({
                color: colorSettings?.cciColor || '#2962FF',
                lineWidth: (colorSettings?.cciWidth || 2) as any,
                lineStyle: colorSettings?.cciLineStyle ? getLineStyle(colorSettings.cciLineStyle) : 0,
                priceFormat: {
                  type: 'price',
                  precision: 2,
                  minMove: 0.01,
                },
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });

              const cciData = mapIndicatorData(d => d.cci13 ?? d.cci);
              let cciSignalSeries: any = null;

              if (cciData.length > 0) {
                cciSeries.setData(cciData as any);
                
                // ✅ 영역 시리즈에도 동일한 데이터 설정
                if (cciOverboughtAreaSeries) {
                  cciOverboughtAreaSeries.setData(cciData as any);
                  const overboughtCount = cciData.filter(d => d.value > (effectiveIndicatorSettings?.cciOverbought || 100)).length;
                  console.log(`✅ [CandlestickChart 서브차트 CCI] 과열 영역 데이터 설정 완료 (총: ${cciData.length}개, 과열: ${overboughtCount}개)`);
                }
                if (cciOversoldAreaSeries) {
                  cciOversoldAreaSeries.setData(cciData as any);
                  const oversoldCount = cciData.filter(d => d.value < (effectiveIndicatorSettings?.cciOversold || -100)).length;
                  console.log(`✅ [CandlestickChart 서브차트 CCI] 침체 영역 데이터 설정 완료 (총: ${cciData.length}개, 침체: ${oversoldCount}개)`);
                }

                console.log(`✅ [서브차트] CCI 데이터 설정: ${cciData.length}개`);

                // CCI 기준선 추가 (설정에 따라)
                if (effectiveIndicatorSettings) {
                  // 과매수선 (기본 100)
                  if (effectiveIndicatorSettings.cciOverboughtEnabled) {
                    const cciOverboughtColor = colorSettings?.cciOverboughtColor || '#ef5350';
                    cciSeries.createPriceLine({
                      price: effectiveIndicatorSettings.cciOverbought || 100,
                      color: cciOverboughtColor,
                      lineWidth: (colorSettings?.cciOverboughtWidth || 1) as any,
                      lineStyle: colorSettings?.cciOverboughtLineStyle ? getLineStyle(colorSettings.cciOverboughtLineStyle) : 2,
                      // 서브패널 좁음: 기준선 우측 박스 + 메인/신호 lastValue 박스가 겹침 → 선만 표시
                      axisLabelVisible: false,
                      axisLabelColor: cciOverboughtColor,
                      title: '',
                    });
                  }
                  // 중앙선 (기본 0)
                  if (effectiveIndicatorSettings.cciMiddleEnabled) {
                    const cciMiddleColor = colorSettings?.cciMiddleColor || '#9e9e9e';
                    cciSeries.createPriceLine({
                      price: effectiveIndicatorSettings.cciMiddle || 0,
                      color: cciMiddleColor,
                      lineWidth: (colorSettings?.cciMiddleWidth || 1) as any,
                      lineStyle: colorSettings?.cciMiddleLineStyle ? getLineStyle(colorSettings.cciMiddleLineStyle) : 2,
                      axisLabelVisible: false,
                      axisLabelColor: cciMiddleColor,
                      title: '',
                    });
                  }
                  // 과매도선 (기본 -100)
                  if (effectiveIndicatorSettings.cciOversoldEnabled) {
                    const cciOversoldColor = colorSettings?.cciOversoldColor || '#26a69a';
                    cciSeries.createPriceLine({
                      price: effectiveIndicatorSettings.cciOversold || -100,
                      color: cciOversoldColor,
                      lineWidth: (colorSettings?.cciOversoldWidth || 1) as any,
                      lineStyle: colorSettings?.cciOversoldLineStyle ? getLineStyle(colorSettings.cciOversoldLineStyle) : 2,
                      axisLabelVisible: false,
                      axisLabelColor: cciOversoldColor,
                      title: '',
                    });
                  }
                }
                
                // CCI 신호선 추가 (ON일 때만 표시)
                if (effectiveIndicatorSettings?.cciSignalEnabled) {
                  cciSignalSeries = subChart.addLineSeries({
                    color: colorSettings?.cciSignalColor || '#FF9800',
                    lineWidth: (colorSettings?.cciSignalWidth || 1) as any,
                    lineStyle: colorSettings?.cciSignalLineStyle ? getLineStyle(colorSettings.cciSignalLineStyle) : 0,
                    priceFormat: {
                      type: 'price',
                      precision: 2,
                      minMove: 0.01,
                    },
                    // 메인 CCI lastValue와 이중으로 쌓여 겹침 → 신호선은 선만(값은 크로스헤어로 확인)
                    lastValueVisible: false,
                    priceLineVisible: false,  // ✅ 자동 가격선 숨김
                  });
                  
                  // CCI 신호선 데이터 계산 (CCI의 N일 이동평균)
                  const signalPeriod = effectiveIndicatorSettings.cciSignalPeriod || 10;
                  const cciSignalData: Array<{ time: Time; value: number }> = [];
                  
                  for (let i = signalPeriod - 1; i < cciData.length; i++) {
                    let sum = 0;
                    for (let j = 0; j < signalPeriod; j++) {
                      sum += cciData[i - j].value;
                    }
                    cciSignalData.push({
                      time: cciData[i].time as Time,
                      value: sum / signalPeriod,
                    });
                  }

                  const cciSignalDataClamped = clampSubChartByMaxTime(
                    cciSignalData as { time: Time; value: number }[]
                  );
                  if (cciSignalDataClamped.length > 0) {
                    cciSignalSeries.setData(cciSignalDataClamped as any);
                    console.log(`✅ [서브차트] CCI 신호선 데이터 설정: ${cciSignalDataClamped.length}개 (기간: ${signalPeriod})`);
                  }
                }

                // ✅ updateLastIndicators / 크로스헤어에서 메인·영역·신호선 모두 갱신 가능하도록 묶음 저장
                const cciRef: {
                  main: typeof cciSeries;
                  overbought?: NonNullable<typeof cciOverboughtAreaSeries>;
                  oversold?: NonNullable<typeof cciOversoldAreaSeries>;
                  signal?: typeof cciSignalSeries;
                } = { main: cciSeries };
                if (cciOverboughtAreaSeries) cciRef.overbought = cciOverboughtAreaSeries;
                if (cciOversoldAreaSeries) cciRef.oversold = cciOversoldAreaSeries;
                if (cciSignalSeries) cciRef.signal = cciSignalSeries;
                subSeriesRef.current[panel.type] = cciRef;
              }
            } else if (hasIndicatorData && panel.type === 'adx') {
              // ADX 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              const adxSeries = subChart.addLineSeries({
                color: colorSettings?.adxColor || '#2962FF',
                lineWidth: (colorSettings?.adxWidth || 2) as any,
                lineStyle: colorSettings?.adxLineStyle ? getLineStyle(colorSettings.adxLineStyle) : 0,
                priceFormat: {
                  type: 'price',
                  precision: 2,
                  minMove: 0.01,
                },
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const adxData = mapIndicatorData(d => d.adx);
              if (adxData.length > 0) {
                adxSeries.setData(adxData as any);
                subSeriesRef.current[panel.type] = adxSeries;
                console.log(`✅ [서브차트] ADX 데이터 설정: ${adxData.length}개`);
                
                // ADX 기준선 추가 (설정에 따라)
                if (effectiveIndicatorSettings) {
                  // 강한 추세선 (기본 25)
                  if (effectiveIndicatorSettings.adxStrongEnabled) {
                    const adxStrongColor = colorSettings?.adxStrongThresholdColor || '#ef5350';
                    adxSeries.createPriceLine({
                      price: effectiveIndicatorSettings.adxStrongThreshold || 25,
                      color: adxStrongColor,
                      lineWidth: (colorSettings?.adxStrongThresholdWidth || 1) as any,
                      lineStyle: colorSettings?.adxStrongThresholdLineStyle ? getLineStyle(colorSettings.adxStrongThresholdLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: adxStrongColor,
                      title: '',
                    });
                  }
                  // 약한 추세선 (기본 20)
                  if (effectiveIndicatorSettings.adxWeakEnabled) {
                    const adxWeakColor = colorSettings?.adxWeakThresholdColor || '#9e9e9e';
                    adxSeries.createPriceLine({
                      price: effectiveIndicatorSettings.adxWeakThreshold || 20,
                      color: adxWeakColor,
                      lineWidth: (colorSettings?.adxWeakThresholdWidth || 1) as any,
                      lineStyle: colorSettings?.adxWeakThresholdLineStyle ? getLineStyle(colorSettings.adxWeakThresholdLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: adxWeakColor,
                      title: '',
                    });
                  }
                }
              }
            } else if (hasIndicatorData && panel.type === 'obv') {
              // OBV 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              const obvSeries = subChart.addLineSeries({ 
                color: colorSettings?.obvColor || '#2962FF', 
                lineWidth: (colorSettings?.obvWidth || 2) as any,
                lineStyle: colorSettings?.obvLineStyle ? getLineStyle(colorSettings.obvLineStyle) : 0,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const obvData = mapIndicatorData(d => d.obv);
              if (obvData.length > 0) {
                obvSeries.setData(obvData as any);
                console.log(`✅ [서브차트] OBV 데이터 설정: ${obvData.length}개`);
                
                // OBV Signal Line (있는 경우)
                console.log(`🔍 [OBV Signal 원본] indicatorData 샘플:`, indicatorData?.slice(0, 3).map(d => ({ 
                  obv: d.obv, 
                  obvSignal: d.obvSignal,
                  hasObvSignal: 'obvSignal' in d
                })));
                const obvSignalData = mapIndicatorData(d => d.obvSignal);
                console.log(`🔍 [OBV Signal] 데이터 길이: ${obvSignalData.length}, obvSignalEnabled: ${effectiveIndicatorSettings?.obvSignalEnabled}`);
                
                if (obvSignalData.length > 0) {
                  // ✅ obvSignalEnabled가 명시적으로 false가 아니면 표시
                  if (effectiveIndicatorSettings?.obvSignalEnabled !== false) {
                    const obvSignalSeries = subChart.addLineSeries({
                      color: colorSettings?.obvSignalColor || '#FF6D00',
                      lineWidth: (colorSettings?.obvSignalWidth || 1.5) as any,
                      lineStyle: colorSettings?.obvSignalLineStyle ? getLineStyle(colorSettings.obvSignalLineStyle) : 0,
                      lastValueVisible: true,  // ✅ 실시간 값 표시
                      priceLineVisible: false,  // ✅ 자동 가격선 숨김
                    });
                    obvSignalSeries.setData(obvSignalData as any);
                    console.log(`✅ [서브차트] OBV Signal 데이터 설정: ${obvSignalData.length}개`);
                    // ✅ updateLastIndicators에서 실시간 갱신 가능하도록 메인·시그널 모두 ref에 보관
                    subSeriesRef.current[panel.type] = { main: obvSeries, signal: obvSignalSeries };
                  } else {
                    console.log(`⏸️ [서브차트] OBV Signal 비활성화됨`);
                    subSeriesRef.current[panel.type] = obvSeries;
                  }
                } else {
                  subSeriesRef.current[panel.type] = obvSeries;
                }
              }
            } else if (hasIndicatorData && panel.type === 'williamsR') {
              // Williams %R 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // ✅ Williams %R 과열 영역 (기준선 위만) - BaselineSeries 사용
              let williamsOverboughtAreaSeries = null;
              if (effectiveIndicatorSettings?.williamsROverboughtEnabled) {
                const williamsOverboughtLevel = effectiveIndicatorSettings.williamsROverbought || -20;
                williamsOverboughtAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: williamsOverboughtLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'rgba(239, 83, 80, 0.25)',
                  topFillColor2: 'rgba(239, 83, 80, 0.15)',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'transparent',
                  bottomFillColor2: 'transparent',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 Williams %R] 과열 영역 시리즈 생성 완료 (기준: ${williamsOverboughtLevel})`);
              }
              
              // ✅ Williams %R 침체 영역 (기준선 아래만) - BaselineSeries 사용
              let williamsOversoldAreaSeries = null;
              if (effectiveIndicatorSettings?.williamsROversoldEnabled) {
                const williamsOversoldLevel = effectiveIndicatorSettings.williamsROversold || -80;
                williamsOversoldAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: williamsOversoldLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'transparent',
                  topFillColor2: 'transparent',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'rgba(33, 150, 243, 0.15)',
                  bottomFillColor2: 'rgba(33, 150, 243, 0.25)',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 Williams %R] 침체 영역 시리즈 생성 완료 (기준: ${williamsOversoldLevel})`);
              }
              
              const williamsRSeries = subChart.addLineSeries({ 
                color: colorSettings?.williamsRColor || '#FF9800', 
                lineWidth: (colorSettings?.williamsRWidth || 2) as any,
                lineStyle: colorSettings?.williamsRLineStyle ? getLineStyle(colorSettings.williamsRLineStyle) : 0,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const williamsRData = mapIndicatorData(d => d.williamsR);
              if (williamsRData.length > 0) {
                williamsRSeries.setData(williamsRData as any);
                
                // ✅ 영역 시리즈에도 동일한 데이터 설정
                if (williamsOverboughtAreaSeries) {
                  williamsOverboughtAreaSeries.setData(williamsRData as any);
                  const overboughtCount = williamsRData.filter(d => d.value > (effectiveIndicatorSettings?.williamsROverbought || -20)).length;
                  console.log(`✅ [CandlestickChart 서브차트 Williams %R] 과열 영역 데이터 설정 완료 (총: ${williamsRData.length}개, 과열: ${overboughtCount}개)`);
                }
                if (williamsOversoldAreaSeries) {
                  williamsOversoldAreaSeries.setData(williamsRData as any);
                  const oversoldCount = williamsRData.filter(d => d.value < (effectiveIndicatorSettings?.williamsROversold || -80)).length;
                  console.log(`✅ [CandlestickChart 서브차트 Williams %R] 침체 영역 데이터 설정 완료 (총: ${williamsRData.length}개, 침체: ${oversoldCount}개)`);
                }
                
                const williamsRef: {
                  main: typeof williamsRSeries;
                  overbought?: NonNullable<typeof williamsOverboughtAreaSeries>;
                  oversold?: NonNullable<typeof williamsOversoldAreaSeries>;
                } = { main: williamsRSeries };
                if (williamsOverboughtAreaSeries) williamsRef.overbought = williamsOverboughtAreaSeries;
                if (williamsOversoldAreaSeries) williamsRef.oversold = williamsOversoldAreaSeries;
                subSeriesRef.current[panel.type] = williamsRef;
                
                console.log(`✅ [서브차트] Williams %R 데이터 설정: ${williamsRData.length}개`);
                
                // Williams %R 기준선 추가 (설정에 따라)
                if (effectiveIndicatorSettings) {
                  // 과매수선 (기본 -20)
                  if (effectiveIndicatorSettings.williamsROverboughtEnabled) {
                    const williamsOverboughtColor = colorSettings?.williamsROverboughtColor || '#ef5350';
                    williamsRSeries.createPriceLine({ 
                      price: effectiveIndicatorSettings.williamsROverbought || -20, 
                      color: williamsOverboughtColor, 
                      lineWidth: (colorSettings?.williamsROverboughtWidth || 1) as any, 
                      lineStyle: colorSettings?.williamsROverboughtLineStyle ? getLineStyle(colorSettings.williamsROverboughtLineStyle) : 2, 
                      axisLabelVisible: true,
                      axisLabelColor: williamsOverboughtColor,
                      title: '',
                    });
                  }
                  // 중앙선 (기본 -50)
                  if (effectiveIndicatorSettings.williamsRMiddleEnabled) {
                    const williamsMiddleColor = colorSettings?.williamsRMiddleColor || '#9e9e9e';
                    williamsRSeries.createPriceLine({
                      price: effectiveIndicatorSettings.williamsRMiddle || -50,
                      color: williamsMiddleColor, 
                      lineWidth: (colorSettings?.williamsRMiddleWidth || 1) as any, 
                      lineStyle: colorSettings?.williamsRMiddleLineStyle ? getLineStyle(colorSettings.williamsRMiddleLineStyle) : 2, 
                      axisLabelVisible: true,
                      axisLabelColor: williamsMiddleColor,
                      title: '',
                    });
                  }
                  // 과매도선 (기본 -80)
                  if (effectiveIndicatorSettings.williamsROversoldEnabled) {
                    const williamsOversoldColor = colorSettings?.williamsROversoldColor || '#26a69a';
                    williamsRSeries.createPriceLine({
                      price: effectiveIndicatorSettings.williamsROversold || -80,
                      color: williamsOversoldColor, 
                      lineWidth: (colorSettings?.williamsROversoldWidth || 1) as any, 
                      lineStyle: colorSettings?.williamsROversoldLineStyle ? getLineStyle(colorSettings.williamsROversoldLineStyle) : 2, 
                      axisLabelVisible: true,
                      axisLabelColor: williamsOversoldColor,
                      title: '',
                    });
                  }
                }
              }
            } else if (hasIndicatorData && panel.type === 'momentum') {
              // Momentum 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              const momentumSeries = subChart.addLineSeries({ 
                color: colorSettings?.momentumColor || '#9C27B0', 
                lineWidth: (colorSettings?.momentumWidth || 2) as any,
                lineStyle: colorSettings?.momentumLineStyle ? getLineStyle(colorSettings.momentumLineStyle) : 0,
                priceFormat: {
                  type: 'price',
                  precision: 2,
                  minMove: 0.01,
                },
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const momentumData = mapIndicatorData(d => d.momentum);
              if (momentumData.length > 0) {
                momentumSeries.setData(momentumData as any);
                subSeriesRef.current[panel.type] = momentumSeries;
                console.log(`✅ [서브차트] Momentum 데이터 설정: ${momentumData.length}개`);
                
                // Momentum 기준선 추가 (설정에 따라)
                if (effectiveIndicatorSettings) {
                  // 0선 (중립선)
                  if (effectiveIndicatorSettings.momentumMiddleEnabled) {
                    const momentumMiddleColor = colorSettings?.momentumMiddleColor || '#9e9e9e';
                    momentumSeries.createPriceLine({
                      price: effectiveIndicatorSettings.momentumMiddle || 0,
                      color: momentumMiddleColor,
                      lineWidth: (colorSettings?.momentumMiddleWidth || 1) as any,
                      lineStyle: colorSettings?.momentumMiddleLineStyle ? getLineStyle(colorSettings.momentumMiddleLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: momentumMiddleColor,
                      title: '',
                    });
                  }
                }
              }
            } else if (hasIndicatorData && panel.type === 'trix') {
              // TRIX 차트 - 설정 색상/굵기/스타일 적용 + Signal Line
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              const trixSeries = subChart.addLineSeries({ 
                color: colorSettings?.trixColor || '#00BCD4', 
                lineWidth: (colorSettings?.trixWidth || 2) as any,
                lineStyle: colorSettings?.trixLineStyle ? getLineStyle(colorSettings.trixLineStyle) : 0,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const trixData = mapIndicatorData(d => d.trix);
              if (trixData.length > 0) {
                trixSeries.setData(trixData as any);
                console.log(`✅ [서브차트] TRIX 데이터 설정: ${trixData.length}개`);
                
                // TRIX Signal Line (있는 경우)
                const trixSignalData = mapIndicatorData(d => d.trixSignal);
                if (trixSignalData.length > 0) {
                  const trixSignalSeries = subChart.addLineSeries({
                    color: colorSettings?.trixSignalColor || '#FF6D00',
                    lineWidth: (colorSettings?.trixSignalWidth || 1.5) as any,
                    lineStyle: colorSettings?.trixSignalLineStyle ? getLineStyle(colorSettings.trixSignalLineStyle) : 0,
                    lastValueVisible: true,  // ✅ 실시간 값 표시
                    priceLineVisible: false,  // ✅ 자동 가격선 숨김
                  });
                  trixSignalSeries.setData(trixSignalData as any);
                  subSeriesRef.current[panel.type] = { trix: trixSeries, signal: trixSignalSeries };
                  console.log(`✅ [서브차트] TRIX Signal 데이터 설정: ${trixSignalData.length}개`);
                } else {
                  subSeriesRef.current[panel.type] = trixSeries;
                }
                
                // TRIX 기준선 추가 (설정에 따라)
                if (effectiveIndicatorSettings) {
                  // 과매수선
                  if (effectiveIndicatorSettings.trixOverboughtEnabled) {
                    const trixOverboughtColor = colorSettings?.trixOverboughtColor || '#ef5350';
                    trixSeries.createPriceLine({
                      price: effectiveIndicatorSettings.trixOverbought || 0.5,
                      color: trixOverboughtColor,
                      lineWidth: (colorSettings?.trixOverboughtWidth || 1) as any,
                      lineStyle: colorSettings?.trixOverboughtLineStyle ? getLineStyle(colorSettings.trixOverboughtLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: trixOverboughtColor,
                      title: '',
                    });
                  }
                  // 중앙선 (0선)
                  if (effectiveIndicatorSettings.trixMiddleEnabled) {
                    const trixMiddleColor = colorSettings?.trixMiddleColor || '#9e9e9e';
                    trixSeries.createPriceLine({
                      price: effectiveIndicatorSettings.trixMiddle || 0,
                      color: trixMiddleColor,
                      lineWidth: (colorSettings?.trixMiddleWidth || 1) as any,
                      lineStyle: colorSettings?.trixMiddleLineStyle ? getLineStyle(colorSettings.trixMiddleLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: trixMiddleColor,
                      title: '',
                    });
                  }
                  // 과매도선
                  if (effectiveIndicatorSettings.trixOversoldEnabled) {
                    const trixOversoldColor = colorSettings?.trixOversoldColor || '#26a69a';
                    trixSeries.createPriceLine({
                      price: effectiveIndicatorSettings.trixOversold || -0.5,
                      color: trixOversoldColor,
                      lineWidth: (colorSettings?.trixOversoldWidth || 1) as any,
                      lineStyle: colorSettings?.trixOversoldLineStyle ? getLineStyle(colorSettings.trixOversoldLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: trixOversoldColor,
                      title: '',
                    });
                  }
                }
              }
            } else if (hasIndicatorData && panel.type === 'disparity') {
              // 이격도 차트 - 4개 라인 (초단기, 단기, 중기, 장기) 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // ✅ 1단계: 모든 이격도 데이터 준비 및 변환
              // 이격도 값(95~105)을 0~10 범위로 변환하여 y축 변동을 명확하게 표시
              const DISPARITY_OFFSET = 95; // 변환 기준값
              
              const transformDisparityData = (data: Array<{ time: any; value: number | null }>) => {
                return data.map(d => ({
                  time: d.time,
                  value: d.value != null ? d.value - DISPARITY_OFFSET : null, // 95 빼기
                })).filter(d => d.value != null) as Array<{ time: any; value: number }>;
              };
              
              const ultraShortData = transformDisparityData(mapIndicatorData(d => (d as any).disparityUltraShort));
              const shortData = transformDisparityData(mapIndicatorData(d => (d as any).disparityShort));
              const midData = transformDisparityData(mapIndicatorData(d => (d as any).disparityMid));
              const longData = transformDisparityData(mapIndicatorData(d => (d as any).disparityLong));
              
              console.log(`✅ [이격도] 데이터 변환 (95 기준): ultraShort ${ultraShortData.length}개, short ${shortData.length}개, mid ${midData.length}개, long ${longData.length}개`);
              
              // ✅ 2단계: 실제 이격도 시리즈 추가
              // 초단기 이격도 (5일)
              const ultraShortSeries = subChart.addLineSeries({ 
                color: colorSettings?.disparityUltraShortColor || '#FF6B35', 
                lineWidth: (colorSettings?.disparityUltraShortWidth || 1.5) as any,
                lineStyle: colorSettings?.disparityUltraShortLineStyle ? getLineStyle(colorSettings.disparityUltraShortLineStyle) : 0,
                visible: effectiveIndicatorSettings?.disparityUltraShortEnabled ?? true,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,
              });
              if (ultraShortData.length > 0) ultraShortSeries.setData(ultraShortData as any);
              
              // 단기 이격도 (10일)
              const shortSeries = subChart.addLineSeries({ 
                color: colorSettings?.disparityShortColor || '#4CAF50', 
                lineWidth: (colorSettings?.disparityShortWidth || 2) as any,
                lineStyle: colorSettings?.disparityShortLineStyle ? getLineStyle(colorSettings.disparityShortLineStyle) : 0,
                visible: effectiveIndicatorSettings?.disparityShortEnabled ?? true,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,
              });
              if (shortData.length > 0) shortSeries.setData(shortData as any);
              
              // 중기 이격도 (20일)
              const midSeries = subChart.addLineSeries({ 
                color: colorSettings?.disparityMidColor || '#2196F3', 
                lineWidth: (colorSettings?.disparityMidWidth || 2) as any,
                lineStyle: colorSettings?.disparityMidLineStyle ? getLineStyle(colorSettings.disparityMidLineStyle) : 0,
                visible: effectiveIndicatorSettings?.disparityMidEnabled ?? true,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,
              });
              if (midData.length > 0) midSeries.setData(midData as any);
              
              // 장기 이격도 (60일)
              const longSeries = subChart.addLineSeries({ 
                color: colorSettings?.disparityLongColor || '#9C27B0', 
                lineWidth: (colorSettings?.disparityLongWidth || 2) as any,
                lineStyle: colorSettings?.disparityLongLineStyle ? getLineStyle(colorSettings.disparityLongLineStyle) : 0,
                visible: effectiveIndicatorSettings?.disparityLongEnabled ?? true,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,
              });
              if (longData.length > 0) longSeries.setData(longData as any);
              
              subSeriesRef.current[panel.type] = { 
                ultraShort: ultraShortSeries, 
                short: shortSeries, 
                mid: midSeries, 
                long: longSeries 
              };
              console.log(`✅ [서브차트] 이격도 데이터 설정: 초단기 ${ultraShortData.length}개, 단기 ${shortData.length}개, 중기 ${midData.length}개, 장기 ${longData.length}개`);
              
              // ✅ 이격도 100 기준선 - visible인 첫 번째 시리즈에 추가 (초단기 OFF 시에도 유지)
              if (effectiveIndicatorSettings && (effectiveIndicatorSettings.disparityBaseLineEnabled ?? true)) {
                // visible인 시리즈 찾기 (우선순위: 초단기 > 단기 > 중기 > 장기)
                const visibleSeries = 
                  (effectiveIndicatorSettings.disparityUltraShortEnabled ?? true) ? ultraShortSeries :
                  (effectiveIndicatorSettings.disparityShortEnabled ?? true) ? shortSeries :
                  (effectiveIndicatorSettings.disparityMidEnabled ?? true) ? midSeries :
                  longSeries; // 기본값: 장기 (마지막 시리즈)
                
                visibleSeries.createPriceLine({ 
                  price: 100 - DISPARITY_OFFSET, // ✅ 변환된 값: 100 - 95 = 5
                  color: colorSettings?.disparityBaseLineColor || '#999999', 
                  lineWidth: (colorSettings?.disparityBaseLineWidth || 1) as any, 
                  lineStyle: colorSettings?.disparityBaseLineStyle ? getLineStyle(colorSettings.disparityBaseLineStyle) : 2, 
                  axisLabelVisible: false,
                  title: ''
                });
                
                console.log(`✅ [이격도] 100 기준선 추가 (변환값: ${100 - DISPARITY_OFFSET})`);
              }
            } else if (hasIndicatorData && panel.type === 'bwi') {
              // BWI 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // ✅ BWI 과열 영역 (기준선 위만) - BaselineSeries 사용
              let bwiOverboughtAreaSeries = null;
              if (effectiveIndicatorSettings?.bwiOverboughtEnabled) {
                const bwiOverboughtLevel = effectiveIndicatorSettings.bwiOverbought || 1.0;
                bwiOverboughtAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: bwiOverboughtLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'rgba(239, 83, 80, 0.25)',
                  topFillColor2: 'rgba(239, 83, 80, 0.15)',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'transparent',
                  bottomFillColor2: 'transparent',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 BWI] 과열 영역 시리즈 생성 완료 (기준: ${bwiOverboughtLevel})`);
              }
              
              // ✅ BWI 침체 영역 (기준선 아래만) - BaselineSeries 사용
              let bwiOversoldAreaSeries = null;
              if (effectiveIndicatorSettings?.bwiOversoldEnabled) {
                const bwiOversoldLevel = effectiveIndicatorSettings.bwiOversold || 0.0;
                bwiOversoldAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: bwiOversoldLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'transparent',
                  topFillColor2: 'transparent',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'rgba(33, 150, 243, 0.15)',
                  bottomFillColor2: 'rgba(33, 150, 243, 0.25)',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 BWI] 침체 영역 시리즈 생성 완료 (기준: ${bwiOversoldLevel})`);
              }
              
              const bwiSeries = subChart.addLineSeries({ 
                color: colorSettings?.bwiColor || '#FF5722', 
                lineWidth: (colorSettings?.bwiWidth || 2) as any,
                lineStyle: colorSettings?.bwiLineStyle ? getLineStyle(colorSettings.bwiLineStyle) : 0,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const bwiData = mapIndicatorData(d => d.bwi);
              if (bwiData.length > 0) {
                bwiSeries.setData(bwiData as any);
                
                // ✅ 영역 시리즈에도 동일한 데이터 설정
                if (bwiOverboughtAreaSeries) {
                  bwiOverboughtAreaSeries.setData(bwiData as any);
                  const overboughtCount = bwiData.filter(d => d.value > (effectiveIndicatorSettings?.bwiOverbought || 1.0)).length;
                  console.log(`✅ [CandlestickChart 서브차트 BWI] 과열 영역 데이터 설정 완료 (총: ${bwiData.length}개, 과열: ${overboughtCount}개)`);
                }
                if (bwiOversoldAreaSeries) {
                  bwiOversoldAreaSeries.setData(bwiData as any);
                  const oversoldCount = bwiData.filter(d => d.value < (effectiveIndicatorSettings?.bwiOversold || 0.0)).length;
                  console.log(`✅ [CandlestickChart 서브차트 BWI] 침체 영역 데이터 설정 완료 (총: ${bwiData.length}개, 침체: ${oversoldCount}개)`);
                }
                
                const bwiRef: {
                  main: typeof bwiSeries;
                  overbought?: NonNullable<typeof bwiOverboughtAreaSeries>;
                  oversold?: NonNullable<typeof bwiOversoldAreaSeries>;
                } = { main: bwiSeries };
                if (bwiOverboughtAreaSeries) bwiRef.overbought = bwiOverboughtAreaSeries;
                if (bwiOversoldAreaSeries) bwiRef.oversold = bwiOversoldAreaSeries;
                subSeriesRef.current[panel.type] = bwiRef;
                
                console.log(`✅ [서브차트] BWI 데이터 설정: ${bwiData.length}개`);
                
                // BWI 기준선 추가 (설정에 따라)
                if (effectiveIndicatorSettings) {
                  // 과매수선 (기본 1.0)
                  if (effectiveIndicatorSettings.bwiOverboughtEnabled) {
                    const bwiOverboughtColor = colorSettings?.bwiOverboughtColor || '#ef5350';
                    bwiSeries.createPriceLine({
                      price: effectiveIndicatorSettings.bwiOverbought || 1.0,
                      color: bwiOverboughtColor,
                      lineWidth: (colorSettings?.bwiOverboughtWidth || 1) as any,
                      lineStyle: colorSettings?.bwiOverboughtLineStyle ? getLineStyle(colorSettings.bwiOverboughtLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: bwiOverboughtColor,
                      title: '',
                    });
                  }
                  // 중앙선 (기본 0.5)
                  if (effectiveIndicatorSettings.bwiMiddleEnabled) {
                    const bwiMiddleColor = colorSettings?.bwiMiddleColor || '#9e9e9e';
                    bwiSeries.createPriceLine({
                      price: effectiveIndicatorSettings.bwiMiddle || 0.5,
                      color: bwiMiddleColor,
                      lineWidth: (colorSettings?.bwiMiddleWidth || 1) as any,
                      lineStyle: colorSettings?.bwiMiddleLineStyle ? getLineStyle(colorSettings.bwiMiddleLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: bwiMiddleColor,
                      title: '',
                    });
                  }
                  // 과매도선 (기본 0.0)
                  if (effectiveIndicatorSettings.bwiOversoldEnabled) {
                    const bwiOversoldColor = colorSettings?.bwiOversoldColor || '#26a69a';
                    bwiSeries.createPriceLine({
                      price: effectiveIndicatorSettings.bwiOversold || 0.0,
                      color: bwiOversoldColor,
                      lineWidth: (colorSettings?.bwiOversoldWidth || 1) as any,
                      lineStyle: colorSettings?.bwiOversoldLineStyle ? getLineStyle(colorSettings.bwiOversoldLineStyle) : 2,
                      axisLabelVisible: true,
                      axisLabelColor: bwiOversoldColor,
                      title: '',
                    });
                  }
                }
              }
            } else if (hasIndicatorData && panel.type === 'volumeOsc') {
              // VolumeOSC 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              const volumeOscSeries = subChart.addLineSeries({ 
                color: colorSettings?.volumeOscColor || '#795548', 
                lineWidth: (colorSettings?.volumeOscWidth || 2) as any,
                lineStyle: colorSettings?.volumeOscLineStyle ? getLineStyle(colorSettings.volumeOscLineStyle) : 0,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              const volumeOscData = mapIndicatorData(d => d.volumeOsc);
              if (volumeOscData.length > 0) {
                volumeOscSeries.setData(volumeOscData as any);
                subSeriesRef.current[panel.type] = volumeOscSeries;
                console.log(`✅ [서브차트] VolumeOSC 데이터 설정: ${volumeOscData.length}개`);
                
                // VolumeOSC 0선 기준선 (설정에 따라)
                if (effectiveIndicatorSettings && effectiveIndicatorSettings.volumeOscZeroLineEnabled) {
                  const volumeOscZeroLineColor = colorSettings?.volumeOscZeroLineColor || (chartColors.gridColor || '#999999');
                  volumeOscSeries.createPriceLine({
                    price: 0,
                    color: volumeOscZeroLineColor,
                    lineWidth: (colorSettings?.volumeOscZeroLineWidth || 1) as any,
                    lineStyle: colorSettings?.volumeOscZeroLineStyle ? getLineStyle(colorSettings.volumeOscZeroLineStyle) : 1,
                    axisLabelVisible: true,
                    axisLabelColor: volumeOscZeroLineColor,
                    title: '',
                  });
                }
              }
            } else if (hasIndicatorData && panel.type === 'vr') {
              // ✅ VR 차트 - DMI 패턴 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // ✅ VR 시리즈 (항상 생성, visible로 제어)
              const vrSeries = subChart.addLineSeries({ 
                color: colorSettings?.vrColor || '#607D8B', 
                lineWidth: (colorSettings?.vrWidth || 2) as any,
                lineStyle: colorSettings?.vrLineStyle ? getLineStyle(colorSettings.vrLineStyle) : 0,
                visible: effectiveIndicatorSettings?.vrEnabled !== false,  // ✅ VR ON/OFF 제어
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,
              });
              
              // ✅ VR2 시리즈 (항상 생성, visible로 제어 - DMI 패턴)
              const vr2Series = subChart.addLineSeries({ 
                color: colorSettings?.vr2Color || '#FF9800', 
                lineWidth: (colorSettings?.vr2Width || 2) as any,
                lineStyle: colorSettings?.vr2LineStyle ? getLineStyle(colorSettings.vr2LineStyle) : 0,
                visible: effectiveIndicatorSettings?.vr2Enabled === true,  // ✅ VR2 ON/OFF 제어
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,
              });
              
              // ✅ 데이터 항상 설정 (DMI 패턴)
              const vrData = mapIndicatorData(d => d.vr);
              const vr2Data = mapIndicatorData(d => d.vr2);
              
              if (vrData.length > 0) vrSeries.setData(vrData as any);
              if (vr2Data.length > 0) vr2Series.setData(vr2Data as any);
              
              // VR 기준선 추가 (설정에 따라)
              if (effectiveIndicatorSettings) {
                if (effectiveIndicatorSettings.vrOverboughtEnabled) {
                  const vrOverboughtColor = colorSettings?.vrOverboughtColor || '#ef5350';
                  vrSeries.createPriceLine({
                    price: effectiveIndicatorSettings.vrOverbought || 150,
                    color: vrOverboughtColor,
                    lineWidth: (colorSettings?.vrOverboughtWidth || 1) as any,
                    lineStyle: colorSettings?.vrOverboughtLineStyle ? getLineStyle(colorSettings.vrOverboughtLineStyle) : 2,
                    axisLabelVisible: true,
                    axisLabelColor: vrOverboughtColor,
                    title: '',
                  });
                }
                if (effectiveIndicatorSettings.vrBaseLineEnabled) {
                  const vrBaseLineColor = colorSettings?.vrBaseLineColor || '#9e9e9e';
                  vrSeries.createPriceLine({
                    price: effectiveIndicatorSettings.vrBaseLine || 100,
                    color: vrBaseLineColor,
                    lineWidth: (colorSettings?.vrBaseLineWidth || 1) as any,
                    lineStyle: colorSettings?.vrBaseLineLineStyle ? getLineStyle(colorSettings.vrBaseLineLineStyle) : 2,
                    axisLabelVisible: true,
                    axisLabelColor: vrBaseLineColor,
                    title: '',
                  });
                }
                if (effectiveIndicatorSettings.vrOversoldEnabled) {
                  const vrOversoldColor = colorSettings?.vrOversoldColor || '#26a69a';
                  vrSeries.createPriceLine({
                    price: effectiveIndicatorSettings.vrOversold || 70,
                    color: vrOversoldColor,
                    lineWidth: (colorSettings?.vrOversoldWidth || 1) as any,
                    lineStyle: colorSettings?.vrOversoldLineStyle ? getLineStyle(colorSettings.vrOversoldLineStyle) : 2,
                    axisLabelVisible: true,
                    axisLabelColor: vrOversoldColor,
                    title: '',
                  });
                }
              }
              
              // ✅ 시리즈 참조 저장 (DMI 패턴)
              subSeriesRef.current[panel.type] = { 
                vr: vrSeries, 
                vr2: vr2Series, 
              };
              console.log(`✅ [서브차트] VR 데이터 설정: VR ${vrData.length}개, VR2 ${vr2Data.length}개 (DMI 패턴)`);
            } else if (hasIndicatorData && panel.type === 'newPsychological') {
              // 신심리도 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // ✅ 신심리도 과열 영역 (기준선 위만) - BaselineSeries 사용
              let newPsyOverboughtAreaSeries = null;
              if (effectiveIndicatorSettings?.newPsychologicalOverboughtEnabled) {
                const newPsyOverboughtLevel = effectiveIndicatorSettings.newPsychologicalOverbought || 75;
                newPsyOverboughtAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: newPsyOverboughtLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'rgba(239, 83, 80, 0.25)',
                  topFillColor2: 'rgba(239, 83, 80, 0.15)',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'transparent',
                  bottomFillColor2: 'transparent',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 신심리도] 과열 영역 시리즈 생성 완료 (기준: ${newPsyOverboughtLevel})`);
              }
              
              // ✅ 신심리도 침체 영역 (기준선 아래만) - BaselineSeries 사용
              let newPsyOversoldAreaSeries = null;
              if (effectiveIndicatorSettings?.newPsychologicalOversoldEnabled) {
                const newPsyOversoldLevel = effectiveIndicatorSettings.newPsychologicalOversold || 25;
                newPsyOversoldAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: newPsyOversoldLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'transparent',
                  topFillColor2: 'transparent',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'rgba(33, 150, 243, 0.15)',
                  bottomFillColor2: 'rgba(33, 150, 243, 0.25)',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 신심리도] 침체 영역 시리즈 생성 완료 (기준: ${newPsyOversoldLevel})`);
              }
              
              const newPsySeries = subChart.addLineSeries({ 
                color: colorSettings?.newPsychologicalColor || '#9C27B0', 
                lineWidth: (colorSettings?.newPsychologicalWidth || 1.5) as any,
                lineStyle: colorSettings?.newPsychologicalLineStyle ? getLineStyle(colorSettings.newPsychologicalLineStyle) : 0,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              
              const newPsyData = mapIndicatorData(d => d.newPsychological ?? d.newPsy);
              
              if (newPsyData.length > 0) {
                newPsySeries.setData(newPsyData as any);
                
                // ✅ 영역 시리즈에도 동일한 데이터 설정
                if (newPsyOverboughtAreaSeries) {
                  newPsyOverboughtAreaSeries.setData(newPsyData as any);
                  const overboughtCount = newPsyData.filter(d => d.value > (effectiveIndicatorSettings?.newPsychologicalOverbought || 75)).length;
                  console.log(`✅ [CandlestickChart 서브차트 신심리도] 과열 영역 데이터 설정 완료 (총: ${newPsyData.length}개, 과열: ${overboughtCount}개)`);
                }
                if (newPsyOversoldAreaSeries) {
                  newPsyOversoldAreaSeries.setData(newPsyData as any);
                  const oversoldCount = newPsyData.filter(d => d.value < (effectiveIndicatorSettings?.newPsychologicalOversold || 25)).length;
                  console.log(`✅ [CandlestickChart 서브차트 신심리도] 침체 영역 데이터 설정 완료 (총: ${newPsyData.length}개, 침체: ${oversoldCount}개)`);
                }
              }
              
              const newPsyRef: {
                newPsy: typeof newPsySeries;
                overbought?: NonNullable<typeof newPsyOverboughtAreaSeries>;
                oversold?: NonNullable<typeof newPsyOversoldAreaSeries>;
              } = { newPsy: newPsySeries };
              if (newPsyOverboughtAreaSeries) newPsyRef.overbought = newPsyOverboughtAreaSeries;
              if (newPsyOversoldAreaSeries) newPsyRef.oversold = newPsyOversoldAreaSeries;
              subSeriesRef.current[panel.type] = newPsyRef;
              
              // ✅ 신심리도 데이터 검증 로그 (최근 5개)
              const newPsyDebug = newPsyData.slice(-5).map(d => `${d.time}: ${d.value?.toFixed(2) || 'null'}`);
              console.log(`✅ [서브차트] 신심리도 데이터 설정: ${newPsyData.length}개`);
              console.log(`   📊 [신심리도] 기간: ${effectiveIndicatorSettings?.newPsychologicalPeriod || 5}일, 최근 5개:`, newPsyDebug);
              
              // 신심리도 기준선 추가 (설정에 따라)
              if (effectiveIndicatorSettings) {
                if (effectiveIndicatorSettings.newPsychologicalOverboughtEnabled) {
                  const newPsyOverboughtColor = colorSettings?.newPsychologicalOverboughtColor || '#ef4444';
                  newPsySeries.createPriceLine({
                    price: effectiveIndicatorSettings.newPsychologicalOverbought || 75,
                    color: newPsyOverboughtColor,
                    lineWidth: (colorSettings?.newPsychologicalOverboughtWidth || 0.5) as any,
                    lineStyle: colorSettings?.newPsychologicalOverboughtLineStyle ? getLineStyle(colorSettings.newPsychologicalOverboughtLineStyle) : 0,
                    axisLabelVisible: true,
                    axisLabelColor: newPsyOverboughtColor,
                    title: '',
                  });
                }
                if (effectiveIndicatorSettings.newPsychologicalMiddleEnabled) {
                  const newPsyNeutralColor = colorSettings?.newPsychologicalNeutralColor || '#666666';
                  newPsySeries.createPriceLine({
                    price: effectiveIndicatorSettings.newPsychologicalMiddle || 50,
                    color: newPsyNeutralColor,
                    lineWidth: (colorSettings?.newPsychologicalNeutralWidth || 0.5) as any,
                    lineStyle: colorSettings?.newPsychologicalNeutralLineStyle ? getLineStyle(colorSettings.newPsychologicalNeutralLineStyle) : 0,
                    axisLabelVisible: true,
                    axisLabelColor: newPsyNeutralColor,
                    title: '',
                  });
                }
                if (effectiveIndicatorSettings.newPsychologicalOversoldEnabled) {
                  const newPsyOversoldColor = colorSettings?.newPsychologicalOversoldColor || '#22c55e';
                  newPsySeries.createPriceLine({
                    price: effectiveIndicatorSettings.newPsychologicalOversold || 25,
                    color: newPsyOversoldColor,
                    lineWidth: (colorSettings?.newPsychologicalOversoldWidth || 0.5) as any,
                    lineStyle: colorSettings?.newPsychologicalOversoldLineStyle ? getLineStyle(colorSettings.newPsychologicalOversoldLineStyle) : 0,
                    axisLabelVisible: true,
                    axisLabelColor: newPsyOversoldColor,
                    title: '',
                  });
                }
              }
            } else if (hasIndicatorData && panel.type === 'investPsychological') {
              // 투자심리도 차트 - 설정 색상/굵기/스타일 적용
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // ✅ 투자심리도 과열 영역 (기준선 위만) - BaselineSeries 사용
              let investPsyOverboughtAreaSeries = null;
              if (effectiveIndicatorSettings?.investPsychologicalOverboughtEnabled) {
                const investPsyOverboughtLevel = effectiveIndicatorSettings.investPsychologicalOverbought || 75;
                investPsyOverboughtAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: investPsyOverboughtLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'rgba(239, 83, 80, 0.25)',
                  topFillColor2: 'rgba(239, 83, 80, 0.15)',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'transparent',
                  bottomFillColor2: 'transparent',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 투자심리도] 과열 영역 시리즈 생성 완료 (기준: ${investPsyOverboughtLevel})`);
              }
              
              // ✅ 투자심리도 침체 영역 (기준선 아래만) - BaselineSeries 사용
              let investPsyOversoldAreaSeries = null;
              if (effectiveIndicatorSettings?.investPsychologicalOversoldEnabled) {
                const investPsyOversoldLevel = effectiveIndicatorSettings.investPsychologicalOversold || 25;
                investPsyOversoldAreaSeries = subChart.addBaselineSeries({
                  baseValue: { type: 'price', price: investPsyOversoldLevel },
                  topLineColor: 'transparent',
                  topFillColor1: 'transparent',
                  topFillColor2: 'transparent',
                  bottomLineColor: 'transparent',
                  bottomFillColor1: 'rgba(33, 150, 243, 0.15)',
                  bottomFillColor2: 'rgba(33, 150, 243, 0.25)',
                  lineVisible: false,
                  priceFormat: {
                    type: 'price',
                    precision: 2,
                    minMove: 0.01,
                  },
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                console.log(`✅ [CandlestickChart 서브차트 투자심리도] 침체 영역 시리즈 생성 완료 (기준: ${investPsyOversoldLevel})`);
              }
              
              const investPsySeries = subChart.addLineSeries({ 
                color: colorSettings?.investPsychologicalColor || '#673AB7', 
                lineWidth: (colorSettings?.investPsychologicalWidth || 1.5) as any,
                lineStyle: colorSettings?.investPsychologicalLineStyle ? getLineStyle(colorSettings.investPsychologicalLineStyle) : 0,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,  // ✅ 자동 가격선 숨김
              });
              
              const investPsyData = mapIndicatorData(d => d.investPsychological ?? d.investPsy);
              
              if (investPsyData.length > 0) {
                investPsySeries.setData(investPsyData as any);
                
                // ✅ 영역 시리즈에도 동일한 데이터 설정
                if (investPsyOverboughtAreaSeries) {
                  investPsyOverboughtAreaSeries.setData(investPsyData as any);
                  const overboughtCount = investPsyData.filter(d => d.value > (effectiveIndicatorSettings?.investPsychologicalOverbought || 75)).length;
                  console.log(`✅ [CandlestickChart 서브차트 투자심리도] 과열 영역 데이터 설정 완료 (총: ${investPsyData.length}개, 과열: ${overboughtCount}개)`);
                }
                if (investPsyOversoldAreaSeries) {
                  investPsyOversoldAreaSeries.setData(investPsyData as any);
                  const oversoldCount = investPsyData.filter(d => d.value < (effectiveIndicatorSettings?.investPsychologicalOversold || 25)).length;
                  console.log(`✅ [CandlestickChart 서브차트 투자심리도] 침체 영역 데이터 설정 완료 (총: ${investPsyData.length}개, 침체: ${oversoldCount}개)`);
                }
              }
              
              const investPsyRef: {
                investPsy: typeof investPsySeries;
                overbought?: NonNullable<typeof investPsyOverboughtAreaSeries>;
                oversold?: NonNullable<typeof investPsyOversoldAreaSeries>;
              } = { investPsy: investPsySeries };
              if (investPsyOverboughtAreaSeries) investPsyRef.overbought = investPsyOverboughtAreaSeries;
              if (investPsyOversoldAreaSeries) investPsyRef.oversold = investPsyOversoldAreaSeries;
              subSeriesRef.current[panel.type] = investPsyRef;
              
              // ✅ 투자심리도 데이터 검증 로그 (최근 5개)
              const investPsyDebug = investPsyData.slice(-5).map(d => `${d.time}: ${d.value?.toFixed(2) || 'null'}`);
              console.log(`✅ [서브차트] 투자심리도 데이터 설정: ${investPsyData.length}개`);
              console.log(`   📊 [투자심리도] 기간: ${effectiveIndicatorSettings?.investPsychologicalPeriod || 10}일, 최근 5개:`, investPsyDebug);
              
              // 투자심리도 기준선 추가 (설정에 따라)
              if (effectiveIndicatorSettings) {
                if (effectiveIndicatorSettings.investPsychologicalOverboughtEnabled) {
                  const investPsyOverboughtColor = colorSettings?.investPsychologicalOverboughtColor || '#EF5350';
                  investPsySeries.createPriceLine({
                    price: effectiveIndicatorSettings.investPsychologicalOverbought || 75,
                    color: investPsyOverboughtColor,
                    lineWidth: (colorSettings?.investPsychologicalOverboughtWidth || 1) as any,
                    lineStyle: colorSettings?.investPsychologicalOverboughtLineStyle ? getLineStyle(colorSettings.investPsychologicalOverboughtLineStyle) : 0,
                    axisLabelVisible: true,
                    axisLabelColor: investPsyOverboughtColor,
                    title: '',
                  });
                }
                if (effectiveIndicatorSettings.investPsychologicalMiddleEnabled) {
                  const investPsyMiddleColor = colorSettings?.investPsychologicalMiddleColor || '#999999';
                  investPsySeries.createPriceLine({
                    price: effectiveIndicatorSettings.investPsychologicalMiddle || 50,
                    color: investPsyMiddleColor,
                    lineWidth: (colorSettings?.investPsychologicalMiddleWidth || 1) as any,
                    lineStyle: colorSettings?.investPsychologicalMiddleLineStyle ? getLineStyle(colorSettings.investPsychologicalMiddleLineStyle) : 0,
                    axisLabelVisible: true,
                    axisLabelColor: investPsyMiddleColor,
                    title: '',
                  });
                }
                if (effectiveIndicatorSettings.investPsychologicalOversoldEnabled) {
                  const investPsyOversoldColor = colorSettings?.investPsychologicalOversoldColor || '#26A69A';
                  investPsySeries.createPriceLine({
                    price: effectiveIndicatorSettings.investPsychologicalOversold || 25,
                    color: investPsyOversoldColor,
                    lineWidth: (colorSettings?.investPsychologicalOversoldWidth || 1) as any,
                    lineStyle: colorSettings?.investPsychologicalOversoldLineStyle ? getLineStyle(colorSettings.investPsychologicalOversoldLineStyle) : 0,
                    axisLabelVisible: true,
                    axisLabelColor: investPsyOversoldColor,
                    title: '',
                  });
                }
              }
            } else if (hasIndicatorData && panel.type === 'dmi') {
              // DMI 차트 (PDI, MDI) - 업비트 방식
              const getLineStyle = (style: string) => style === 'solid' ? 0 : style === 'dotted' ? 1 : 2;
              
              // PDI (+DI) 시리즈
              const plusDiSeries = subChart.addLineSeries({ 
                color: colorSettings?.plusDiColor || '#808080', 
                lineWidth: (colorSettings?.plusDiWidth || 1.5) as any,
                lineStyle: colorSettings?.plusDiLineStyle ? getLineStyle(colorSettings.plusDiLineStyle) : 0,
                visible: effectiveIndicatorSettings?.plusDiEnabled !== false,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,
              });
              
              // MDI (-DI) 시리즈
              const minusDiSeries = subChart.addLineSeries({ 
                color: colorSettings?.minusDiColor || '#FF00FF', 
                lineWidth: (colorSettings?.minusDiWidth || 1.5) as any,
                lineStyle: colorSettings?.minusDiLineStyle ? getLineStyle(colorSettings.minusDiLineStyle) : 0,
                visible: effectiveIndicatorSettings?.minusDiEnabled !== false,
                lastValueVisible: true,  // ✅ 실시간 값 표시
                priceLineVisible: false,
              });
              
              const plusDiData = mapIndicatorData(d => d.plusDI ?? d.plusDi);
              const minusDiData = mapIndicatorData(d => d.minusDI ?? d.minusDi);
              
              if (plusDiData.length > 0) plusDiSeries.setData(plusDiData as any);
              if (minusDiData.length > 0) minusDiSeries.setData(minusDiData as any);
              
              // DMI 기준선 추가 (설정에 따라)
              if (effectiveIndicatorSettings) {
                // 과열선 (기본 30)
                if (effectiveIndicatorSettings.dmiOverboughtEnabled) {
                  plusDiSeries.createPriceLine({
                    price: effectiveIndicatorSettings.dmiOverbought || 30,
                    color: colorSettings?.dmiOverboughtColor || '#EF5350',
                    lineWidth: (colorSettings?.dmiOverboughtWidth || 1) as any,
                    lineStyle: colorSettings?.dmiOverboughtLineStyle ? getLineStyle(colorSettings.dmiOverboughtLineStyle) : 0,
                    axisLabelVisible: true,
                    title: ''
                  });
                }
                // 중간값선 (기본 20)
                if (effectiveIndicatorSettings.dmiMiddleEnabled) {
                  plusDiSeries.createPriceLine({
                    price: effectiveIndicatorSettings.dmiMiddle || 20,
                    color: colorSettings?.dmiMiddleColor || '#FFA726',
                    lineWidth: (colorSettings?.dmiMiddleWidth || 1) as any,
                    lineStyle: colorSettings?.dmiMiddleLineStyle ? getLineStyle(colorSettings.dmiMiddleLineStyle) : 0,
                    axisLabelVisible: true,
                    title: ''
                  });
                }
                // 침체선 (기본 10)
                if (effectiveIndicatorSettings.dmiOversoldEnabled) {
                  plusDiSeries.createPriceLine({
                    price: effectiveIndicatorSettings.dmiOversold || 10,
                    color: colorSettings?.dmiOversoldColor || '#26A69A',
                    lineWidth: (colorSettings?.dmiOversoldWidth || 1) as any,
                    lineStyle: colorSettings?.dmiOversoldLineStyle ? getLineStyle(colorSettings.dmiOversoldLineStyle) : 0,
                    axisLabelVisible: true,
                    title: ''
                  });
                }
              }
              
              subSeriesRef.current[panel.type] = { 
                plusDi: plusDiSeries, 
                minusDi: minusDiSeries, 
              };
              console.log(`✅ [서브차트] DMI 데이터 설정 (업비트 스타일): +DI ${plusDiData.length}개, -DI ${minusDiData.length}개`);
              console.log(`   📊 [DMI] 기간: ${effectiveIndicatorSettings?.dmiPeriod || 14}일, 최근 5개:`, 
                plusDiData.slice(-5).map((d, i) => {
                  const mdiValue = minusDiData.slice(-5)[i]?.value?.toFixed(2) || 'N/A';
                  return `${new Date((d.time as number) * 1000).toLocaleDateString()}: +DI=${d.value?.toFixed(2)}, -DI=${mdiValue}`;
                }));
            }
            
            // ✅ 일목균형표 활성화 시: 투명한 범위 확장용 시리즈 추가 (모든 보조지표 공통)
            if (isSenkouSpanEnabled && data.length > 0) {
              // 전체 data 범위(미래 영역 포함)에 대한 투명한 라인 시리즈 생성
              const rangeExtensionSeries = subChart.addLineSeries({
                color: 'transparent',
                lineWidth: 1,
                lastValueVisible: false,
                priceLineVisible: false,
                crosshairMarkerVisible: false,
                priceScaleId: 'right',
                visible: false,  // ✅ 완전히 숨김
              });
              
              // 전체 시간 범위의 더미 데이터 생성 (value는 0)
              const mainDisp = mainCandleDisplayTimesRef.current;
              const rangeExtensionData =
                mainDisp.length > 0
                  ? mainDisp
                      .filter((sec) => sec > 0)
                      .map((sec) => ({ time: sec as Time, value: 0 }))
                  : data
                      .filter((d) => d.time)
                      .map((d) => ({
                        time: parseTime(d.time),
                        value: 0,
                      }))
                      .filter((d) => d.time !== 0);
              
              if (rangeExtensionData.length > 0) {
                rangeExtensionSeries.setData(rangeExtensionData as any);
                console.log(`📏 [서브차트 ${panel.type}] 범위 확장용 투명 시리즈 추가: ${rangeExtensionData.length}개 (미래 영역 포함)`);
              }
            }
          }

          // 메인 차트와 time scale 동기화
          const mainTimeScale = chartRef.current!.timeScale();
          const subTimeScale = subChart.timeScale();

          // ✅ 메인 timeScale 가시 범위를 서브에 복사 (선행스팬 ON: 시간 기준 — 논리봉 수 불일치 대응)
          try {
            applyMainVisibleToSubCharts(mainTimeScale, subChartsRef.current, senkouSpanActiveForSubSyncRef.current);
            const mr = mainTimeScale.getVisibleLogicalRange();
            if (mr) {
              console.log(`✅ [서브차트 ${panel.type}] 초기 범위 동기화 완료 (선행스팬=${senkouSpanActiveForSubSyncRef.current ? '시간' : 'logical'}), main logical: ${mr.from} ~ ${mr.to}`);
            }
          } catch (e) {
            console.warn(`서브차트 ${panel.type} 초기 범위 설정 실패:`, e);
          }

          // ✅ 메인 차트 logical range 변경 시 모든 서브 동기화
          mainTimeScale.subscribeVisibleLogicalRangeChange(() => {
            // ✅ 범위 설정 중이면 무시 (무한 루프 방지)
            if (isSettingRangeRef.current) return;
            
            try {
              applyMainVisibleToSubCharts(mainTimeScale, subChartsRef.current, senkouSpanActiveForSubSyncRef.current);
            } catch (e) {
              // 에러 무시
            }
          });

          // ✅ 크로스헤어 동기화는 모든 서브차트 생성 후에 설정됩니다 (하단 참조)

          // ✅ 데이터 설정 완료 후 재동기화 (다음 프레임 — lightweight-charts "Value is null" 방지)
          const mainLogicalRange = mainTimeScale.getVisibleLogicalRange();
          if (mainLogicalRange || senkouSpanActiveForSubSyncRef.current) {
            requestAnimationFrame(() => {
              if (isDisposedRef.current || !subChartsRef.current[panel.type]) return;
              try {
                applyMainVisibleToSubCharts(mainTimeScale, subChartsRef.current, senkouSpanActiveForSubSyncRef.current);
                console.log(`✅ [서브차트 ${panel.type}] 데이터 설정 후 범위 재적용`);
              } catch (e) {
                // 에러 무시
              }
            });
          }

          console.log(`✅ [서브차트] ${panel.type} 생성 완료`);
          
          // ✅ 디버깅: 생성 직후 실제 logical range와 scrollPosition 확인 (cleanup 시 취소)
          const timeout200 = window.setTimeout(() => {
            if (isDisposedRef.current) return;
            const actualLogicalRange = subTimeScale.getVisibleLogicalRange();
            const actualScrollPosition = subTimeScale.scrollPosition();
            const mainLogicalRange = mainTimeScale.getVisibleLogicalRange();
            const mainScrollPosition = mainTimeScale.scrollPosition();
            
            if (actualLogicalRange) {
              console.log(`🔍 [${panel.type}] 실제 logical range: ${actualLogicalRange.from} ~ ${actualLogicalRange.to}, scroll: ${actualScrollPosition}`);
            }
            if (mainLogicalRange) {
              console.log(`🔍 [메인] 현재 logical range: ${mainLogicalRange.from} ~ ${mainLogicalRange.to}, scroll: ${mainScrollPosition}`);
            }
            
            // 선행스팬 ON: 논리 인덱스 대신 시간 가시구간으로 불일치 검사
            if (!isDisposedRef.current) {
              if (isSenkouSpanEnabled) {
                const mainVr = mainTimeScale.getVisibleRange();
                const subVr = subTimeScale.getVisibleRange();
                if (mainVr && subVr && mainVr.from != null && mainVr.to != null && subVr.from != null && subVr.to != null) {
                  const timeDiff =
                    Math.abs(Number(mainVr.from) - Number(subVr.from)) +
                    Math.abs(Number(mainVr.to) - Number(subVr.to));
                  if (timeDiff > 0.5) {
                    console.warn(`⚠️ [${panel.type}] 시간 범위 불일치 (${timeDiff}), 재적용`);
                    applyMainVisibleToSubCharts(mainTimeScale, subChartsRef.current, true);
                  } else {
                    console.log(`✅ [${panel.type}] 시간 범위 일치 (Δ≈${timeDiff.toFixed(3)}s)`);
                  }
                }
              } else if (actualLogicalRange && mainLogicalRange) {
                const rangeDiff = Math.abs(actualLogicalRange.from - mainLogicalRange.from) +
                  Math.abs(actualLogicalRange.to - mainLogicalRange.to);
                const scrollDiff = Math.abs(actualScrollPosition - mainScrollPosition);

                if (rangeDiff > 5.0 || scrollDiff > 1.0) {
                  console.warn(`⚠️ [${panel.type}] 불일치 감지! range차이: ${rangeDiff}, scroll차이: ${scrollDiff}, 재적용 중...`);
                  const subChartNow = subChartsRef.current[panel.type];
                  if (!isDisposedRef.current && subChartNow) {
                    try {
                      subChartNow.timeScale().setVisibleLogicalRange(mainLogicalRange);
                      console.log(`✅ [${panel.type}] logical range 재적용 완료`);
                    } catch (e) {
                      if (!isDisposedRef.current) console.error(`❌ [${panel.type}] 재적용 실패:`, e);
                    }
                  }
                } else {
                  console.log(`✅ [${panel.type}] logical range & scrollPosition 일치 (range차이: ${rangeDiff.toFixed(2)}, scroll차이: ${scrollDiff.toFixed(2)})`);
                }
              }
            }
          }, 200);
          syncTimeoutsRef.current.push(timeout200);
        } catch (error) {
          console.error(`서브차트 생성 실패 (${panel.type}):`, error);
        }
      });

      // ✅ 모든 보조지표 패널에 투명 범위 확장 시리즈 추가 (X축 동기화)
      if (indicatorData && indicatorData.length > 0) {
        const firstTime = indicatorData[0]?.time || indicatorData[0]?.timestamp;
        const lastTime = indicatorData[indicatorData.length - 1]?.time || indicatorData[indicatorData.length - 1]?.timestamp;
        
        // 시간 파싱 헬퍼 함수
        const parseTimeToLightweight = (timeStr: string | undefined): Time => {
          if (!timeStr) return 0 as Time;
          
          try {
            // 문자열 타임스탬프 변환
            const date = new Date(timeStr);
            if (isNaN(date.getTime())) {
              console.error('Invalid time:', timeStr);
              return 0 as Time;
            }
            // lightweight-charts는 초 단위 타임스탬프를 사용
            return Math.floor(date.getTime() / 1000) as Time;
          } catch (e) {
            console.error('Time parsing error:', e, timeStr);
            return 0 as Time;
          }
        };
        
        if (firstTime && lastTime) {
          const parsedFirstTime = parseTimeToLightweight(firstTime);
          const parsedLastTime = parseTimeToLightweight(lastTime);
          
          if (parsedFirstTime !== 0 && parsedLastTime !== 0) {
            enabledPanels.forEach(panel => {
              const subChart = subChartsRef.current[panel.type];
              if (!subChart) return;
              
              try {
                const rangeExtensionSeries = subChart.addLineSeries({
                  color: 'rgba(0,0,0,0)',  // 완전 투명
                  lineWidth: 1,
                  priceScaleId: 'right',
                  lastValueVisible: false,
                  priceLineVisible: false,
                });
                
                rangeExtensionSeries.setData([
                  { time: parsedFirstTime, value: 0 },
                  { time: parsedLastTime, value: 0 }
                ]);
                
                console.log(`✅ [서브차트 ${panel.type}] X축 범위 확장 시리즈 추가: ${firstTime} ~ ${lastTime}`);
              } catch (error) {
                console.error(`❌ [서브차트 ${panel.type}] 범위 확장 시리즈 추가 실패:`, error);
              }
            });
          }
        }
      }

      // ✅ 모든 서브차트 생성 후 메인 차트와 범위 재동기화 & 크로스헤어 동기화
      if (chartRef.current && enabledPanels.length > 0) {
        const mainChart = chartRef.current;
        const mainTimeScale = mainChart.timeScale();
        
        // ✅ 크로스헤어 동기화: 메인 차트의 크로스헤어를 모든 서브차트로 전파
        const crosshairHandler = (param: any) => {
          // 모든 서브차트에 동일한 크로스헤어 이벤트 전파
          enabledPanels.forEach(panel => {
            const subChart = subChartsRef.current[panel.type];
            if (!subChart) return;
            
            if (!param || !param.time) {
              // 크로스헤어가 없을 때는 서브차트도 숨김
              try {
                subChart.clearCrosshairPosition();
              } catch (e) {
                // 무시
              }
              return;
            }

            // 서브차트의 시리즈가 있는지 확인
            const subSeries = subSeriesRef.current[panel.type];
            if (!subSeries) return;
            
            // 시리즈 타입에 따라 적절한 시리즈 선택
            let targetSeries: any = null;
            
            if (typeof subSeries === 'object' && subSeries !== null) {
              if ('histogram' in subSeries && (subSeries as any).histogram) targetSeries = (subSeries as any).histogram;
              else if ('macdLine' in subSeries) targetSeries = subSeries.macdLine;
              else if ('kLine' in subSeries) targetSeries = subSeries.kLine;
              else if ('main' in subSeries && (subSeries as any).main) targetSeries = (subSeries as any).main;
              else if ('plusDi' in subSeries) targetSeries = subSeries.plusDi;
              else if ('newPsy' in subSeries) targetSeries = subSeries.newPsy;
              else if ('ultraShort' in subSeries) targetSeries = subSeries.ultraShort;
              else if ('trix' in subSeries) targetSeries = subSeries.trix;
              else {
                const values = Object.values(subSeries);
                targetSeries = values.length > 0 ? values[0] : null;
              }
            } else {
              targetSeries = subSeries;
            }
            
            if (targetSeries && param.point) {
              try {
                // ✅ 좌표 기반으로 크로스헤어 위치 설정 (coordinate를 price로 변환)
                const price = targetSeries.coordinateToPrice(param.point.y);
                if (price !== null) {
                  subChart.setCrosshairPosition(
                    price,
                    param.time,
                    targetSeries
                  );
                }
              } catch (e) {
                // 에러 무시 (데이터 범위 밖일 수 있음)
              }
            }
          });
        };
        
        mainChart.subscribeCrosshairMove(crosshairHandler);
        
        // ✅ 서브차트에서 메인 차트 및 다른 서브차트로 크로스헤어 동기화 (역방향)
        enabledPanels.forEach(sourcePanel => {
          const sourceChart = subChartsRef.current[sourcePanel.type];
          if (!sourceChart) return;
          
          sourceChart.subscribeCrosshairMove((param: any) => {
            // ✅ 업비트 스타일 크로스헤어: 전역 수직선 위치 업데이트
            if (param && param.point) {
              setGlobalCrosshairX(param.point.x);
              setActiveCrosshairChart(sourcePanel.type);
            } else {
              setGlobalCrosshairX(null);
              setActiveCrosshairChart(null);
            }
            
            // 크로스헤어가 없을 때
            if (!param || !param.time) {
              // 메인 차트 크로스헤어 숨김
              try {
                mainChart.clearCrosshairPosition();
              } catch (e) {
                // 무시
              }
              
              // 다른 모든 서브차트 크로스헤어 숨김
              enabledPanels.forEach(targetPanel => {
                if (targetPanel.type === sourcePanel.type) return; // 자기 자신 제외
                const targetChart = subChartsRef.current[targetPanel.type];
                if (targetChart) {
                  try {
                    targetChart.clearCrosshairPosition();
                  } catch (e) {
                    // 무시
                  }
                }
              });
              return;
            }
            
            // 메인 차트에 크로스헤어 설정
            const mainSeries = candleSeriesRef.current;
            if (mainSeries && param.point) {
              try {
                // ✅ 좌표 기반으로 크로스헤어 위치 설정 (coordinate를 price로 변환)
                const price = mainSeries.coordinateToPrice(param.point.y);
                if (price !== null) {
                  mainChart.setCrosshairPosition(
                    price,
                    param.time,
                    mainSeries
                  );
                }
              } catch (e) {
                // 에러 무시
              }
            }
            
            // 다른 모든 서브차트에 크로스헤어 설정
            enabledPanels.forEach(targetPanel => {
              if (targetPanel.type === sourcePanel.type) return; // 자기 자신 제외
              
              const targetChart = subChartsRef.current[targetPanel.type];
              if (!targetChart) return;
              
              const targetSeries = subSeriesRef.current[targetPanel.type];
              if (!targetSeries) return;
              
              // 시리즈 타입에 따라 적절한 시리즈 선택
              let series: any = null;
              if (typeof targetSeries === 'object' && targetSeries !== null) {
                if ('histogram' in targetSeries && (targetSeries as any).histogram) series = (targetSeries as any).histogram;
                else if ('macdLine' in targetSeries) series = targetSeries.macdLine;
                else if ('kLine' in targetSeries) series = targetSeries.kLine;
                else if ('main' in targetSeries && (targetSeries as any).main) series = (targetSeries as any).main;
                else if ('plusDi' in targetSeries) series = targetSeries.plusDi;
                else if ('newPsy' in targetSeries) series = targetSeries.newPsy;
                else if ('ultraShort' in targetSeries) series = targetSeries.ultraShort;
                else if ('trix' in targetSeries) series = targetSeries.trix;
                else {
                  const values = Object.values(targetSeries);
                  series = values.length > 0 ? values[0] : null;
                }
              } else {
                series = targetSeries;
              }
              
              if (series && param.point) {
                try {
                  // ✅ 좌표 기반으로 크로스헤어 위치 설정 (coordinate를 price로 변환)
                  const price = series.coordinateToPrice(param.point.y);
                  if (price !== null) {
                    targetChart.setCrosshairPosition(
                      price,
                      param.time,
                      series
                    );
                  }
                } catch (e) {
                  // 에러 무시
                }
              }
            });
          });
        });
        
        console.log(`✅ [크로스헤어 역방향 동기화] 모든 서브차트에서 메인 차트로 연결 완료: ${enabledPanels.length}개`);
        
        // ✅ 모든 서브차트 생성 완료 후 메인과 동일한 가시 범위 적용 (선행스팬: 시간 기준)
        const mainLogicalRange = mainTimeScale.getVisibleLogicalRange();
        const mainScrollPosition = mainTimeScale.scrollPosition();
        const syncSubsFromMain = () => {
          applyMainVisibleToSubCharts(mainTimeScale, subChartsRef.current, senkouSpanActiveForSubSyncRef.current);
        };

        if (mainLogicalRange || senkouSpanActiveForSubSyncRef.current) {
          console.log(`🔒 [최종 범위 동기화] main logical: ${mainLogicalRange?.from} ~ ${mainLogicalRange?.to}, scroll: ${mainScrollPosition}`);
          syncSubsFromMain();

          requestAnimationFrame(() => {
            const currentLogicalRange = mainTimeScale.getVisibleLogicalRange();
            if (currentLogicalRange || senkouSpanActiveForSubSyncRef.current) {
              syncSubsFromMain();
            }

            // 3단계: 추가 안전장치 (100ms 후) — 그 안에 500ms/1000ms 재확인
            const timeout100 = window.setTimeout(() => {
              if (isUserInteractingRef.current) {
                console.log(`⏸️ [동기화 스킵] 사용자 인터랙션 감지됨 (100ms)`);
                return;
              }
              syncSubsFromMain();
              console.log(`✅ [서브차트] 100ms 후 범위 동기화`);

              const timeout500 = window.setTimeout(() => {
                if (isUserInteractingRef.current) {
                  console.log(`⏸️ [동기화 스킵] 사용자 인터랙션 감지됨 (500ms)`);
                  return;
                }
                if (senkouSpanActiveForSubSyncRef.current) {
                  enabledPanels.forEach((panel) => {
                    const subChart = subChartsRef.current[panel.type];
                    if (!subChart) return;
                    const mainVr = mainTimeScale.getVisibleRange();
                    const subVr = subChart.timeScale().getVisibleRange();
                    if (mainVr && subVr && mainVr.from != null && subVr.from != null) {
                      const timeDiff =
                        Math.abs(Number(mainVr.from) - Number(subVr.from)) +
                        Math.abs(Number(mainVr.to) - Number(subVr.to));
                      if (timeDiff > 0.5) {
                        syncSubsFromMain();
                        console.log(`🔄 [서브차트 ${panel.type}] 500ms 시간범위 재동기화 (Δ=${timeDiff})`);
                      }
                    }
                  });
                } else {
                  const logicalRange500 = mainTimeScale.getVisibleLogicalRange();
                  const scrollPosition500 = mainTimeScale.scrollPosition();
                  if (logicalRange500) {
                    enabledPanels.forEach((panel) => {
                      const subChart = subChartsRef.current[panel.type];
                      if (!subChart) return;
                      try {
                        const actualLogicalRange = subChart.timeScale().getVisibleLogicalRange();
                        const actualScrollPosition = subChart.timeScale().scrollPosition();
                        const rangeDiff = actualLogicalRange
                          ? Math.abs(actualLogicalRange.from - logicalRange500.from) +
                            Math.abs(actualLogicalRange.to - logicalRange500.to)
                          : 999;
                        const scrollDiff = Math.abs(actualScrollPosition - scrollPosition500);
                        if (rangeDiff > 5.0 || scrollDiff > 1.0) {
                          subChart.timeScale().setVisibleLogicalRange(logicalRange500);
                          console.log(`🔄 [서브차트 ${panel.type}] 500ms 재동기화 (range차이: ${rangeDiff}, scroll차이: ${scrollDiff})`);
                        }
                      } catch {
                        /* ignore */
                      }
                    });
                  }
                }
              }, 500);
              syncTimeoutsRef.current.push(timeout500);

              const timeout1000 = window.setTimeout(() => {
                if (isUserInteractingRef.current) {
                  console.log(`⏸️ [동기화 스킵] 사용자 인터랙션 감지됨 (1000ms)`);
                  return;
                }
                if (senkouSpanActiveForSubSyncRef.current) {
                  enabledPanels.forEach((panel) => {
                    const subChart = subChartsRef.current[panel.type];
                    if (!subChart) return;
                    const mainVr = mainTimeScale.getVisibleRange();
                    const subVr = subChart.timeScale().getVisibleRange();
                    if (mainVr && subVr && mainVr.from != null && subVr.from != null) {
                      const timeDiff =
                        Math.abs(Number(mainVr.from) - Number(subVr.from)) +
                        Math.abs(Number(mainVr.to) - Number(subVr.to));
                      if (timeDiff > 0.5) {
                        syncSubsFromMain();
                        console.log(`🔄 [서브차트 ${panel.type}] 1000ms 시간범위 재동기화 (Δ=${timeDiff})`);
                      } else {
                        console.log(`✅ [서브차트 ${panel.type}] 최종 확인: 시간 범위 일치 (Δ≈${timeDiff.toFixed(3)}s)`);
                      }
                    }
                  });
                } else {
                  const logicalRange1000 = mainTimeScale.getVisibleLogicalRange();
                  const scrollPosition1000 = mainTimeScale.scrollPosition();
                  if (logicalRange1000) {
                    enabledPanels.forEach((panel) => {
                      const subChart = subChartsRef.current[panel.type];
                      if (!subChart) return;
                      try {
                        const actualLogicalRange = subChart.timeScale().getVisibleLogicalRange();
                        const actualScrollPosition = subChart.timeScale().scrollPosition();
                        const rangeDiff = actualLogicalRange
                          ? Math.abs(actualLogicalRange.from - logicalRange1000.from) +
                            Math.abs(actualLogicalRange.to - logicalRange1000.to)
                          : 999;
                        const scrollDiff = Math.abs(actualScrollPosition - scrollPosition1000);
                        if (rangeDiff > 5.0 || scrollDiff > 1.0) {
                          subChart.timeScale().setVisibleLogicalRange(logicalRange1000);
                          console.log(`🔄 [서브차트 ${panel.type}] 1000ms 최종 재동기화 (range차이: ${rangeDiff}, scroll차이: ${scrollDiff})`);
                        } else {
                          console.log(
                            `✅ [서브차트 ${panel.type}] 최종 확인: logical 일치 (range차이: ${rangeDiff.toFixed(2)}, scroll차이: ${scrollDiff.toFixed(2)})`
                          );
                        }
                      } catch {
                        /* ignore */
                      }
                    });
                  }
                }
              }, 1000);
              syncTimeoutsRef.current.push(timeout1000);
            }, 100);
            syncTimeoutsRef.current.push(timeout100);
          });
        }
        
        console.log(`✅ [크로스헤어 동기화] 모든 서브차트 연결 완료: ${enabledPanels.length}개`);
      }

      // ✅✅ 메인 차트의 스크롤/줌 이벤트를 서브차트에 실시간 동기화
      let syncAnimationFrameId: number | null = null;
      const mainChartLogicalRangeChangeHandler = () => {
        // requestAnimationFrame으로 브라우저 렌더링 사이클에 맞춰 동기화 (부드러운 스크롤)
        if (syncAnimationFrameId) {
          cancelAnimationFrame(syncAnimationFrameId);
        }
        
        syncAnimationFrameId = requestAnimationFrame(() => {
          if (!chartRef.current) return;
          const ts = chartRef.current.timeScale();
          if (!ts.getVisibleLogicalRange() && !senkouSpanActiveForSubSyncRef.current) return;
          applyMainVisibleToSubCharts(ts, subChartsRef.current, senkouSpanActiveForSubSyncRef.current);
          syncAnimationFrameId = null;
        });
      };
      
      if (chartRef.current) {
        chartRef.current.timeScale().subscribeVisibleLogicalRangeChange(mainChartLogicalRangeChangeHandler);
        console.log(`✅ [실시간 동기화] 메인 차트 스크롤/줌 이벤트 리스너 등록`);
      }

      // 리사이즈 핸들러 (✅ 메인 차트 너비와 강제 동기화)
      const handleResize = () => {
        // ✅ 메인 차트의 실제 clientWidth 가져오기
        const mainChartWidth = chartContainerRef.current?.clientWidth;
        
        enabledPanels.forEach(panel => {
          const container = subChartContainersRef.current[panel.type];
          const chart = subChartsRef.current[panel.type];
          if (container && chart) {
            // ✅ 메인 차트의 width가 있으면 강제로 사용 (완벽한 동기화)
            const targetWidth = mainChartWidth || container.clientWidth;
            chart.applyOptions({
              width: targetWidth,
              height: panel.height || 120,
            });
          }
        });
        
        // ✅ 메인 차트도 명시적으로 리사이즈
        if (chartRef.current && mainChartWidth) {
          chartRef.current.applyOptions({
            width: mainChartWidth,
          });
        }
      };

      window.addEventListener('resize', handleResize);

      return () => {
        // ✅ cleanup: 이벤트 리스너와 타이머만 정리 (차트 제거는 useEffect 본문에서 처리)
        window.removeEventListener('resize', handleResize);
        
        // ✅ 메인 차트 스크롤/줌 이벤트 리스너 제거
        // ⚠️ 차트가 이미 dispose되었을 수 있으므로 안전하게 처리
        if (chartRef.current && !isChartDisposingRef.current) {
          try {
            // timeScale() 메서드 호출 전에 차트가 유효한지 확인
            const timeScale = chartRef.current.timeScale();
            if (timeScale && typeof timeScale.unsubscribeVisibleLogicalRangeChange === 'function') {
              timeScale.unsubscribeVisibleLogicalRangeChange(mainChartLogicalRangeChangeHandler);
              console.log(`✅ [서브차트 cleanup] 메인 차트 이벤트 리스너 제거`);
            }
          } catch (e) {
            // Object is disposed 에러 무시 (이미 차트가 제거됨)
            const errorMessage = e instanceof Error ? e.message : String(e);
            if (!errorMessage.includes('disposed')) {
              console.warn(`메인 차트 이벤트 리스너 제거 실패:`, e);
            }
          }
        }
        
        // requestAnimationFrame 정리
        if (syncAnimationFrameId) {
          cancelAnimationFrame(syncAnimationFrameId);
        }
        
        // ✅ 동기화 setTimeout 모두 취소
        syncTimeoutsRef.current.forEach(timeoutId => {
          clearTimeout(timeoutId);
        });
        syncTimeoutsRef.current = [];
        console.log(`✅ [서브차트 cleanup] 동기화 타이머 취소`);
      };
    // ✅ indicatorData / allIndicatorData 객체는 deps에서 제외: WebSocket으로 마지막 봉만 갱신될 때마다
    //    배열 참조가 바뀌어 이 effect가 실행되면 서브차트 전체 remove→재생성 → 실시간 updateLastIndicators가 무력화됨.
    // → 길이·시리즈 구조·테마·설정 변경 시에만 재생성. 마지막 봉 OHLC/지표값은 updateLastIndicators()로만 반영.
    // data?.[0]?.time: 종목/기간 로드 시 첫 봉이 바뀌면 재생성
    }, [subPanels, data?.length, data?.[0]?.time, indicatorData?.length, allIndicatorData?.length, themeMode, settingsVersion, colorVersion, forceUpdateCounter, effectiveIndicatorSettingsKey, showSubPanelTimeScale, ichimokuSettingsKey]);

    // ✅ hideTimeScale 변경 시 timeScale 가시성 업데이트
    useEffect(() => {
      if (chartRef.current) {
        chartRef.current.applyOptions({
          timeScale: {
            visible: !hideTimeScale,
          },
        });
      }
    }, [hideTimeScale]);

    // ✅ 마커 재설정 useEffect (줌인/줌아웃 시에도 마커 유지)
    useEffect(() => {
      const candleSeries = candleSeriesRef.current;
      const markers = markersRef.current;

      if (!candleSeries || !markers || markers.length === 0) {
        console.log('⏸️ [useEffect] 마커 재설정 스킵 - series:', !!candleSeries, 'markers:', markers?.length || 0);
        return;
      }

      // ✅ 마커 재설정 (ref에 저장된 마커 사용)
      console.log('🔄 [useEffect] 데이터 변경 후 마커 재설정:', markers.length, '개', {
        buyCount: markers.filter((m: any) => m.position === 'belowBar').length,
        sellCount: markers.filter((m: any) => m.position === 'aboveBar').length
      });

      const markersCopy = JSON.parse(JSON.stringify(markers));
      candleSeries.setMarkers(markersCopy as any);

      // ✅ 추가 안전장치: 100ms, 200ms, 500ms 후에도 재설정
      const timers = [
        setTimeout(() => {
          if (candleSeriesRef.current && markersRef.current && markersRef.current.length > 0) {
            const markers2 = JSON.parse(JSON.stringify(markersRef.current));
            candleSeriesRef.current.setMarkers(markers2 as any);
            console.log('✅ [useEffect+100ms] 마커 재설정 완료:', markersRef.current.length, '개');
          }
        }, 100),
        setTimeout(() => {
          if (candleSeriesRef.current && markersRef.current && markersRef.current.length > 0) {
            const markers3 = JSON.parse(JSON.stringify(markersRef.current));
            candleSeriesRef.current.setMarkers(markers3 as any);
            console.log('✅ [useEffect+200ms] 마커 재설정 완료:', markersRef.current.length, '개');
          }
        }, 200),
        setTimeout(() => {
          if (candleSeriesRef.current && markersRef.current && markersRef.current.length > 0) {
            const markers4 = JSON.parse(JSON.stringify(markersRef.current));
            candleSeriesRef.current.setMarkers(markers4 as any);
            console.log('✅ [useEffect+500ms] 마커 재설정 완료:', markersRef.current.length, '개');
          }
        }, 500),
      ];

      return () => {
        timers.forEach(timer => clearTimeout(timer));
      };
    }, [data.length]); // data.length가 변경될 때만 재설정 (참조 변경 무시)

    // ★ hideNativeCrosshair 동적 업데이트
    useEffect(() => {
      const chart = chartRef.current;
      if (!chart) {
        return;
      }

      try {
        chart.applyOptions({
          crosshair: {
            mode: isMobileMode ? 1 : 0, // 모바일: Magnet 모드, PC: Normal 모드
            vertLine: {
              visible: isMobileMode ? !hideNativeCrosshair : false, // PC는 업비트 스타일 (전역 수직선)
              width: 1,
              color: chartColors.crosshairColor,
              style: isMobileMode ? 3 : 0, // 모바일: 점선, PC: 실선
              labelBackgroundColor: chartColors.crosshairLabelBg,
              labelVisible: !hideNativeCrosshair, // ✅ PC/모바일 모두 세로선 날짜 레이블 표시
            },
            horzLine: {
              visible: !hideNativeCrosshair,
              width: 1,
              color: chartColors.crosshairColor,
              style: isMobileMode ? 3 : 0, // 모바일: 점선, PC: 실선
              labelBackgroundColor: chartColors.crosshairLabelBg,
              labelVisible: !hideNativeCrosshair,
            },
          },
        });
      } catch (error) {
        console.error('[CandlestickChart] crosshair mode 적용 실패:', error);
      }
    }, [hideNativeCrosshair, chartColors, chartReady]);

    // ✅ 돋보기 모드: 마우스 휠 확대/축소 기능
    useEffect(() => {
      const chart = chartRef.current;
      const container = chartContainerRef.current;
      
      if (!chart || !container || !magnifierActive) {
        return;
      }

      const handleWheel = (e: WheelEvent) => {
        // 돋보기 모드일 때만 확대/축소
        if (!magnifierActive) return;

        e.preventDefault();
        e.stopPropagation();

        const timeScale = chart.timeScale();
        const visibleRange = timeScale.getVisibleLogicalRange();
        
        if (!visibleRange) return;

        // 휠 방향: deltaY < 0 = 위로 (확대), deltaY > 0 = 아래로 (축소)
        const delta = e.deltaY < 0 ? -0.1 : 0.1; // 10% 단위로 확대/축소
        const zoomFactor = 1 + delta;

        // 현재 표시 중인 logical range 크기
        const currentRange = visibleRange.to - visibleRange.from;
        const newRange = currentRange * zoomFactor;

        // 확대/축소 제한 (최소 10개, 최대 전체 데이터)
        const minRange = 10;
        const maxRange = 500;

        if (newRange < minRange || newRange > maxRange) {
          console.log('🔍 [돋보기] 확대/축소 제한:', { newRange, minRange, maxRange });
          return;
        }

        // 마우스 위치를 기준으로 확대/축소
        const chartRect = container.getBoundingClientRect();
        const mouseX = e.clientX - chartRect.left;
        const chartWidth = chartRect.width;
        
        // 마우스 위치 비율 (0 = 왼쪽, 1 = 오른쪽)
        const mouseRatio = mouseX / chartWidth;

        // 새로운 범위 계산 (마우스 위치 유지)
        const leftExpansion = (newRange - currentRange) * mouseRatio;
        const rightExpansion = (newRange - currentRange) * (1 - mouseRatio);

        const newFrom = visibleRange.from - leftExpansion;
        const newTo = visibleRange.to + rightExpansion;

        console.log('🔍 [돋보기] 확대/축소:', {
          direction: delta < 0 ? '확대' : '축소',
          currentRange,
          newRange,
          from: newFrom,
          to: newTo,
          mouseRatio: mouseRatio.toFixed(2),
        });

        // 새로운 logical range 적용
        try {
          // 🔒 다른 range 조정 로직 차단
          isMagnifierZoomingRef.current = true;

          timeScale.setVisibleLogicalRange({
            from: newFrom,
            to: newTo,
          });

          // 줌 레벨 업데이트 (UI 표시용)
          const zoomLevel = maxRange / newRange;
          setMagnifierZoomLevel(zoomLevel);

          // 500ms 후 플래그 해제 (줌이 안정화될 때까지)
          setTimeout(() => {
            isMagnifierZoomingRef.current = false;
          }, 500);
        } catch (error) {
          console.error('🔍 [돋보기] setVisibleLogicalRange 오류:', error);
          isMagnifierZoomingRef.current = false;
        }
      };

      // passive: false로 설정해야 preventDefault가 동작
      container.addEventListener('wheel', handleWheel, { passive: false });

      console.log('🔍 [돋보기] 마우스 휠 이벤트 등록');

      return () => {
        container.removeEventListener('wheel', handleWheel);
        console.log('🔍 [돋보기] 마우스 휠 이벤트 해제');
      };
    }, [magnifierActive, chartReady]);

    // ✅ 돋보기 모드: ESC 키로 전체 차트 리셋
    useEffect(() => {
      if (!magnifierActive) {
        // 돋보기 모드가 꺼질 때 플래그 초기화 및 돋보기 차트 제거
        isMagnifierZoomingRef.current = false;
        magnifierJustClickedRef.current = false;
        magnifierShowChartRef.current = false;
        magnifierClickPosRef.current = null;
        setMagnifierShowChart(false);
        setMagnifierCenterTime(null);
        setMagnifierTradeMarkers([]); // 마커 오버레이 초기화
        if (magnifierChartRef.current) {
          magnifierChartRef.current.chart?.remove();
          magnifierChartRef.current = null;
        }
        return;
      }

      const handleKeyDown = (e: KeyboardEvent) => {
        if (e.key === 'Escape') {
          const chart = chartRef.current;
          if (chart) {
            isMagnifierZoomingRef.current = false; // 플래그 해제
            chart.timeScale().fitContent();
            setMagnifierZoomLevel(1);
            console.log('🔍 [돋보기] ESC - 전체 차트로 리셋');
          }
          
          // 돋보기 차트 표시 끄기 (안내 모드로 복귀)
          if (magnifierShowChartRef.current) {
            setMagnifierShowChart(false);
            magnifierShowChartRef.current = false;
            magnifierClickPosRef.current = null; // 클릭 위치 초기화
            if (magnifierChartRef.current) {
              magnifierChartRef.current.chart?.remove();
              magnifierChartRef.current = null;
            }
            console.log('🔍 [돋보기] ESC - 안내 모드로 복귀');
          }
        }
      };

      window.addEventListener('keydown', handleKeyDown);

      return () => {
        window.removeEventListener('keydown', handleKeyDown);
        isMagnifierZoomingRef.current = false; // cleanup 시 플래그 해제
      };
    }, [magnifierActive]);

    // ✅ 돋보기용 별도 차트 생성 및 업데이트
    useEffect(() => {
      console.log('🔍 [돋보기] useEffect 실행:', {
        magnifierActive,
        magnifierShowChart,
        hasContainer: !!magnifierContainerRef.current,
        magnifierCenterTime,
        dataLength: data?.length || 0,
      });

      // magnifierShowChart가 false면 차트 제거
      if (!magnifierShowChart && magnifierChartRef.current) {
        console.log('🔍 [돋보기] 차트 숨김 - 차트 제거');
        try {
          magnifierChartRef.current.chart?.remove();
        } catch (e) {
          console.warn('🔍 [돋보기] 차트 제거 실패:', e);
        }
        magnifierChartRef.current = null;
        magnifierClickPosRef.current = null; // 클릭 위치 초기화
        setMagnifierTradeMarkers([]); // 마커 오버레이 초기화
        return;
      }

      if (!magnifierActive || !magnifierShowChart || !magnifierCenterTime || !data || data.length === 0) {
        console.log('🔍 [돋보기] useEffect 조건 미충족 - 종료');
        return;
      }

      // DOM 업데이트를 위한 약간의 딜레이
      const timer = setTimeout(() => {
        const container = magnifierContainerRef.current;
        
        if (!container) {
          console.error('🔍 [돋보기] 컨테이너가 없습니다!');
          return;
        }

        console.log('🔍 [돋보기] 차트 생성 시작, container:', container);

        // 캔들 데이터 변환 (data prop -> candle format)
        const candleDataArray = data.map(item => ({
          time: Math.floor(new Date(item.time).getTime() / 1000),
          open: item.open,
          high: item.high,
          low: item.low,
          close: item.close,
        })).sort((a, b) => a.time - b.time);

        console.log('🔍 [돋보기] 캔들 데이터 변환 완료:', candleDataArray.length);

        // 기존 차트 제거 (재생성)
        if (magnifierChartRef.current) {
          console.log('🔍 [돋보기] 기존 차트 제거');
          try {
            magnifierChartRef.current.chart?.remove();
          } catch (e) {
            console.warn('🔍 [돋보기] 기존 차트 제거 실패:', e);
          }
          magnifierChartRef.current = null;
        }

        // 돋보기 차트 생성
        console.log('🔍 [돋보기] 새 차트 생성 시작');
        const magnifierChart = createChart(container, {
          width: 400,
          height: 400,
          layout: {
            background: { color: themeMode === 'dark' ? '#1e1e1e' : '#ffffff' },
            textColor: themeMode === 'dark' ? '#d1d4dc' : '#191919',
          },
          grid: {
            vertLines: { color: themeMode === 'dark' ? '#2B2B43' : '#e6e6e6' },
            horzLines: { color: themeMode === 'dark' ? '#2B2B43' : '#e6e6e6' },
          },
          crosshair: {
            mode: 0, // ✅ Normal 모드: 마우스 위치를 정확히 따라감
            vertLine: {
              visible: true,
              width: 1,
              color: chartColors.crosshairColor,
              style: 0,
              labelBackgroundColor: chartColors.crosshairLabelBg,
            },
            horzLine: {
              visible: true,
              width: 1,
              color: chartColors.crosshairColor,
              style: 0,
              labelBackgroundColor: chartColors.crosshairLabelBg,
            },
          },
          timeScale: {
            timeVisible: true,
            secondsVisible: false,
            borderColor: themeMode === 'dark' ? '#2B2B43' : '#d1d4dc',
          },
          rightPriceScale: {
            borderColor: themeMode === 'dark' ? '#2B2B43' : '#d1d4dc',
            scaleMargins: {
              top: 0.1,
              bottom: 0.15,  // ✅ 0.1 → 0.15로 증가 (X축 라벨 영역과 겹치지 않도록)
            },
          },
        });

        // 캔들스틱 시리즈 추가 (basic 차트 스타일)
        const magnifierCandleSeries = magnifierChart.addCandlestickSeries({
          upColor: '#EF5350',      // 밝은 빨간색 (양봉)
          downColor: '#42A5F5',    // 밝은 파란색 (음봉)
          wickUpColor: '#EF5350',
          wickDownColor: '#42A5F5',
          borderVisible: false,
        });

        magnifierChartRef.current = {
          chart: magnifierChart,
          series: magnifierCandleSeries,
        };

        console.log('🔍 [돋보기] 기본 차트 구조 생성 완료');

        // 마우스 위치 기준으로 앞뒤 5개 캔들만 추출
        const centerIndex = candleDataArray.findIndex(d => Math.abs(d.time - magnifierCenterTime) < 60);
        
        if (centerIndex === -1) {
          console.warn('🔍 [돋보기] 중심 캔들을 찾을 수 없습니다');
          return;
        }

        const visibleCandleCount = 10; // 앞뒤 5개씩
        const startIndex = Math.max(0, centerIndex - Math.floor(visibleCandleCount / 2));
        const endIndex = Math.min(candleDataArray.length, startIndex + visibleCandleCount);
        const visibleCandles = candleDataArray.slice(startIndex, endIndex);

        console.log('🔍 [돋보기] 캔들 범위 계산:', {
          centerTime: new Date(magnifierCenterTime * 1000).toISOString(),
          centerIndex,
          visibleRange: [startIndex, endIndex],
          visibleCount: visibleCandles.length,
        });

        // 캔들 데이터 설정
        magnifierCandleSeries.setData(visibleCandles.map(c => ({
          time: c.time as Time,
          open: c.open,
          high: c.high,
          low: c.low,
          close: c.close,
        })));
        console.log('🔍 [돋보기] 캔들 데이터 설정 완료:', visibleCandles.length, '개');

        // 📊 이동평균선 추가 (maLines가 있으면) - 확대 범위에 맞는 데이터만
        if (maLines && maLines.length > 0) {
          console.log('🔍 [돋보기] 이동평균선 추가 시작');
          
          const visibleTimeStart = visibleCandles[0].time;
          const visibleTimeEnd = visibleCandles[visibleCandles.length - 1].time;
          
          // ✅ 돋보기 MA 계산용 데이터 준비
          const sortedDataForMA = data
            .filter((d: any) => d.close !== undefined && d.close !== null)
            .map((d: any) => {
              const timeValue = d.timestamp || d.time || d.date;
              return {
                time: Math.floor(typeof timeValue === 'string' ? new Date(timeValue).getTime() / 1000 : timeValue),
                close: d.close,
              };
            });
          
          maLines.forEach(ma => {
            if (ma.enabled) {
              // ✅ 프론트엔드에서 직접 SMA 계산 (전체 데이터로 계산 후 확대 범위만 필터링)
              const maDataFull = calculateSMA(sortedDataForMA, ma.period);
              const maDataFiltered = maDataFull.filter((d): d is { time: number; value: number } => 
                d.value !== null &&  // ✅ null 값 제거 (초기 기간)
                d.time >= visibleTimeStart && 
                d.time <= visibleTimeEnd
              );
              
              // ✅ 동일 time 중복 제거 (lightweight-charts 요구사항)
              const byTime = new Map<number, { time: number; value: number }>();
              maDataFiltered.forEach(d => byTime.set(d.time, d));
              const maData = Array.from(byTime.values()).sort((a, b) => a.time - b.time);
              
              const dupCount = maDataFiltered.length - maData.length;
              if (dupCount > 0) {
                console.log(`🔍 [돋보기] MA${ma.period} 중복 제거: ${dupCount}개`);
              }

              if (maData.length > 0) {
                const maSeries = magnifierChart.addLineSeries({
                  color: ma.color,
                  lineWidth: ma.width as any,
                  lastValueVisible: false,
                  priceLineVisible: false,
                  priceScaleId: 'right',
                });
                maSeries.setData(maData as any);
                console.log(`🔍 [돋보기] MA${ma.period} 추가 완료 (${maData.length}개)`);
              }
            }
          });
        }

        // 📊 볼린저 밴드 추가 (settings가 있으면) - 확대 범위에 맞는 데이터만
        if (settings && chartColors) {
          console.log('🔍 [돋보기] 볼린저밴드 추가 시작');
          
          const { bollingerUpper, bollingerMiddle, bollingerLower } = settings;
          const visibleTimeStart = visibleCandles[0].time;
          const visibleTimeEnd = visibleCandles[visibleCandles.length - 1].time;

          // 상한선
          if (bollingerUpper) {
            const upperData = data
              .filter((item: any) => {
                const timeValue = typeof item.time === 'string'
                  ? new Date(item.time).getTime() / 1000
                  : item.time;
                return timeValue >= visibleTimeStart && timeValue <= visibleTimeEnd &&
                       item.bollingerUpper != null && item.bollingerUpper > 0;
              })
              .map((item: any) => ({
                time: typeof item.time === 'string' ? new Date(item.time).getTime() / 1000 : item.time,
                value: item.bollingerUpper,
              }));

            if (upperData.length > 0) {
              const upperSeries = magnifierChart.addLineSeries({
                color: chartColors.bollingerUpperColor || '#FF9800',
                lineWidth: (chartColors.bollingerUpperWidth || 1.5) as any,
                lineStyle: chartColors.bollingerUpperLineStyle === 'dashed' ? 2 : chartColors.bollingerUpperLineStyle === 'dotted' ? 3 : 0,
                lastValueVisible: false,
                priceLineVisible: false,
              });
              upperSeries.setData(upperData as any);
              console.log(`🔍 [돋보기] 볼린저 상한선 추가 완료 (${upperData.length}개)`);
            }
          }

          // 중간선
          if (bollingerMiddle) {
            const middleData = data
              .filter((item: any) => {
                const timeValue = typeof item.time === 'string'
                  ? new Date(item.time).getTime() / 1000
                  : item.time;
                return timeValue >= visibleTimeStart && timeValue <= visibleTimeEnd &&
                       item.bollingerMiddle != null && item.bollingerMiddle > 0;
              })
              .map((item: any) => ({
                time: typeof item.time === 'string' ? new Date(item.time).getTime() / 1000 : item.time,
                value: item.bollingerMiddle,
              }));

            if (middleData.length > 0) {
              const middleSeries = magnifierChart.addLineSeries({
                color: chartColors.bollingerMiddleColor || '#2196F3',
                lineWidth: (chartColors.bollingerMiddleWidth || 1.5) as any,
                lineStyle: chartColors.bollingerMiddleLineStyle === 'dashed' ? 2 : chartColors.bollingerMiddleLineStyle === 'dotted' ? 3 : 0,
                lastValueVisible: false,
                priceLineVisible: false,
              });
              middleSeries.setData(middleData as any);
              console.log(`🔍 [돋보기] 볼린저 중간선 추가 완료 (${middleData.length}개)`);
            }
          }

          // 하한선
          if (bollingerLower) {
            const lowerData = data
              .filter((item: any) => {
                const timeValue = typeof item.time === 'string'
                  ? new Date(item.time).getTime() / 1000
                  : item.time;
                return timeValue >= visibleTimeStart && timeValue <= visibleTimeEnd &&
                       item.bollingerLower != null && item.bollingerLower > 0;
              })
              .map((item: any) => ({
                time: typeof item.time === 'string' ? new Date(item.time).getTime() / 1000 : item.time,
                value: item.bollingerLower,
              }));

            if (lowerData.length > 0) {
              const lowerSeries = magnifierChart.addLineSeries({
                color: chartColors.bollingerLowerColor || '#4CAF50',
                lineWidth: (chartColors.bollingerLowerWidth || 1.5) as any,
                lineStyle: chartColors.bollingerLowerLineStyle === 'dashed' ? 2 : chartColors.bollingerLowerLineStyle === 'dotted' ? 3 : 0,
                lastValueVisible: false,
                priceLineVisible: false,
              });
              lowerSeries.setData(lowerData as any);
              console.log(`🔍 [돋보기] 볼린저 하한선 추가 완료 (${lowerData.length}개)`);
            }
          }
        }

        // 📊 일목균형표 추가 (ichimokuSettings가 있으면) - 확대 범위에 맞는 데이터만
        if (ichimokuSettings && ichimokuColors) {
          console.log('🔍 [돋보기] 일목균형표 추가 시작');
          
          const visibleTimeStart = visibleCandles[0].time;
          const visibleTimeEnd = visibleCandles[visibleCandles.length - 1].time;
          
          const ichimokuData: any[] = [];
          data.forEach((item: any) => {
            const timeValue = typeof item.time === 'string'
              ? new Date(item.time).getTime() / 1000
              : item.time;
            
            // 확대 범위에 포함되는 데이터만
            if (timeValue >= visibleTimeStart && timeValue <= visibleTimeEnd) {
              ichimokuData.push({
                time: timeValue,
                // ✅ 올바른 필드명 사용 (ichimokuTenkan 등)
                tenkan: item.ichimokuTenkan || item.tenkanSen,
                kijun: item.ichimokuKijun || item.kijunSen,
                senkouA: item.ichimokuSenkouA || item.senkouSpanA,
                senkouB: item.ichimokuSenkouB || item.senkouSpanB,
                chikou: item.ichimokuChikou || item.chikouSpan,
              });
            }
          });

          // 전환선
          if (ichimokuSettings.tenkan) {
            const tenkanData = ichimokuData
              .filter(d => d.tenkan !== null && d.tenkan !== undefined)
              .map(d => ({ time: d.time, value: d.tenkan }));
            if (tenkanData.length > 0) {
              const tenkanSeries = magnifierChart.addLineSeries({
                color: ichimokuColors.tenkan,
                lineWidth: (ichimokuColors.tenkanWidth || 1) as any,  // ✅ 사용자 설정 굵기
                lastValueVisible: false,
                priceLineVisible: false,
              });
              tenkanSeries.setData(tenkanData as any);
              console.log(`🔍 [돋보기] 전환선 추가 완료 (${tenkanData.length}개, 굵기: ${ichimokuColors.tenkanWidth || 1})`);
            }
          }

          // 기준선
          if (ichimokuSettings.kijun) {
            const kijunData = ichimokuData
              .filter(d => d.kijun !== null && d.kijun !== undefined)
              .map(d => ({ time: d.time, value: d.kijun }));
            if (kijunData.length > 0) {
              const kijunSeries = magnifierChart.addLineSeries({
                color: ichimokuColors.kijun,
                lineWidth: (ichimokuColors.kijunWidth || 1) as any,  // ✅ 사용자 설정 굵기
                lastValueVisible: false,
                priceLineVisible: false,
              });
              kijunSeries.setData(kijunData as any);
              console.log(`🔍 [돋보기] 기준선 추가 완료 (${kijunData.length}개, 굵기: ${ichimokuColors.kijunWidth || 1})`);
            }
          }

          // 선행스팬 A
          if (ichimokuSettings.senkouA) {
            const senkouAData = ichimokuData
              .filter(d => d.senkouA !== null && d.senkouA !== undefined)
              .map(d => ({ time: d.time, value: d.senkouA }));
            if (senkouAData.length > 0) {
              const senkouASeries = magnifierChart.addLineSeries({
                color: ichimokuColors.senkouA,
                lineWidth: (ichimokuColors.senkouAWidth || 1) as any,  // ✅ 사용자 설정 굵기
                lastValueVisible: false,
                priceLineVisible: false,
              });
              senkouASeries.setData(senkouAData as any);
              console.log(`🔍 [돋보기] 선행스팬A 추가 완료 (${senkouAData.length}개)`);
            }
          }

          // 선행스팬 B
          if (ichimokuSettings.senkouB) {
            const senkouBData = ichimokuData
              .filter(d => d.senkouB !== null && d.senkouB !== undefined)
              .map(d => ({ time: d.time, value: d.senkouB }));
            if (senkouBData.length > 0) {
              const senkouBSeries = magnifierChart.addLineSeries({
                color: ichimokuColors.senkouB,
                lineWidth: (ichimokuColors.senkouBWidth || 1) as any,  // ✅ 사용자 설정 굵기
                lastValueVisible: false,
                priceLineVisible: false,
              });
              senkouBSeries.setData(senkouBData as any);
              console.log(`🔍 [돋보기] 선행스팬B 추가 완료 (${senkouBData.length}개)`);
            }
          }

          // 후행스팬
          if (ichimokuSettings.chikou) {
            const chikouData = ichimokuData
              .filter(d => d.chikou !== null && d.chikou !== undefined)
              .map(d => ({ time: d.time, value: d.chikou }));
            if (chikouData.length > 0) {
              const chikouSeries = magnifierChart.addLineSeries({
                color: ichimokuColors.chikou,
                lineWidth: (ichimokuColors.chikouWidth || 1) as any,  // ✅ 사용자 설정 굵기
                lastValueVisible: false,
                priceLineVisible: false,
              });
              chikouSeries.setData(chikouData as any);
              console.log(`🔍 [돋보기] 후행스팬 추가 완료 (${chikouData.length}개)`);
            }
          }
          
          // ✅ 일목균형표 구름 영역 추가 (선행스팬A와 B 사이) - Canvas로 그리기
          if (ichimokuSettings.senkouA && ichimokuSettings.senkouB) {
            const cloudData = ichimokuData
              .filter(d => d.senkouA !== null && d.senkouA !== undefined && 
                          d.senkouB !== null && d.senkouB !== undefined)
              .map(d => ({ time: d.time, senkouA: d.senkouA, senkouB: d.senkouB }));
            
            console.log(`🔍 [돋보기] 일목 구름 데이터 준비 완료 (${cloudData.length}개)`);
            
            if (cloudData.length > 0 && magnifierContainerRef.current) {
              console.log(`🔍 [돋보기] 일목 구름 영역 그리기 시작 (${cloudData.length}개)`);
              
              const magnifierContainer = magnifierContainerRef.current;
              
              // Canvas 생성 (이미 있으면 재사용)
              let cloudCanvas = magnifierContainer.querySelector('canvas.ichimoku-cloud') as HTMLCanvasElement;
              if (!cloudCanvas) {
                cloudCanvas = document.createElement('canvas');
                cloudCanvas.className = 'ichimoku-cloud';
                cloudCanvas.style.position = 'absolute';
                cloudCanvas.style.top = '0';
                cloudCanvas.style.left = '0';
                cloudCanvas.style.width = '100%';
                cloudCanvas.style.height = '100%';
                cloudCanvas.style.pointerEvents = 'none';
                cloudCanvas.style.zIndex = '1'; // 캔들 위에 표시
                magnifierContainer.style.position = 'relative';
                
                // 첫 번째 자식으로 추가
                if (magnifierContainer.firstChild) {
                  magnifierContainer.insertBefore(cloudCanvas, magnifierContainer.firstChild);
                } else {
                  magnifierContainer.appendChild(cloudCanvas);
                }
                
              }
              
              // Canvas 크기 설정
              const containerRect = magnifierContainer.getBoundingClientRect();
              cloudCanvas.width = containerRect.width;
              cloudCanvas.height = containerRect.height;
              
              // 구름 그리기
              const ctx = cloudCanvas.getContext('2d');
              if (ctx) {
                ctx.clearRect(0, 0, cloudCanvas.width, cloudCanvas.height);
                
                const timeScale = magnifierChart.timeScale();
                
                // 실제 일목균형표 구름 색상 (적절한 투명도로 설정)
                const cloudBullishColor = ichimokuColors.cloudBullish || 'rgba(239, 83, 80, 0.15)';
                const cloudBearishColor = ichimokuColors.cloudBearish || 'rgba(38, 166, 154, 0.15)';
                
                console.log('🔍 [돋보기] 일목 구름 색상:', { 상승: cloudBullishColor, 하락: cloudBearishColor });
                
                let drawnCount = 0;
                let skippedCoords = 0;
                
                for (let i = 0; i < cloudData.length - 1; i++) {
                  const curr = cloudData[i];
                  const next = cloudData[i + 1];
                  
                  const x1 = timeScale.timeToCoordinate(curr.time as any);
                  const x2 = timeScale.timeToCoordinate(next.time as any);
                  
                  if (x1 === null || x2 === null) {
                    if (i < 3) console.log(`🔍 [돋보기] 구름 ${i}: x좌표 null (x1=${x1}, x2=${x2})`);
                    skippedCoords++;
                    continue;
                  }
                  
                  const y1A = magnifierCandleSeries.priceToCoordinate(curr.senkouA);
                  const y1B = magnifierCandleSeries.priceToCoordinate(curr.senkouB);
                  const y2A = magnifierCandleSeries.priceToCoordinate(next.senkouA);
                  const y2B = magnifierCandleSeries.priceToCoordinate(next.senkouB);
                  
                  if (y1A === null || y1B === null || y2A === null || y2B === null) {
                    if (i < 3) console.log(`🔍 [돋보기] 구름 ${i}: y좌표 null (y1A=${y1A}, y1B=${y1B}, y2A=${y2A}, y2B=${y2B})`);
                    skippedCoords++;
                    continue;
                  }
                  
                  // 상승/하락 구름 판단
                  const isBullish = curr.senkouA >= curr.senkouB;
                  ctx.fillStyle = isBullish ? cloudBullishColor : cloudBearishColor;
                  
                  // 사각형 그리기 (4개 꼭지점)
                  ctx.beginPath();
                  ctx.moveTo(x1, y1A);
                  ctx.lineTo(x2, y2A);
                  ctx.lineTo(x2, y2B);
                  ctx.lineTo(x1, y1B);
                  ctx.closePath();
                  ctx.fill();
                  
                  drawnCount++;
                }
                
                console.log(`🔍 [돋보기] 일목 구름 그리기 완료: ${drawnCount}개 폴리곤`);
              }
            }
          }
        }

        console.log('🔍 [돋보기] 모든 차트 요소 추가 완료 (캔들 + MA + Ichimoku + Cloud)');

        // 🎯 매수/매도 마커 설정 (확대 범위에 해당하는 마커만 필터링)
        let filteredMarkers: any[] = [];
        
        if (markersRef.current && markersRef.current.length > 0) {
          const visibleTimeStart = visibleCandles[0].time;
          const visibleTimeEnd = visibleCandles[visibleCandles.length - 1].time;
          
          filteredMarkers = markersRef.current.filter((marker: any) => {
            const markerTime = typeof marker.time === 'number' ? marker.time : new Date(marker.time).getTime() / 1000;
            return markerTime >= visibleTimeStart && markerTime <= visibleTimeEnd;
          });

          console.log('🔍 [돋보기] 마커 필터링:', {
            totalMarkers: markersRef.current.length,
            filteredMarkers: filteredMarkers.length,
            visibleTimeRange: [
              new Date(visibleTimeStart * 1000).toISOString(),
              new Date(visibleTimeEnd * 1000).toISOString()
            ]
          });

          if (filteredMarkers.length > 0) {
            magnifierCandleSeries.setMarkers(filteredMarkers as any);
            console.log('🔍 [돋보기] 마커 설정 완료:', filteredMarkers.length, '개');
          }
        }

        // 전체 범위 표시 (자동 스케일)
        magnifierChart.timeScale().fitContent();
        
        // 🎯 매수/매도 마커 오버레이 위치 계산 (사각형 배경용)
        setTimeout(() => {
          if (!magnifierChartRef.current || filteredMarkers.length === 0) {
            setMagnifierTradeMarkers([]);
            return;
          }

          const magnifierChart = magnifierChartRef.current.chart;
          const magnifierSeries = magnifierChartRef.current.series;
          const timeScale = magnifierChart.timeScale();
          
          const overlayMarkers: Array<{ x: number; y: number; type: 'BUY' | 'SELL'; time: string }> = [];

          filteredMarkers.forEach((marker: any) => {
            const markerTime = typeof marker.time === 'number' ? marker.time : new Date(marker.time).getTime() / 1000;
            const x = timeScale.timeToCoordinate(markerTime as any);
            
            if (x === null || x < 0) return;

            // 해당 시간의 캔들 데이터 찾기
            const candle = visibleCandles.find(c => Math.abs(c.time - markerTime) < 60);
            if (!candle) return;

            // 가격 좌표 계산 (매수는 low, 매도는 high)
            const price = marker.position === 'belowBar' ? candle.low : candle.high;
            const y = magnifierSeries.priceToCoordinate(price);
            
            if (y === null) return;

            overlayMarkers.push({
              x: x,
              y: marker.position === 'belowBar' ? y + 40 : y - 40, // 화살표와 겹치지 않도록 거리 조정
              type: marker.position === 'belowBar' ? 'BUY' : 'SELL',
              time: new Date(markerTime * 1000).toISOString(),
            });
          });

          console.log('🔍 [돋보기] 마커 오버레이 위치 계산 완료:', overlayMarkers.length, '개');
          setMagnifierTradeMarkers(overlayMarkers);
        }, 100); // 차트 렌더링 후 위치 계산
        
        console.log('🔍 [돋보기] 차트 업데이트 완료');
      }, 50); // 50ms 딜레이

      return () => {
        clearTimeout(timer);
      };
    }, [magnifierActive, magnifierShowChart, magnifierCenterTime, data, chartColors, themeMode, maLines, ichimokuSettings, ichimokuColors]);

    const formatNumber = (value: number | null): string => {
      if (value === null) return '-';
      if (value >= 1000000) {
        return value.toLocaleString('ko-KR', { maximumFractionDigits: 0 });
      } else if (value >= 1) {
        return value.toLocaleString('ko-KR', { maximumFractionDigits: 2 });
      } else {
        return value.toFixed(8);
      }
    };

    return (
      <Box 
        ref={chartMainContainerRef} 
        sx={{ position: 'relative', width: '100%', p: 0 }}
        onMouseEnter={() => {
          // ✅ 차트 영역에 마우스 진입 시 (툴바는 항상 표시)
          // setShowNavigationToolbar(true); // 항상 표시되므로 불필요
        }}
        onMouseLeave={() => {
          // ✅ 마우스가 차트 영역을 벗어나면 크로스헤어 숨김 (툴바는 유지)
          setGlobalCrosshairX(null);
          setActiveCrosshairChart(null);
          // setShowNavigationToolbar(false); // 항상 표시되도록 주석 처리
        }}
      >
        {/* ✅ 업비트 스타일 전역 크로스헤어 수직선 (모든 차트 관통) */}
        {globalCrosshairX !== null && (
          <Box
            sx={{
              position: 'absolute',
              left: globalCrosshairX,
              top: 0,
              bottom: 0,
              width: '1px',
              bgcolor: chartColors.crosshairColor || '#758696',
              pointerEvents: 'none',
              zIndex: 1000,
            }}
          />
        )}

        {/* 보조지표 토글 버튼 - shouldShowToggle이 true일 때만 표시 */}
        {shouldShowToggle && (
          <Box sx={{
            display: 'flex',
            flexWrap: 'wrap',
            gap: 0.5,
            mb: 1,
            alignItems: 'center'
          }}>
            <Typography variant="caption" sx={{ mr: 1, color: 'text.secondary', fontWeight: 'bold' }}>
              보조지표:
            </Typography>
            <Chip
              label="MACD"
              size="small"
              variant={settings.macdLine ? "filled" : "outlined"}
              color={settings.macdLine ? "primary" : "default"}
              onClick={() => updateSettings({ macdLine: !settings.macdLine })}
              sx={{ height: '24px', fontSize: '11px', cursor: 'pointer', '&:hover': { opacity: 0.8 } }}
            />
            <Chip
              label="Stoch"
              size="small"
              variant={settings.stochasticSlow ? "filled" : "outlined"}
              color={settings.stochasticSlow ? "primary" : "default"}
              onClick={() => updateSettings({ stochasticSlow: !settings.stochasticSlow })}
              sx={{ height: '24px', fontSize: '11px', cursor: 'pointer', '&:hover': { opacity: 0.8 } }}
            />
            <Chip
              label="ADX"
              size="small"
              variant={settings.adx ? "filled" : "outlined"}
              color={settings.adx ? "primary" : "default"}
              onClick={() => updateSettings({ adx: !settings.adx })}
              sx={{ height: '24px', fontSize: '11px', cursor: 'pointer', '&:hover': { opacity: 0.8 } }}
            />
            <Chip
              label="RSI"
              size="small"
              variant={settings.rsi ? "filled" : "outlined"}
              color={settings.rsi ? "primary" : "default"}
              onClick={() => updateSettings({ rsi: !settings.rsi })}
              sx={{ height: '24px', fontSize: '11px', cursor: 'pointer', '&:hover': { opacity: 0.8 } }}
            />
            <Chip
              label="CCI"
              size="small"
              variant={settings.cci ? "filled" : "outlined"}
              color={settings.cci ? "primary" : "default"}
              onClick={() => updateSettings({ cci: !settings.cci })}
              sx={{ height: '24px', fontSize: '11px', cursor: 'pointer', '&:hover': { opacity: 0.8 } }}
            />
          </Box>
        )}

        <Box 
          sx={{ 
            position: 'relative', 
            width: '100%', 
            height: `${height}px`, 
            minHeight: Math.max(80, height),
            flexShrink: 0,
            p: 0, 
            m: 0,
            // ✅ 일목 구름 표시 시: 차트 배경이 transparent이므로 부모에 배경색 지정
            ...(ichimokuSettings?.senkouA && ichimokuSettings?.senkouB && { bgcolor: chartColors.background })
          }}
        >
          {/* ✅ 캔들차트 좌측 상단: 추세 범례(정배열/역배열/혼조) + 이동평균선/일목균형표/볼린저밴드 범례 (hideLegends 시 숨김) */}
          {!hideLegends && ((!isMobileMode && chartReady && chartRef.current && indicatorData && indicatorData.length > 0) || (data.length > 0 && maLines && maLines.filter(line => line.enabled).length > 0) || (data.length > 0 && ichimokuSettings && (ichimokuSettings.tenkan || ichimokuSettings.kijun || ichimokuSettings.chikou || ichimokuSettings.senkouA || ichimokuSettings.senkouB)) || (data.length > 0 && (overlaySettingsOverride ? (overlaySettingsOverride.bollingerUpper || overlaySettingsOverride.bollingerMiddle || overlaySettingsOverride.bollingerLower) : (settings?.bollingerUpper || settings?.bollingerMiddle || settings?.bollingerLower)))) ? (
            <Box
              sx={{
                position: 'absolute',
                top: 8,
                left: 8,
                zIndex: 10,
                display: 'flex',
                alignItems: 'center',
                gap: 1,
                flexWrap: 'wrap',
                pointerEvents: 'none',
              }}
            >
              {/* 추세 범례 (정배열/역배열/혼조) */}
              {!isMobileMode && chartReady && chartRef.current && indicatorData && indicatorData.length > 0 && (
                <Box
                  sx={{
                    display: 'flex',
                    gap: 2,
                    fontSize: '11px',
                    fontWeight: 500,
                    color: 'text.primary',
                    bgcolor: chartColors.tooltipBg,
                    borderRadius: 1,
                    px: 1.5,
                    py: 0.5,
                    border: `1px solid ${chartColors.tooltipBorder}`,
                  }}
                >
                  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
                    <div style={{
                      width: 18,
                      height: 10,
                      backgroundColor: 'rgba(76, 175, 80, 0.75)',
                      border: '1.5px solid rgba(0,0,0,0.3)',
                      borderRadius: '2px'
                    }} />
                    <span>정배열</span>
                  </Box>
                  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
                    <div style={{
                      width: 18,
                      height: 10,
                      backgroundColor: 'rgba(244, 67, 54, 0.75)',
                      border: '1.5px solid rgba(0,0,0,0.3)',
                      borderRadius: '2px'
                    }} />
                    <span>역배열</span>
                  </Box>
                  <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
                    <div style={{
                      width: 18,
                      height: 10,
                      backgroundColor: 'rgba(158, 158, 158, 0.5)',
                      border: '1.5px solid rgba(0,0,0,0.3)',
                      borderRadius: '2px'
                    }} />
                    <span>혼조</span>
                  </Box>
                </Box>
              )}
              {/* 이동평균선 범례 - 내용에 맞게 너비 조절 */}
              {data.length > 0 && maLines && maLines.filter(line => line.enabled).length > 0 && (
                <Box
                  sx={{
                    display: 'flex',
                    gap: 2,
                    flexWrap: 'wrap',
                    width: 'fit-content',
                    maxWidth: 'calc(100% - 20px)',
                    bgcolor: chartColors.tooltipBg,
                    borderRadius: 1,
                    px: 1.5,
                    py: 0.5,
                    border: `1px solid ${chartColors.tooltipBorder}`,
                  }}
                >
                  {maLines.filter(line => line.enabled).map((line) => {
                    let maValue = null;
                    if (line.period === 12) maValue = maValues.ma12;
                    else if (line.period === 26) maValue = maValues.ma26;
                    else if (line.period === 60) maValue = maValues.ma60;

                    return (
                      <Box key={line.id} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                        <Box sx={{ width: 12, height: 2, bgcolor: line.color }} />
                        <Typography variant="caption" sx={{ color: line.color, fontWeight: 'bold' }}>
                          {line.type}{line.period}
                        </Typography>
                        <Typography variant="caption" sx={{ color: 'text.secondary' }}>
                          {formatNumber(maValue)}
                        </Typography>
                      </Box>
                    );
                  })}
                </Box>
              )}
              {/* 일목균형표 범례 */}
              {data.length > 0 && ichimokuSettings && ichimokuColors && (ichimokuSettings.tenkan || ichimokuSettings.kijun || ichimokuSettings.chikou || ichimokuSettings.senkouA || ichimokuSettings.senkouB) && (
                <Box
                  sx={{
                    display: 'flex',
                    gap: 2,
                    flexWrap: 'wrap',
                    bgcolor: chartColors.tooltipBg,
                    borderRadius: 1,
                    px: 1.5,
                    py: 0.5,
                    border: `1px solid ${chartColors.tooltipBorder}`,
                  }}
                >
                  {ichimokuSettings.tenkan && (
                    <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                      <Box sx={{ width: 12, height: 2, bgcolor: ichimokuColors.tenkan }} />
                      <Typography variant="caption" sx={{ color: ichimokuColors.tenkan, fontWeight: 'bold' }}>전환</Typography>
                    </Box>
                  )}
                  {ichimokuSettings.kijun && (
                    <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                      <Box sx={{ width: 12, height: 2, bgcolor: ichimokuColors.kijun }} />
                      <Typography variant="caption" sx={{ color: ichimokuColors.kijun, fontWeight: 'bold' }}>기준</Typography>
                    </Box>
                  )}
                  {ichimokuSettings.chikou && (
                    <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                      <Box sx={{ width: 12, height: 2, bgcolor: ichimokuColors.chikou }} />
                      <Typography variant="caption" sx={{ color: ichimokuColors.chikou, fontWeight: 'bold' }}>후행</Typography>
                    </Box>
                  )}
                  {ichimokuSettings.senkouA && (
                    <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                      <Box sx={{ width: 12, height: 2, bgcolor: ichimokuColors.senkouA || '#26A69A' }} />
                      <Typography variant="caption" sx={{ color: ichimokuColors.senkouA || '#26A69A', fontWeight: 'bold' }}>선행1</Typography>
                    </Box>
                  )}
                  {ichimokuSettings.senkouB && (
                    <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                      <Box sx={{ width: 12, height: 2, bgcolor: ichimokuColors.senkouB || '#FF7043' }} />
                      <Typography variant="caption" sx={{ color: ichimokuColors.senkouB || '#FF7043', fontWeight: 'bold' }}>선행2</Typography>
                    </Box>
                  )}
                </Box>
              )}
              {/* 볼린저밴드 범례 */}
              {data.length > 0 && (() => {
                const bb = overlaySettingsOverride ?? (settings ? { bollingerUpper: settings.bollingerUpper, bollingerMiddle: settings.bollingerMiddle, bollingerLower: settings.bollingerLower } : null);
                return bb && (bb.bollingerUpper || bb.bollingerMiddle || bb.bollingerLower);
              })() && (
                <Box
                  sx={{
                    display: 'flex',
                    gap: 2,
                    flexWrap: 'wrap',
                    bgcolor: chartColors.tooltipBg,
                    borderRadius: 1,
                    px: 1.5,
                    py: 0.5,
                    border: `1px solid ${chartColors.tooltipBorder}`,
                  }}
                >
                  {(overlaySettingsOverride ?? settings)?.bollingerUpper && (
                    <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                      <Box sx={{ width: 12, height: 2, bgcolor: chartColors.bollingerUpperColor || '#FF9800' }} />
                      <Typography variant="caption" sx={{ color: chartColors.bollingerUpperColor || '#FF9800', fontWeight: 'bold' }}>상한</Typography>
                    </Box>
                  )}
                  {(overlaySettingsOverride ?? settings)?.bollingerMiddle && (
                    <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                      <Box sx={{ width: 12, height: 2, bgcolor: chartColors.bollingerMiddleColor || '#2196F3' }} />
                      <Typography variant="caption" sx={{ color: chartColors.bollingerMiddleColor || '#2196F3', fontWeight: 'bold' }}>중간</Typography>
                    </Box>
                  )}
                  {(overlaySettingsOverride ?? settings)?.bollingerLower && (
                    <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
                      <Box sx={{ width: 12, height: 2, bgcolor: chartColors.bollingerLowerColor || '#4CAF50' }} />
                      <Typography variant="caption" sx={{ color: chartColors.bollingerLowerColor || '#4CAF50', fontWeight: 'bold' }}>하한</Typography>
                    </Box>
                  )}
                </Box>
              )}
            </Box>
          ) : null}

          {/* 툴팁 - 업비트 스타일 (차트 상단 고정) */}
          {showTooltip && tooltipData && tooltipData.visible && (
            <Box
              sx={{
                position: 'absolute',
                left: 10,
                top: 10,
                zIndex: 100,
                bgcolor: themeMode === 'dark' ? 'rgba(30, 30, 30, 0.95)' : 'rgba(255, 255, 255, 0.95)',
                color: themeMode === 'dark' ? 'white' : 'text.primary',
                borderRadius: 1.5,
                border: `1.5px solid ${themeMode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
                px: 2,
                py: 1.5,
                pointerEvents: 'none',
                boxShadow: themeMode === 'dark' ? '0 4px 12px rgba(0,0,0,0.5)' : '0 4px 12px rgba(0,0,0,0.15)',
              }}
            >
              {/* 시간 */}
              <Typography variant="body2" sx={{ fontWeight: 600, mb: 1, fontSize: '13px', color: themeMode === 'dark' ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)' }}>
                {tooltipData.time}
              </Typography>
              
              {/* OHLCV 정보 - 가로 배치 */}
              <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
                {/* 시가 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>시가</Typography>
                  <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px' }}>
                    {formatNumber(tooltipData.open)}
                  </Typography>
                </Box>
                
                {/* 고가 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>고가</Typography>
                  <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px', color: chartColors.candleFalling }}>
                    {formatNumber(tooltipData.high)}
                  </Typography>
                </Box>
                
                {/* 저가 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>저가</Typography>
                  <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px', color: '#42a5f5' }}>
                    {formatNumber(tooltipData.low)}
                  </Typography>
                </Box>
                
                {/* 종가 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>종가</Typography>
                  <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px' }}>
                    {formatNumber(tooltipData.close)}
                  </Typography>
                </Box>
                
                {/* 변동 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>변동</Typography>
                  <Typography
                    variant="body2"
                    sx={{
                      fontWeight: 700,
                      fontSize: '13px',
                      color: tooltipData.change >= 0 ? chartColors.candleFalling : '#42a5f5'
                    }}
                  >
                    {tooltipData.change >= 0 ? '+' : ''}{formatNumber(tooltipData.change)}
                    <Typography component="span" sx={{ fontSize: '11px', ml: 0.5 }}>
                      ({tooltipData.changePercent >= 0 ? '+' : ''}{tooltipData.changePercent.toFixed(2)}%)
                    </Typography>
                  </Typography>
                </Box>
                
                {/* 거래량 */}
                {tooltipData.volume > 0 && (
                  <Box>
                    <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>거래량</Typography>
                    <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px' }}>
                      {tooltipData.volume.toLocaleString()}
                    </Typography>
                  </Box>
                )}
              </Box>
            </Box>
          )}

          {/* ✅ 고정된 툴팁 (더블 클릭으로 생성) - 클릭 가능 */}
          {pinnedTooltipData && pinnedTooltipData.visible && (
            <Box
              ref={tooltipRef}
              sx={{
                position: 'absolute',
                left: 10,
                top: 80,
                zIndex: 200,
                bgcolor: themeMode === 'dark' ? 'rgba(20, 20, 20, 0.98)' : 'rgba(255, 255, 255, 0.98)',
                color: themeMode === 'dark' ? 'white' : 'text.primary',
                borderRadius: 1.5,
                border: `2px solid ${themeMode === 'dark' ? 'rgba(66, 165, 245, 0.5)' : 'rgba(66, 165, 245, 0.5)'}`,
                px: 2,
                py: 1.5,
                pointerEvents: 'auto',
                boxShadow: themeMode === 'dark' ? '0 8px 24px rgba(0,0,0,0.7)' : '0 8px 24px rgba(0,0,0,0.25)',
                cursor: 'default',
                '&:hover': {
                  borderColor: themeMode === 'dark' ? 'rgba(66, 165, 245, 0.8)' : 'rgba(66, 165, 245, 0.8)',
                },
              }}
              onClick={(e) => e.stopPropagation()}
            >
              {/* 헤더 - 시간 + 닫기 버튼 */}
              <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
                <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px', color: themeMode === 'dark' ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)' }}>
                  📌 {pinnedTooltipData.time}
                </Typography>
                <IconButton
                  size="small"
                  onClick={() => {
                    console.log(`📌 [툴팁 닫기] 닫기 버튼 클릭`);
                    setPinnedTooltipData(null);
                  }}
                  sx={{
                    width: 20,
                    height: 20,
                    p: 0,
                    ml: 1,
                    color: 'text.secondary',
                    '&:hover': {
                      color: 'error.main',
                      bgcolor: 'rgba(255, 0, 0, 0.1)',
                    },
                  }}
                >
                  <Typography sx={{ fontSize: '16px', lineHeight: 1 }}>×</Typography>
                </IconButton>
              </Box>
              
              {/* OHLCV 정보 - 가로 배치 */}
              <Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
                {/* 시가 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>시가</Typography>
                  <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px' }}>
                    {formatNumber(pinnedTooltipData.open)}
                  </Typography>
                </Box>
                
                {/* 고가 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>고가</Typography>
                  <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px', color: chartColors.candleFalling }}>
                    {formatNumber(pinnedTooltipData.high)}
                  </Typography>
                </Box>
                
                {/* 저가 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>저가</Typography>
                  <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px', color: '#42a5f5' }}>
                    {formatNumber(pinnedTooltipData.low)}
                  </Typography>
                </Box>
                
                {/* 종가 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>종가</Typography>
                  <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px' }}>
                    {formatNumber(pinnedTooltipData.close)}
                  </Typography>
                </Box>
                
                {/* 변동 */}
                <Box>
                  <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>변동</Typography>
                  <Typography
                    variant="body2"
                    sx={{
                      fontWeight: 700,
                      fontSize: '13px',
                      color: pinnedTooltipData.change >= 0 ? chartColors.candleFalling : '#42a5f5'
                    }}
                  >
                    {pinnedTooltipData.change >= 0 ? '+' : ''}{formatNumber(pinnedTooltipData.change)}
                    <Typography component="span" sx={{ fontSize: '11px', ml: 0.5 }}>
                      ({pinnedTooltipData.changePercent >= 0 ? '+' : ''}{pinnedTooltipData.changePercent.toFixed(2)}%)
                    </Typography>
                  </Typography>
                </Box>
                
                {/* 거래량 */}
                {pinnedTooltipData.volume > 0 && (
                  <Box>
                    <Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '11px', display: 'block' }}>거래량</Typography>
                    <Typography variant="body2" sx={{ fontWeight: 600, fontSize: '13px' }}>
                      {pinnedTooltipData.volume.toLocaleString()}
                    </Typography>
                  </Box>
                )}
              </Box>
            </Box>
          )}

          {/* 좌우 화살표 버튼 (과거/미래 이동) - 업비트 스타일 */}
          {onNavigate && data.length > 0 && (
            <>
              {/* 왼쪽 화살표 버튼 (과거 데이터로 이동) */}
              {showLeftButton && (
                <IconButton
                  onClick={(e) => {
                    e.stopPropagation();
                    onNavigate('past');
                  }}
                  disabled={isLoading}
                  sx={{
                    position: 'absolute',
                    left: 4,
                    top: '50%',
                    transform: 'translateY(-50%)',
                    zIndex: 100,
                    color: 'text.secondary',
                    bgcolor: 'transparent',
                    p: 0.5,
                    '&:hover': {
                      bgcolor: 'action.hover',
                      color: 'primary.main',
                    },
                    '&:disabled': {
                      color: 'action.disabled',
                    },
                  }}
                >
                  {isLoading ? (
                    <CircularProgress size={20} />
                  ) : (
                    <Typography sx={{ fontSize: 24, fontWeight: 'bold', lineHeight: 1 }}>‹</Typography>
                  )}
                </IconButton>
              )}

              {/* 오른쪽 화살표 버튼 (미래 데이터로 이동) */}
              {showRightButton && (
                <IconButton
                  onClick={(e) => {
                    e.stopPropagation();
                    onNavigate('future');
                  }}
                  disabled={isLoading}
                  sx={{
                    position: 'absolute',
                    right: 4,
                    top: '50%',
                    transform: 'translateY(-50%)',
                    zIndex: 100,
                    color: 'text.secondary',
                    bgcolor: 'transparent',
                    p: 0.5,
                    '&:hover': {
                      bgcolor: 'action.hover',
                      color: 'primary.main',
                    },
                    '&:disabled': {
                      color: 'action.disabled',
                    },
                  }}
                >
                  {isLoading ? (
                    <CircularProgress size={20} />
                  ) : (
                    <Typography sx={{ fontSize: 24, fontWeight: 'bold', lineHeight: 1 }}>›</Typography>
                  )}
                </IconButton>
              )}
            </>
          )}

          <div
            ref={chartContainerRef}
            className="indicator-chart-container"
            onMouseEnter={() => {
              console.log(`🖱️ [CandlestickChart] 마우스 진입: isMouseOverChart → true`);
              setIsMouseOverChart(true);
              // ✅ 툴바는 부모 컨테이너에서 제어 (자식에서는 제어하지 않음)
            }}
            onMouseLeave={() => {
              console.log(`🖱️ [CandlestickChart] 마우스 벗어남: isMouseOverChart → false`);
              setIsMouseOverChart(false);
              // ✅ 툴바는 부모 컨테이너에서 제어 (자식에서는 숨기지 않음)
              // 돋보기 위치 초기화
              if (magnifierActive) {
                setMagnifierPos(null);
              }
            }}
            onMouseDown={(e) => {
              console.log(`🖱️ [div onMouseDown] 마우스 다운 이벤트 발생`);

              // 차트의 시간축에서 마우스 위치를 시간으로 변환
              if (chartRef.current && onChartMouseDownRef.current) {
                const rect = chartContainerRef.current?.getBoundingClientRect();
                if (rect) {
                  const x = e.clientX - rect.left;
                  const timeScale = chartRef.current.timeScale();
                  const time = timeScale.coordinateToTime(x);

                  if (time) {
                    const clickedTime = time as number;
                    console.log(`🖱️ [div onMouseDown] 변환된 시간: ${clickedTime}`);
                    onChartMouseDownRef.current(clickedTime);
                  }
                }
              }
            }}
            onMouseMove={(e) => {
              // 돋보기 모드일 때 마우스 위치 업데이트
              if (magnifierActive) {
                setMagnifierPos({ x: e.clientX, y: e.clientY });
              }
              
              // 차트의 시간축에서 마우스 위치를 시간으로 변환
              if (chartRef.current && onChartMouseMoveRef.current) {
                const rect = chartContainerRef.current?.getBoundingClientRect();
                if (rect) {
                  const x = e.clientX - rect.left;
                  const timeScale = chartRef.current.timeScale();
                  const time = timeScale.coordinateToTime(x);

                  if (time) {
                    const currentTime = time as number;
                    onChartMouseMoveRef.current(currentTime);
                  }
                }
              }
            }}
            onClick={(e) => {
              console.log(`🖱️ [div onClick] 클릭 이벤트 발생`);

              // ✅ 피보나치 타임존 등 클릭 기반 도구를 위한 onClick 이벤트
              if (chartRef.current && onChartClick) {
                const rect = chartContainerRef.current?.getBoundingClientRect();
                if (rect) {
                  const x = e.clientX - rect.left;
                  const timeScale = chartRef.current.timeScale();
                  const time = timeScale.coordinateToTime(x);

                  if (time) {
                    const clickedTime = time as number;
                    console.log(`🖱️ [div onClick] 변환된 시간: ${clickedTime}`);
                    onChartClick(clickedTime);
                  }
                }
              }
            }}
            onDoubleClick={(e) => {
              console.log(`🖱️🖱️ [div onDoubleClick] 더블 클릭 이벤트 발생`);
              e.stopPropagation();

              // ✅ 더블 클릭 시 현재 위치의 캔들 정보로 툴팁 고정
              if (!chartRef.current) {
                console.log(`⚠️ [툴팁 고정 실패] chartRef가 없음`);
                return;
              }

              // 1. tooltipData가 있으면 바로 사용
              if (tooltipData && tooltipData.visible) {
                setPinnedTooltipData({
                  ...tooltipData,
                  visible: true,
                });
                console.log(`📌 [툴팁 고정] 시간: ${tooltipData.time} (tooltipData 사용)`);
                return;
              }

              // 2. tooltipData가 없으면 클릭 위치에서 가장 가까운 캔들 찾기
              const rect = chartContainerRef.current?.getBoundingClientRect();
              if (!rect) {
                console.log(`⚠️ [툴팁 고정 실패] rect가 없음`);
                return;
              }

              const x = e.clientX - rect.left;
              const timeScale = chartRef.current.timeScale();
              const clickedTime = timeScale.coordinateToTime(x);

              if (!clickedTime) {
                console.log(`⚠️ [툴팁 고정 실패] 시간 변환 실패`);
                return;
              }

              // 가장 가까운 캔들 데이터 찾기
              const targetTimestamp = Number(clickedTime);
              const candleData = data.map((d, idx) => {
                const candleTime = typeof d.time === 'string' ? parseFloat(d.time) : Number(d.time);
                return {
                  ...d,
                  index: idx,
                  timeDiff: Math.abs(candleTime - targetTimestamp)
                };
              }).sort((a, b) => a.timeDiff - b.timeDiff);

              if (candleData.length === 0) {
                console.log(`⚠️ [툴팁 고정 실패] 캔들 데이터 없음`);
                return;
              }

              const nearestCandle = candleData[0];
              const candleTimestamp = typeof nearestCandle.time === 'string' ? parseFloat(nearestCandle.time) : Number(nearestCandle.time);
              const date = new Date(candleTimestamp * 1000);
              const month = (date.getMonth() + 1).toString().padStart(2, '0');
              const day = date.getDate().toString().padStart(2, '0');
              const hours = date.getHours().toString().padStart(2, '0');
              const minutes = date.getMinutes().toString().padStart(2, '0');
              const timeStr = `${month}/${day} ${hours}:${minutes}`;

              const change = nearestCandle.close - nearestCandle.open;
              const changePercent = (change / nearestCandle.open) * 100;

              setPinnedTooltipData({
                visible: true,
                x: e.clientX - rect.left,
                y: e.clientY - rect.top,
                time: timeStr,
                open: nearestCandle.open,
                high: nearestCandle.high,
                low: nearestCandle.low,
                close: nearestCandle.close,
                change,
                changePercent,
                volume: nearestCandle.volume || 0,
              });

              console.log(`📌 [툴팁 고정] 시간: ${timeStr}, 가격: ${nearestCandle.close} (근접 캔들 검색)`);
            }}
            onMouseUp={(e) => {
              console.log(`🖱️ [div onMouseUp] 마우스 업 이벤트 발생`);

              // 차트의 시간축에서 마우스 위치를 시간으로 변환
              if (chartRef.current && onChartMouseUpRef.current) {
                const rect = chartContainerRef.current?.getBoundingClientRect();
                if (rect) {
                  const x = e.clientX - rect.left;
                  const timeScale = chartRef.current.timeScale();
                  const time = timeScale.coordinateToTime(x);

                  if (time) {
                    const clickedTime = time as number;
                    console.log(`🖱️ [div onMouseUp] 변환된 시간: ${clickedTime}`);
                    onChartMouseUpRef.current(clickedTime);
                  }
                }
              }
            }}
            style={{
              boxSizing: 'border-box',  // ✅ border를 width 안에 포함 (서브차트 및 IndicatorChart와 완전히 일치)
              position: 'relative',  // ✅ 변경: absolute → relative (차트 컨테이너로서 정상 레이아웃)
              zIndex: 2,  // 차트 영역 (구름은 zIndex:1로 뒤에, 차트 배경 transparent로 구름 노출)
              top: 0,
              left: 0,
              width: '100%',
              height: '100%',
              minHeight: Math.max(80, height),  // ✅ clientHeight 0 시 차트 사라짐 방지 (멀티차트 영역조정 드래그)
              touchAction: 'none', // ✅ 차트 내부에서만 제스처 처리 (차트 외부는 기본 동작)
              cursor: !activeTool ? 'default' :
                activeTool === 'crosshair' ? 'crosshair' :
                activeTool === 'magnifier' ? 'none' :
                activeTool === 'trendLine' || activeTool === 'diagonalLine' || activeTool === 'angleLine' ? 'crosshair' :
                  activeTool === 'horizontalLine' || activeTool === 'verticalLine' ? 'crosshair' :
                    activeTool === 'fibonacci' || activeTool === 'fibonacciFan' || activeTool === 'fibonacciCircle' || activeTool === 'fibonacciWedge' ? 'crosshair' :
                      activeTool === 'rectangle' || activeTool === 'parallelChannel' ? 'crosshair' :
                        activeTool === 'measure' ? 'crosshair' :
                          activeTool === 'text' ? 'text' :
                            activeTool === 'brush' ? 'crosshair' :
                              'default',
            }}
          >
            {/* ✅ 캔들차트용 내비게이션 툴바 (x축 라벨 위쪽) - 모바일·멀티차트·알림목록(hideNavigationToolbar)에서는 숨김 */}
            {showNavigationToolbar && !isMobileMode && !hideNavigationToolbar && (() => {
              const btnSize = compactToolbar ? 22 : 32;
              const iconSize = compactToolbar ? 14 : 20;
              return (
              <Box
                sx={{
                  position: 'absolute',
                  bottom: 28,  // x축 라벨 높이만큼 위쪽
                  left: '50%',
                  transform: 'translateX(-50%)',
                  zIndex: 150,
                  bgcolor: 'background.paper',
                  boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
                  borderRadius: compactToolbar ? 0.75 : 1,
                  border: '1px solid',
                  borderColor: 'divider',
                  display: 'flex',
                  alignItems: 'center',
                  gap: compactToolbar ? 0.25 : 0.5,
                  px: compactToolbar ? 0.25 : 0.5,
                  py: compactToolbar ? 0.25 : 0.5,
                  pointerEvents: 'auto', // ✅ 툴바 버튼 클릭 가능하도록 설정
                }}
              >
                {/* 작게보기 */}
                <Tooltip title="작게보기">
                  <IconButton
                    size="small"
                    onClick={handleZoomOut}
                    sx={{
                      width: btnSize,
                      height: btnSize,
                      minWidth: btnSize,
                      minHeight: btnSize,
                      '&:hover': {
                        bgcolor: 'action.hover',
                      },
                    }}
                  >
                    <ZoomOut sx={{ fontSize: iconSize }} />
                  </IconButton>
                </Tooltip>

                {/* 크게보기 */}
                <Tooltip title="크게보기">
                  <IconButton
                    size="small"
                    onClick={handleZoomIn}
                    sx={{
                      width: btnSize,
                      height: btnSize,
                      minWidth: btnSize,
                      minHeight: btnSize,
                      '&:hover': {
                        bgcolor: 'action.hover',
                      },
                    }}
                  >
                    <ZoomIn sx={{ fontSize: iconSize }} />
                  </IconButton>
                </Tooltip>

                {/* 좌로 스크롤 */}
                <Tooltip title="좌로 스크롤">
                  <IconButton
                    size="small"
                    onClick={handleScrollLeft}
                    sx={{
                      width: btnSize,
                      height: btnSize,
                      minWidth: btnSize,
                      minHeight: btnSize,
                      '&:hover': {
                        bgcolor: 'action.hover',
                      },
                    }}
                  >
                    <ChevronLeft sx={{ fontSize: iconSize }} />
                  </IconButton>
                </Tooltip>

                {/* 우로 스크롤 */}
                <Tooltip title="우로 스크롤">
                  <IconButton
                    size="small"
                    onClick={handleScrollRight}
                    sx={{
                      width: btnSize,
                      height: btnSize,
                      minWidth: btnSize,
                      minHeight: btnSize,
                      '&:hover': {
                        bgcolor: 'action.hover',
                      },
                    }}
                  >
                    <ChevronRight sx={{ fontSize: iconSize }} />
                  </IconButton>
                </Tooltip>

                {/* 재설정 */}
                <Tooltip title="차트 재설정">
                  <IconButton
                    size="small"
                    onClick={handleResetChart}
                    sx={{
                      width: btnSize,
                      height: btnSize,
                      minWidth: btnSize,
                      minHeight: btnSize,
                      '&:hover': {
                        bgcolor: 'action.hover',
                      },
                    }}
                  >
                    <RestartAlt sx={{ fontSize: iconSize }} />
                  </IconButton>
                </Tooltip>
              </Box>
            );
            })()}
          </div>

          {/* 줌 레벨 컨트롤러 (상단 우측) - ❌ 제거됨 */}
          {/* {totalTimeRange && (
            <Box
              sx={{
                position: 'absolute',
                top: 8,
                right: 90,
                width: 200,
                backgroundColor: themeMode === 'dark' ? 'rgba(30, 35, 45, 0.92)' : 'rgba(255, 255, 255, 0.95)',
                backdropFilter: 'blur(6px)',
                borderRadius: 1,
                px: 1.5,
                py: 0.5,
                display: 'flex',
                alignItems: 'center',
                gap: 1,
                zIndex: 10,
                border: themeMode === 'dark' ? `1px solid ${chartColors.gridColor}` : '1px solid rgba(0,0,0,0.15)',
              }}
            >
              <Typography variant="caption" sx={{ color: themeMode === 'dark' ? '#e0e0e0' : '#666', fontSize: '10px', minWidth: 40 }}>
                Zoom
              </Typography>
              <Slider
                value={scrollbarValue}
                onChange={(_, newValue) => {
                  if (typeof newValue === 'number' && chartRef.current && totalTimeRange && !isScrollingRef.current) {
                    isScrollingRef.current = true;
                    setScrollbarValue(newValue);

                    const chart = chartRef.current;
                    const timeScale = chart.timeScale();
                    const currentRange = timeScale.getVisibleLogicalRange();

                    if (currentRange && data.length > 0) {
                      const totalBars = data.length;
                      const minVisibleBars = 50;
                      const maxVisibleBars = totalBars;

                      const targetVisibleBars = maxVisibleBars - ((maxVisibleBars - minVisibleBars) * newValue / 100);
                      
                      // ✅ 업비트 스타일: 우측(최신 시간)을 고정하고 좌측만 확대/축소
                      let newTo = currentRange.to;  // 우측 고정
                      let newFrom = (newTo as number) - targetVisibleBars;  // 좌측만 이동

                      // 좌측이 0보다 작아지면 조정
                      if (newFrom < 0) {
                        newFrom = 0;
                        newTo = Math.min(targetVisibleBars, totalBars) as any;
                      }
                      // 우측이 전체 범위를 넘어가면 조정
                      if ((newTo as number) > totalBars) {
                        newTo = totalBars as any;
                        newFrom = Math.max(0, totalBars - targetVisibleBars);
                      }

                      try {
                        isSettingRangeRef.current = true;
                        timeScale.setVisibleLogicalRange({
                          from: newFrom,
                          to: newTo,
                        });

                        // 보조지표 동기화를 위해 logical range를 time range로 변환하여 콜백 호출
                        if (onZoomChangeRef.current) {
                          const fromIndex = Math.max(0, Math.floor(newFrom));
                          const toIndex = Math.min(data.length - 1, Math.floor(newTo));

                          if (fromIndex < data.length && toIndex >= 0) {
                            const fromTime = new Date(data[fromIndex].time).getTime() / 1000;
                            const toTime = new Date(data[toIndex].time).getTime() / 1000;
                            onZoomChangeRef.current(fromTime, toTime);
                          }
                        }

                        setTimeout(() => {
                          isSettingRangeRef.current = false;
                          isScrollingRef.current = false;
                        }, 50);
                      } catch (e) {
                        console.error('줌 컨트롤 범위 설정 오류:', e);
                        isScrollingRef.current = false;
                      }
                    }
                  }
                }}
                min={0}
                max={100}
                step={1}
                size="small"
                sx={{
                  color: themeMode === 'dark' ? '#fff' : '#1976d2',
                  '& .MuiSlider-thumb': {
                    width: 12,
                    height: 12,
                    backgroundColor: themeMode === 'dark' ? '#fff' : '#1976d2',
                  },
                  '& .MuiSlider-track': {
                    height: 3,
                    backgroundColor: themeMode === 'dark' ? '#2196f3' : '#1976d2',
                  },
                  '& .MuiSlider-rail': {
                    height: 3,
                    opacity: 0.3,
                    backgroundColor: themeMode === 'dark' ? '#fff' : '#666',
                  },
                }}
              />
            </Box>
          )} */}

          {/* 볼린저밴드 배경 영역 Canvas Overlay */}
          {(() => {
            // ✅ 상한선과 하한선이 모두 ON일 때만 구름 영역 표시
            const shouldRender = bollingerBandData.length > 0 && 
                                chartRef.current && 
                                chartPriceRange.max > 0 && 
                                settings.bollingerUpper && 
                                settings.bollingerLower;
            console.log(`🎨 [Bollinger Overlay] 렌더링 조건:`, {
              dataLength: bollingerBandData.length,
              hasChart: !!chartRef.current,
              priceRangeMax: chartPriceRange.max,
              bollingerUpperEnabled: settings.bollingerUpper,
              bollingerLowerEnabled: settings.bollingerLower,
              shouldRender,
              backgroundColor: chartColors?.bollingerBandBackgroundColor
            });
            
            return             shouldRender ? (
              <BollingerBandOverlay
                chartRef={chartRef}
                candleSeriesRef={candleSeriesRef}
                bandData={bollingerBandData}
                priceRange={chartPriceRange}
                containerWidth={chartContainerRef.current?.clientWidth || 0}
                containerHeight={height}
                backgroundColor={chartColors?.bollingerBandBackgroundColor}
                yAxisScaleFactor={yAxisScaleFactor}
              />
            ) : null;
          })()}

          {/* 일목균형표 구름 영역 Canvas Overlay (업비트 스타일) */}
          {/* 미래 시점(26일 선행)까지 포함하여 두 선 사이를 정확히 채움 */}
          {(ichimokuCloudData.length > 0 || senkouALineData.length > 0 || senkouBLineData.length > 0 || tenkanLineData.length > 0 || kijunLineData.length > 0) && chartRef.current && chartPriceRange.max > 0 && (chartContainerRef.current?.clientWidth ?? 0) > 0 && height > 0 && (
            <IchimokuCloudOverlay
              chartRef={chartRef}
              candleSeriesRef={candleSeriesRef}
              isDisposedRef={isDisposedRef}
              cloudData={ichimokuCloudData}
              priceRange={chartPriceRange}
              containerWidth={chartContainerRef.current?.clientWidth || 0}
              containerHeight={chartContainerRef.current?.clientHeight || height}
              cloudBullishColor={ichimokuColors?.cloudBullish}
              cloudBearishColor={ichimokuColors?.cloudBearish}
              yAxisScaleFactor={yAxisScaleFactor}
              senkouALineData={senkouALineData}
              senkouBLineData={senkouBLineData}
              senkouAColor={ichimokuColors?.senkouA}
              senkouBColor={ichimokuColors?.senkouB}
              senkouAWidth={ichimokuColors?.senkouAWidth as number | undefined}
              senkouBWidth={ichimokuColors?.senkouBWidth as number | undefined}
              tenkanLineData={tenkanLineData}
              kijunLineData={kijunLineData}
              tenkanColor={ichimokuColors?.tenkan}
              kijunColor={ichimokuColors?.kijun}
              tenkanWidth={ichimokuColors?.tenkanWidth as number | undefined}
              kijunWidth={ichimokuColors?.kijunWidth as number | undefined}
            />
          )}

          {/* 커스텀 크로스헤어 오버레이 (외부 호버 시, 마우스가 차트 밖에 있을 때만) */}
          {externalCrosshair && externalCrosshair.visible && !isMouseOverChart && (
            <>
              {/* 세로선 */}
              <div
                style={{
                  position: 'absolute',
                  top: 0,
                  left: externalCrosshair.x,
                  width: 1,
                  height: '100%',
                  backgroundColor: chartColors.crosshairColor || '#758696',
                  pointerEvents: 'none',
                  zIndex: 100,
                }}
              />
              {/* 가로선 */}
              <div
                style={{
                  position: 'absolute',
                  top: externalCrosshair.y,
                  left: 0,
                  width: '100%',
                  height: 1,
                  backgroundColor: chartColors.crosshairColor || '#758696',
                  pointerEvents: 'none',
                  zIndex: 100,
                }}
              />
              {/* 가격 레이블 */}
              <div
                style={{
                  position: 'absolute',
                  top: externalCrosshair.y - 10,
                  right: 0,
                  backgroundColor: chartColors.crosshairLabelBg || '#4c525e',
                  color: '#fff',
                  padding: '2px 6px',
                  fontSize: '11px',
                  borderRadius: '2px',
                  pointerEvents: 'none',
                  zIndex: 101,
                }}
              >
                {externalCrosshair.price >= 1
                  ? externalCrosshair.price.toLocaleString('ko-KR', { maximumFractionDigits: 0 })
                  : externalCrosshair.price.toFixed(4)
                }원
              </div>
            </>
          )}

          {/* 매수/매도 마커 오버레이 (배경 있는 박스 형태) - 더블클릭 가능 */}
          {tradeMarkers.map((marker, index) => (
            <Box
              key={`${marker.type}-${marker.time}-${index}`}
              onDoubleClick={(e) => {
                e.stopPropagation();
                console.log('🖱️🖱️ [매수/매도 텍스트 더블클릭]', marker);
                if (onMarkerClickRef.current) {
                  onMarkerClickRef.current(marker.time, marker.type);
                }
              }}
              sx={{
                position: 'absolute',
                left: `${marker.x}px`,
                top: `${marker.y}px`,
                transform: 'translate(-50%, -50%)',
                bgcolor: marker.type === 'BUY' ? '#2196F3' : '#f44336',
                color: 'white',
                px: 0.8,
                py: 0.2,
                borderRadius: '3px',
                fontSize: '10px',
                fontWeight: 'bold',
                pointerEvents: 'auto',
                cursor: 'pointer',
                zIndex: 150,
                boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
                whiteSpace: 'nowrap',
                border: '1px solid rgba(255,255,255,0.3)',
                transition: 'all 0.2s',
                '&:hover': {
                  transform: 'translate(-50%, -50%) scale(1.1)',
                  boxShadow: '0 2px 8px rgba(0,0,0,0.6)',
                },
              }}
            >
              {marker.type === 'BUY' ? '매수' : '매도'}
            </Box>
          ))}
        </Box>

        {/* 트렌드 상세 정보 모달 */}
        {trendDetailModal && (
          <Dialog
            open={trendDetailModal.visible}
            onClose={() => setTrendDetailModal(null)}
            maxWidth="md"
            fullWidth
            PaperProps={{
              sx: {
                borderRadius: 2,
                boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
              }
            }}
          >
            <DialogTitle sx={{
              bgcolor: trendDetailModal.trendType === '정배열'
                ? 'rgba(76, 175, 80, 0.35)'
                : trendDetailModal.trendType === '역배열'
                  ? 'rgba(244, 67, 54, 0.35)'
                  : 'rgba(158, 158, 158, 0.35)',
              fontWeight: 700,
              fontSize: '1.3rem',
              display: 'flex',
              alignItems: 'center',
              gap: 1.5,
              py: 2,
              borderBottom: '2px solid',
              borderColor: trendDetailModal.trendType === '정배열'
                ? 'rgba(76, 175, 80, 0.6)'
                : trendDetailModal.trendType === '역배열'
                  ? 'rgba(244, 67, 54, 0.6)'
                  : 'rgba(158, 158, 158, 0.6)',
              color: trendDetailModal.trendType === '정배열'
                ? '#2E7D32'
                : trendDetailModal.trendType === '역배열'
                  ? '#C62828'
                  : '#616161',
            }}>
              <span style={{
                fontSize: '1.4em',
                lineHeight: 1,
              }}>
                {trendDetailModal.trendType === '정배열' ? '📈' : trendDetailModal.trendType === '역배열' ? '📉' : '📊'}
              </span>
              트렌드 분석 - {trendDetailModal.trendType}
            </DialogTitle>
            <DialogContent sx={{
              pt: 2.5,
              pb: 2,
              px: 3,
              '&::-webkit-scrollbar': {
                width: '8px',
              },
              '&::-webkit-scrollbar-track': {
                backgroundColor: 'rgba(0,0,0,0.05)',
                borderRadius: '4px',
              },
              '&::-webkit-scrollbar-thumb': {
                backgroundColor: 'rgba(0,0,0,0.2)',
                borderRadius: '4px',
                '&:hover': {
                  backgroundColor: 'rgba(0,0,0,0.3)',
                },
              },
            }}>
              {/* 시간 정보 */}
              <Box sx={{
                mb: 2.5,
                p: 2,
                bgcolor: 'background.default',
                borderRadius: 1.5,
                border: '1px solid',
                borderColor: 'divider'
              }}>
                <Typography variant="subtitle2" color="text.secondary" sx={{ mb: 0.5, fontSize: '0.85rem' }}>
                  ⏰ 시간
                </Typography>
                <Typography variant="h6" fontWeight={600}>
                  {new Date(trendDetailModal.timestamp).toLocaleString('ko-KR', {
                    year: 'numeric',
                    month: '2-digit',
                    day: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: false
                  })}
                </Typography>
              </Box>

              {/* MA 값 비교 테이블 */}
              <Box sx={{ mb: 2.5 }}>
                <Typography variant="subtitle1" fontWeight={700} gutterBottom sx={{ mb: 1.5 }}>
                  📊 이동평균선 (MA) 분석
                </Typography>
                <Table
                  size="small"
                  sx={{
                    border: '1px solid',
                    borderColor: 'divider',
                    borderRadius: 1,
                    overflow: 'hidden',
                  }}
                >
                  <TableBody>
                    <TableRow sx={{ bgcolor: 'rgba(33, 150, 243, 0.12)' }}>
                      <TableCell sx={{
                        fontWeight: 600,
                        width: '40%',
                        py: 1.5,
                        borderBottom: '1px solid',
                        borderColor: 'divider'
                      }}>
                        단기 MA ({trendShortMA}일)
                      </TableCell>
                      <TableCell align="right" sx={{
                        fontSize: '1.15em',
                        fontWeight: 700,
                        py: 1.5,
                        borderBottom: '1px solid',
                        borderColor: 'divider'
                      }}>
                        {trendDetailModal.maShort.toLocaleString('ko-KR', { maximumFractionDigits: 2 })}
                      </TableCell>
                    </TableRow>
                    <TableRow sx={{ bgcolor: 'rgba(156, 39, 176, 0.12)' }}>
                      <TableCell sx={{
                        fontWeight: 600,
                        py: 1.5,
                        borderBottom: '1px solid',
                        borderColor: 'divider'
                      }}>
                        중기 MA ({trendMediumMA}일)
                      </TableCell>
                      <TableCell align="right" sx={{
                        fontSize: '1.15em',
                        fontWeight: 700,
                        py: 1.5,
                        borderBottom: '1px solid',
                        borderColor: 'divider'
                      }}>
                        {trendDetailModal.maMedium.toLocaleString('ko-KR', { maximumFractionDigits: 2 })}
                      </TableCell>
                    </TableRow>
                    <TableRow sx={{ bgcolor: 'rgba(255, 152, 0, 0.12)' }}>
                      <TableCell sx={{
                        fontWeight: 600,
                        py: 1.5,
                        borderBottom: 'none'
                      }}>
                        장기 MA ({trendLongMA}일)
                      </TableCell>
                      <TableCell align="right" sx={{
                        fontSize: '1.15em',
                        fontWeight: 700,
                        py: 1.5,
                        borderBottom: 'none'
                      }}>
                        {trendDetailModal.maLong.toLocaleString('ko-KR', { maximumFractionDigits: 2 })}
                      </TableCell>
                    </TableRow>
                  </TableBody>
                </Table>
              </Box>

              {/* 판단 근거 */}
              <Box sx={{
                p: 2.5,
                borderRadius: 1.5,
                bgcolor: trendDetailModal.trendType === '정배열'
                  ? 'rgba(76, 175, 80, 0.15)'
                  : trendDetailModal.trendType === '역배열'
                    ? 'rgba(244, 67, 54, 0.15)'
                    : 'rgba(158, 158, 158, 0.15)',
                border: '2px solid',
                borderColor: trendDetailModal.trendType === '정배열'
                  ? 'rgba(76, 175, 80, 0.5)'
                  : trendDetailModal.trendType === '역배열'
                    ? 'rgba(244, 67, 54, 0.5)'
                    : 'rgba(158, 158, 158, 0.5)',
                mb: 2.5
              }}>
                <Typography variant="subtitle1" fontWeight={700} gutterBottom sx={{ mb: 1.5 }}>
                  📋 판단 근거
                </Typography>

                {trendDetailModal.trendType === '정배열' && (
                  <Box>
                    <Typography variant="body2" sx={{ mb: 1.5, fontWeight: 600 }}>
                      ✅ 정배열 조건 충족
                    </Typography>
                    <Typography variant="body2" component="div" sx={{ pl: 2, lineHeight: 1.8, mb: 1.5 }}>
                      • 단기 MA ({trendShortMA}일) &gt; 중기 MA ({trendMediumMA}일) &gt; 장기 MA ({trendLongMA}일)<br />
                      • <strong>{trendDetailModal.maShort.toFixed(0)}</strong> &gt; <strong>{trendDetailModal.maMedium.toFixed(0)}</strong> &gt; <strong>{trendDetailModal.maLong.toFixed(0)}</strong>
                    </Typography>
                    <Box sx={{
                      p: 1.5,
                      bgcolor: 'rgba(76, 175, 80, 0.15)',
                      borderRadius: 1,
                      borderLeft: '4px solid #4CAF50'
                    }}>
                      <Typography variant="body2" sx={{ fontWeight: 600, color: '#2E7D32' }}>
                        📈 상승 추세
                      </Typography>
                      <Typography variant="body2" sx={{ mt: 0.5, color: 'text.secondary' }}>
                        단기 평균이 장기 평균보다 높아 상승 모멘텀이 있습니다.
                      </Typography>
                    </Box>
                  </Box>
                )}

                {trendDetailModal.trendType === '역배열' && (
                  <Box>
                    <Typography variant="body2" sx={{ mb: 1.5, fontWeight: 600 }}>
                      ✅ 역배열 조건 충족
                    </Typography>
                    <Typography variant="body2" component="div" sx={{ pl: 2, lineHeight: 1.8, mb: 1.5 }}>
                      • 단기 MA ({trendShortMA}일) &lt; 중기 MA ({trendMediumMA}일) &lt; 장기 MA ({trendLongMA}일)<br />
                      • <strong>{trendDetailModal.maShort.toFixed(0)}</strong> &lt; <strong>{trendDetailModal.maMedium.toFixed(0)}</strong> &lt; <strong>{trendDetailModal.maLong.toFixed(0)}</strong>
                    </Typography>
                    <Box sx={{
                      p: 1.5,
                      bgcolor: 'rgba(244, 67, 54, 0.15)',
                      borderRadius: 1,
                      borderLeft: '4px solid #F44336'
                    }}>
                      <Typography variant="body2" sx={{ fontWeight: 600, color: '#C62828' }}>
                        📉 하락 추세
                      </Typography>
                      <Typography variant="body2" sx={{ mt: 0.5, color: 'text.secondary' }}>
                        단기 평균이 장기 평균보다 낮아 하락 모멘텀이 있습니다.
                      </Typography>
                    </Box>
                  </Box>
                )}

                {trendDetailModal.trendType === '혼조' && (
                  <Box>
                    <Typography variant="body2" sx={{ mb: 1.5, fontWeight: 600 }}>
                      ⚠️ 혼조 상태
                    </Typography>
                    <Typography variant="body2" component="div" sx={{ pl: 2, lineHeight: 1.8, mb: 1.5 }}>
                      • 정배열/역배열 조건을 모두 충족하지 않음<br />
                      • 단기: <strong>{trendDetailModal.maShort.toFixed(0)}</strong>, 중기: <strong>{trendDetailModal.maMedium.toFixed(0)}</strong>, 장기: <strong>{trendDetailModal.maLong.toFixed(0)}</strong>
                    </Typography>
                    <Box sx={{
                      p: 1.5,
                      bgcolor: 'rgba(158, 158, 158, 0.15)',
                      borderRadius: 1,
                      borderLeft: '4px solid #9E9E9E'
                    }}>
                      <Typography variant="body2" sx={{ fontWeight: 600, color: '#616161' }}>
                        📊 혼조 추세
                      </Typography>
                      <Typography variant="body2" sx={{ mt: 0.5, color: 'text.secondary' }}>
                        MA 순서가 섞여 있어 명확한 방향성이 없습니다.
                      </Typography>
                    </Box>
                  </Box>
                )}
              </Box>

              {/* 모든 MA 값 표시 */}
              {trendDetailModal.allMaValues && (
                <Box sx={{
                  pt: 2.5,
                  borderTop: '2px solid',
                  borderColor: 'divider',
                }}>
                  <Typography variant="subtitle1" fontWeight={700} gutterBottom sx={{ mb: 1.5 }}>
                    📊 전체 이동평균선 값
                  </Typography>
                  <Box sx={{
                    display: 'grid',
                    gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))',
                    gap: 1.5,
                  }}>
                    {Object.keys(trendDetailModal.allMaValues)
                      .filter(key => key.startsWith('sma') || key.startsWith('ema'))
                      .sort((a, b) => {
                        const numA = parseInt(a.replace(/[^0-9]/g, ''));
                        const numB = parseInt(b.replace(/[^0-9]/g, ''));
                        return numA - numB;
                      })
                      .map(key => {
                        const value = trendDetailModal.allMaValues[key];
                        if (value === null || value === undefined) return null;
                        return (
                          <Box
                            key={key}
                            sx={{
                              p: 1.5,
                              bgcolor: 'background.default',
                              borderRadius: 1,
                              textAlign: 'center',
                              border: '1px solid',
                              borderColor: 'divider',
                              transition: 'all 0.2s',
                              '&:hover': {
                                borderColor: 'primary.main',
                                bgcolor: 'action.hover',
                              }
                            }}
                          >
                            <Typography
                              variant="caption"
                              color="text.secondary"
                              display="block"
                              sx={{ fontSize: '0.7rem', mb: 0.5 }}
                            >
                              {key.toUpperCase()}
                            </Typography>
                            <Typography variant="body2" fontWeight={600}>
                              {typeof value === 'number' ? value.toFixed(0) : value}
                            </Typography>
                          </Box>
                        );
                      })
                      .filter(Boolean)}
                  </Box>
                </Box>
              )}
            </DialogContent>
            <DialogActions sx={{
              p: 2.5,
              borderTop: '1px solid',
              borderColor: 'divider',
              bgcolor: 'background.default',
            }}>
              <Button
                onClick={() => setTrendDetailModal(null)}
                variant="contained"
                size="large"
                sx={{
                  minWidth: 120,
                  fontWeight: 600,
                }}
              >
                닫기
              </Button>
            </DialogActions>
          </Dialog>
        )}

        {/* 돋보기 렌즈 */}
        {magnifierActive && magnifierPos && (
          <Box
            sx={{
              position: 'fixed',
              left: magnifierPos.x - 200,
              top: magnifierPos.y - 200,
              width: 400,
              height: 400,
              borderRadius: '50%',
              border: '4px solid',
              borderColor: 'primary.main',
              boxShadow: '0 8px 32px rgba(0,0,0,0.3), inset 0 0 0 2px rgba(255,255,255,0.2)',
              overflow: 'visible', // 마커 오버레이를 위해 visible로 변경
              pointerEvents: 'none',
              zIndex: 9999,
              bgcolor: magnifierShowChart 
                ? (themeMode === 'dark' ? '#1e1e1e' : '#ffffff')
                : 'rgba(0, 0, 0, 0.05)',
            }}
          >
            {/* 돋보기 차트 컨테이너 - 항상 렌더링 */}
            <div
              ref={magnifierContainerRef}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: '100%',
                display: magnifierShowChart ? 'block' : 'none',
                clipPath: 'circle(50% at 50% 50%)', // 원형으로 클리핑
                overflow: 'hidden',
              }}
            />

            {/* 돋보기 차트 매수/매도 마커 오버레이 컨테이너 */}
            {magnifierShowChart && (
              <Box
                sx={{
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  width: '100%',
                  height: '100%',
                  clipPath: 'circle(50% at 50% 50%)',
                  overflow: 'hidden',
                  pointerEvents: 'none',
                  zIndex: 10000,
                }}
              >
                {/* 매수/매도 마커 오버레이 (사각형 배경) */}
                {magnifierTradeMarkers.map((marker, index) => (
                  <Box
                    key={`magnifier-${marker.type}-${marker.time}-${index}`}
                    sx={{
                      position: 'absolute',
                      left: `${marker.x}px`,
                      top: `${marker.y}px`,
                      transform: 'translate(-50%, -50%)',
                      bgcolor: marker.type === 'BUY' ? '#2196F3' : '#f44336',
                      color: 'white',
                      px: 0.8,
                      py: 0.2,
                      borderRadius: '3px',
                      fontSize: '10px',
                      fontWeight: 'bold',
                      pointerEvents: 'none',
                      boxShadow: '0 1px 4px rgba(0,0,0,0.4)',
                      whiteSpace: 'nowrap',
                      border: '1px solid rgba(255,255,255,0.3)',
                    }}
                  >
                    {marker.type === 'BUY' ? '매수' : '매도'}
                  </Box>
                ))}
              </Box>
            )}
            
            {magnifierShowChart ? (
              // 차트 표시 모드 (클릭 후)
              <>
                {/* 상단 안내 */}
                <Box
                  sx={{
                    position: 'absolute',
                    top: 10,
                    left: '50%',
                    transform: 'translateX(-50%)',
                    bgcolor: themeMode === 'dark' ? 'rgba(30, 30, 30, 0.9)' : 'rgba(255, 255, 255, 0.9)',
                    color: themeMode === 'dark' ? 'white' : 'text.primary',
                    px: 1.5,
                    py: 0.5,
                    borderRadius: 1,
                    fontSize: '10px',
                    fontWeight: 600,
                    boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
                    whiteSpace: 'nowrap',
                    zIndex: 10000,
                  }}
                >
                  🔍 확대 보기
                </Box>
              </>
            ) : (
              // 기본 안내 모드 (클릭 전)
              <>
                {/* 돋보기 효과 안내 */}
                <Box
                  sx={{
                    position: 'absolute',
                    top: 20,
                    left: '50%',
                    transform: 'translateX(-50%)',
                    bgcolor: themeMode === 'dark' ? 'rgba(30, 30, 30, 0.9)' : 'rgba(255, 255, 255, 0.9)',
                    color: themeMode === 'dark' ? 'white' : 'text.primary',
                    px: 2,
                    py: 0.5,
                    borderRadius: 1,
                    fontSize: '12px',
                    fontWeight: 600,
                    boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
                    whiteSpace: 'nowrap',
                  }}
                >
                  🔍 돋보기 모드
                </Box>
                
                {/* 클릭 안내 */}
                <Box
                  sx={{
                    position: 'absolute',
                    bottom: 20,
                    left: '50%',
                    transform: 'translateX(-50%)',
                    bgcolor: themeMode === 'dark' ? 'rgba(30, 30, 30, 0.9)' : 'rgba(255, 255, 255, 0.9)',
                    color: themeMode === 'dark' ? 'white' : 'text.primary',
                    px: 2,
                    py: 0.5,
                    borderRadius: 1,
                    fontSize: '11px',
                    fontWeight: 500,
                    boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
                    whiteSpace: 'nowrap',
                  }}
                >
                  클릭하여 확대
                </Box>
                
                {/* 중앙 십자선 */}
                <Box
                  sx={{
                    position: 'absolute',
                    left: '50%',
                    top: '20%',
                    bottom: '20%',
                    width: 2,
                    bgcolor: 'primary.main',
                    transform: 'translateX(-50%)',
                    opacity: 0.8,
                  }}
                />
                <Box
                  sx={{
                    position: 'absolute',
                    top: '50%',
                    left: '20%',
                    right: '20%',
                    height: 2,
                    bgcolor: 'primary.main',
                    transform: 'translateY(-50%)',
                    opacity: 0.8,
                  }}
                />
                
                {/* 중앙 포인트 */}
                <Box
                  sx={{
                    position: 'absolute',
                    left: '50%',
                    top: '50%',
                    width: 8,
                    height: 8,
                    borderRadius: '50%',
                    bgcolor: 'primary.main',
                    transform: 'translate(-50%, -50%)',
                    boxShadow: '0 0 0 2px white, 0 0 8px rgba(25, 118, 210, 0.6)',
                  }}
                />
              </>
        )}
      </Box>
        )}

        {/* ✅ 캔들 영역 리사이즈 핸들 (캔들/보조지표 경계 - 투자분석 화면과 동일) */}
        {showCandleResizeHandle && onCandleHeightResize && subPanels && subPanels.length > 0 && subPanels.some(p => p.enabled) && (
          <Box
            onMouseDown={(e: React.MouseEvent) => {
              e.preventDefault();
              e.stopPropagation();
              setIsCandleResizing(true);
              isResizingRef.current = true;
              const startY = e.clientY;
              const startHeight = height;
              const handleMouseMove = (moveEvent: MouseEvent) => {
                moveEvent.preventDefault();
                const deltaY = moveEvent.clientY - startY;
                const newHeight = Math.max(80, Math.min(800, startHeight + deltaY));
                onCandleHeightResize(newHeight);
              };
              const handleMouseUp = () => {
                setIsCandleResizing(false);
                isResizingRef.current = isResizing;
                document.removeEventListener('mousemove', handleMouseMove);
                document.removeEventListener('mouseup', handleMouseUp);
                document.body.style.cursor = '';
                document.body.style.userSelect = '';
              };
              document.body.style.cursor = 'ns-resize';
              document.body.style.userSelect = 'none';
              document.addEventListener('mousemove', handleMouseMove, { passive: false });
              document.addEventListener('mouseup', handleMouseUp);
            }}
            sx={{
              position: 'relative',
              width: '100%',
              height: 8,
              cursor: 'ns-resize',
              bgcolor: 'transparent',
              flexShrink: 0,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              zIndex: 200,
              userSelect: 'none',
              '&:hover': { '& .candle-resize-handle-icon': { opacity: 1 } },
            }}
          >
            <Box
              className="candle-resize-handle-icon"
              sx={{
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                bgcolor: isCandleResizing ? 'primary.main' : 'rgba(144, 202, 249, 0.8)',
                borderRadius: '12px',
                px: 1.5,
                py: 0.5,
                opacity: isCandleResizing ? 1 : 0.6,
                transition: 'opacity 0.2s, background-color 0.2s',
                boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
              }}
            >
              <DragIndicator sx={{ fontSize: 16, color: 'white', transform: 'rotate(90deg)' }} />
            </Box>
          </Box>
        )}

        {/* ✅ 추세 구간 표시 바 (캔들차트 X축 바로 위) — hideTrendAlignmentBar 시 비표시 */}
        {chartReady && !hideTrendAlignmentBar && chartRef.current && indicatorData && indicatorData.length > 0 && (
          <Box sx={{ 
            width: '100%', 
            position: 'relative',
          }}>
            {/* 추세 구간 표시 바 (정배열/역배열/혼조) */}
            {(() => {
              // ✅ 모바일 모드에서는 추세 바 숨김
              if (isMobileMode) return null;
              if (!indicatorData || indicatorData.length === 0) return null;

              try {
                const chart = chartRef.current!;
                const timeScale = chart.timeScale();
                const visibleRange = timeScale.getVisibleRange();

                if (!visibleRange) return null;

                const visibleFrom = visibleRange.from as number;
                const visibleTo = visibleRange.to as number;

                // 보이는 범위의 데이터 필터
                const visibleData = indicatorData.filter(ind => {
                  const time = new Date(ind.timestamp).getTime() / 1000;
                  return time >= visibleFrom && time <= visibleTo;
                });

                // 필터링 실패하면 전체 데이터 사용
                const dataToUse = visibleData.length > 0 ? visibleData : indicatorData;

                // 트렌드 바 생성
                const bars: JSX.Element[] = [];

                for (let i = 0; i < dataToUse.length; i++) {
                  const ind = dataToUse[i];
                  const time = new Date(ind.timestamp).getTime() / 1000;

                  // MA 값 (sma로 접근)
                  const maShort = (ind as any)[`sma${trendShortMA}`];
                  const maMedium = (ind as any)[`sma${trendMediumMA}`];
                  const maLong = (ind as any)[`sma${trendLongMA}`];

                  // 트렌드 판단
                  let color = 'rgba(158, 158, 158, 0.5)'; // 기본: 혼조 (회색)
                  let trendType = '혼조';

                  if (maShort && maMedium && maLong) {
                    if (maShort > maMedium && maMedium > maLong) {
                      color = 'rgba(76, 175, 80, 0.75)'; // 정배열 (초록)
                      trendType = '정배열';
                    } else if (maShort < maMedium && maMedium < maLong) {
                      color = 'rgba(244, 67, 54, 0.75)'; // 역배열 (빨강)
                      trendType = '역배열';
                    }
                  }

                  // 좌표 변환
                  const x = timeScale.timeToCoordinate(time as Time);
                  if (x === null) continue;

                  // 너비 계산
                  let width = 8;
                  if (i < dataToUse.length - 1) {
                    const nextTime = new Date(dataToUse[i + 1].timestamp).getTime() / 1000;
                    const nextX = timeScale.timeToCoordinate(nextTime as Time);
                    if (nextX !== null) {
                      width = nextX - x;
                    }
                  }

                  bars.push(
                    <div
                      key={i}
                      style={{
                        position: 'absolute',
                        left: `${x}px`,
                        width: `${Math.max(width, 1)}px`,
                        height: '100%',
                        backgroundColor: color,
                        borderRight: '1px solid rgba(0,0,0,0.25)',
                        cursor: 'pointer',
                      }}
                      onDoubleClick={() => {
                        setTrendDetailModal({
                          visible: true,
                          timestamp: ind.timestamp,
                          maShort: maShort || 0,
                          maMedium: maMedium || 0,
                          maLong: maLong || 0,
                          trendType: trendType as '정배열' | '역배열' | '혼조',
                          allMaValues: ind,
                        });
                      }}
                    />
                  );
                }

                if (bars.length === 0) return null;

                return (
                  <Box
                    sx={{
                      position: 'relative',
                      width: '100%',
                      height: 16,
                      pointerEvents: 'auto',
                      border: themeMode === 'dark' 
                        ? '1px solid rgba(255, 255, 255, 0.15)' 
                        : '1px solid rgba(0, 0, 0, 0.15)',
                      borderTop: '2px solid rgba(0,0,0,0.2)',
                      borderBottom: '2px solid rgba(0,0,0,0.2)',
                      bgcolor: chartColors.background,
                    }}
                  >
                    {bars}
                  </Box>
                );
              } catch (error) {
                console.error('트렌드 바 렌더링 오류:', error);
                return null;
              }
            })()}
          </Box>
        )}

        {/* ✅ 서브차트 패널들 (TradingView 스타일) - @dnd-kit 드래그 앤 드롭 */}
        {subPanels && subPanels.length > 0 && subPanels.some(p => p.enabled) && (
          <DndContext
            sensors={sensors}
            collisionDetection={closestCenter}
            onDragStart={() => {
              // 드래그 시작 시 차트 갱신 중지
              isDraggingRef.current = true;
            }}
            onDragEnd={(event: DragEndEvent) => {
              const { active, over } = event;
              if (over && active.id !== over.id && onSubPanelReorder) {
                // ✅ 전체 subPanels 배열에서 실제 인덱스를 찾아야 함
                const oldIndex = subPanels.findIndex(p => p.type === active.id);
                const newIndex = subPanels.findIndex(p => p.type === over.id);
                
                console.log(`🔄 [SubPanel Reorder] ${active.id} (index: ${oldIndex}) → ${over.id} (index: ${newIndex})`);
                
                if (oldIndex !== -1 && newIndex !== -1) {
                  onSubPanelReorder(oldIndex, newIndex);
                }
              }
              
              // 드래그 종료 후 차트 갱신 재개
              isDraggingRef.current = false;
            }}
          >
            <SortableContext
              items={subPanels.filter(p => p.enabled).map(p => p.type)}
              strategy={verticalListSortingStrategy}
            >
              <Box 
                sx={{ 
                  width: '100%', 
                  mt: 0, 
                  position: 'relative',
                  minHeight: maximizedPanel ? '400px' : 'auto',
                }}
              >
                {subPanels.filter(p => p.enabled).map((panel, index) => {
                  const isEven = index % 2 === 0;
                  const isMaximized = maximizedPanel === panel.type;
                  const shouldHide = maximizedPanel && !isMaximized;
                  
                  return (
                    <Box
                      key={`${panel.type}-${colorVersion}-${settingsVersion}`}
                      sx={{
                        display: shouldHide ? 'none' : 'block',
                      }}
                    >
                      <SortableSubPanel
                        panel={panel}
                        index={index}
                        isEven={isEven}
                        chartColors={chartColors}
                        themeMode={themeMode}
                        subChartContainersRef={subChartContainersRef}
                        onSubPanelSettingsClick={onSubPanelSettingsClick}
                        onIndicatorDelete={onIndicatorDelete}
                        showDragHandle={!!onSubPanelReorder && subPanels.filter(p => p.enabled).length > 1}
                        isMaximized={isMaximized}
                        onMaximize={() => setMaximizedPanel(panel.type)}
                        onRestore={() => setMaximizedPanel(null)}
                        colorVersion={colorVersion}
                        settingsVersion={settingsVersion}
                      />
                    </Box>
                  );
                })}
                
                {/* ✅ 보조지표 하단 툴바 (hideSubPanelToolbar·hideNavigationToolbar·최대화·모바일 시 숨김) */}
                {showNavigationToolbar && !hideNavigationToolbar && !hideSubPanelToolbar && !maximizedPanel && !isMobileMode && (() => {
                  const btnSize = compactToolbar ? 22 : 32;
                  const iconSize = compactToolbar ? 14 : 20;
                  return (
                  <Box
                    sx={{
                      position: 'relative',
                      width: '100%',
                      display: 'flex',
                      justifyContent: 'center',
                      py: 1,
                    }}
                  >
                    <Box
                      sx={{
                        bgcolor: 'background.paper',
                        boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
                        borderRadius: compactToolbar ? 0.75 : 1,
                        border: '1px solid',
                        borderColor: 'divider',
                        display: 'flex',
                        alignItems: 'center',
                        gap: compactToolbar ? 0.25 : 0.5,
                        px: compactToolbar ? 0.25 : 0.5,
                        py: compactToolbar ? 0.25 : 0.5,
                      }}
                    >
                      {/* 작게보기 */}
                      <Tooltip title="작게보기">
                        <IconButton
                          size="small"
                          onClick={handleZoomOut}
                          sx={{
                            width: btnSize,
                            height: btnSize,
                            minWidth: btnSize,
                            minHeight: btnSize,
                            '&:hover': { bgcolor: 'action.hover' },
                          }}
                        >
                          <ZoomOut sx={{ fontSize: iconSize }} />
                        </IconButton>
                      </Tooltip>

                      {/* 크게보기 */}
                      <Tooltip title="크게보기">
                        <IconButton
                          size="small"
                          onClick={handleZoomIn}
                          sx={{
                            width: btnSize,
                            height: btnSize,
                            minWidth: btnSize,
                            minHeight: btnSize,
                            '&:hover': { bgcolor: 'action.hover' },
                          }}
                        >
                          <ZoomIn sx={{ fontSize: iconSize }} />
                        </IconButton>
                      </Tooltip>

                      {/* 좌로 스크롤 */}
                      <Tooltip title="좌로 스크롤">
                        <IconButton
                          size="small"
                          onClick={handleScrollLeft}
                          sx={{
                            width: btnSize,
                            height: btnSize,
                            minWidth: btnSize,
                            minHeight: btnSize,
                            '&:hover': { bgcolor: 'action.hover' },
                          }}
                        >
                          <ChevronLeft sx={{ fontSize: iconSize }} />
                        </IconButton>
                      </Tooltip>

                      {/* 우로 스크롤 */}
                      <Tooltip title="우로 스크롤">
                        <IconButton
                          size="small"
                          onClick={handleScrollRight}
                          sx={{
                            width: btnSize,
                            height: btnSize,
                            minWidth: btnSize,
                            minHeight: btnSize,
                            '&:hover': { bgcolor: 'action.hover' },
                          }}
                        >
                          <ChevronRight sx={{ fontSize: iconSize }} />
                        </IconButton>
                      </Tooltip>

                      {/* 재설정 */}
                      <Tooltip title="차트 재설정">
                        <IconButton
                          size="small"
                          onClick={handleResetChart}
                          sx={{
                            width: btnSize,
                            height: btnSize,
                            minWidth: btnSize,
                            minHeight: btnSize,
                            '&:hover': { bgcolor: 'action.hover' },
                          }}
                        >
                          <RestartAlt sx={{ fontSize: iconSize }} />
                        </IconButton>
                      </Tooltip>
                    </Box>
                  </Box>
                );
                })()}
              </Box>
            </SortableContext>
          </DndContext>
        )}

      </Box>
    );
  }
);

CandlestickChart.displayName = 'CandlestickChart';

// React.memo로 감싸서 불필요한 리렌더링 방지
export default React.memo(CandlestickChart, (prevProps, nextProps) => {
  // 데이터 길이와 마지막 항목을 비교하여 변경 여부 판단
  if (prevProps.data.length !== nextProps.data.length) {
    return false; // 길이가 다르면 리렌더링
  }
  
  // ✅ 매수/매도 마커 개수 변경 시 리렌더링 (백테스트 결과 반영)
  const prevBuyCount = prevProps.data?.filter((c: any) => c.isBuyPoint).length || 0;
  const nextBuyCount = nextProps.data?.filter((c: any) => c.isBuyPoint).length || 0;
  const prevSellCount = prevProps.data?.filter((c: any) => c.isSellPoint).length || 0;
  const nextSellCount = nextProps.data?.filter((c: any) => c.isSellPoint).length || 0;
  if (prevBuyCount !== nextBuyCount || prevSellCount !== nextSellCount) {
    return false; // 마커 변경 시 리렌더링
  }
  
  const prevLast = prevProps.data[prevProps.data.length - 1];
  const nextLast = nextProps.data[nextProps.data.length - 1];
  
  // 마지막 데이터의 time과 close 값이 같으면 리렌더링 스킵
  if (prevLast?.time !== nextLast?.time || prevLast?.close !== nextLast?.close) {
    return false; // 데이터가 변경되면 리렌더링
  }
  
  // indicatorData 길이 비교
  const prevIndLen = prevProps.indicatorData?.length || 0;
  const nextIndLen = nextProps.indicatorData?.length || 0;
  if (prevIndLen !== nextIndLen) {
    return false;
  }
  
  // externalHoverTime이 변경되면 리렌더링 (크로스헤어 동기화)
  if (prevProps.externalHoverTime !== nextProps.externalHoverTime) {
    return false;
  }
  
  // sharedTimeRange가 변경되면 리렌더링
  if (JSON.stringify(prevProps.sharedTimeRange) !== JSON.stringify(nextProps.sharedTimeRange)) {
    return false;
  }
  
  // height, isLoading 등 주요 props 변경 확인
  if (prevProps.height !== nextProps.height || 
      prevProps.isLoading !== nextProps.isLoading ||
      prevProps.showLeftButton !== nextProps.showLeftButton ||
      prevProps.showRightButton !== nextProps.showRightButton ||
      prevProps.activeTool !== nextProps.activeTool ||
      prevProps.chartViewMode !== nextProps.chartViewMode ||
      prevProps.compactToolbar !== nextProps.compactToolbar ||
      prevProps.hideNavigationToolbar !== nextProps.hideNavigationToolbar ||
      prevProps.hideTrendAlignmentBar !== nextProps.hideTrendAlignmentBar ||
      prevProps.hideSubPanelToolbar !== nextProps.hideSubPanelToolbar) {
    return false;
  }
  
  // maLines 비교
  if (JSON.stringify(prevProps.maLines) !== JSON.stringify(nextProps.maLines)) {
    return false;
  }
  
  // ichimokuSettings 비교
  if (JSON.stringify(prevProps.ichimokuSettings) !== JSON.stringify(nextProps.ichimokuSettings)) {
    return false;
  }

  // ✅ maCalculationData, allIndicatorData 변경 시 리렌더 (멀티차트 워밍업/지표 데이터 반영)
  const prevMaLen = prevProps.maCalculationData?.length ?? 0;
  const nextMaLen = nextProps.maCalculationData?.length ?? 0;
  if (prevMaLen !== nextMaLen) return false;
  const prevAllLen = prevProps.allIndicatorData?.length ?? 0;
  const nextAllLen = nextProps.allIndicatorData?.length ?? 0;
  if (prevAllLen !== nextAllLen) return false;

  // overlaySettingsOverride 비교
  if (JSON.stringify(prevProps.overlaySettingsOverride) !== JSON.stringify(nextProps.overlaySettingsOverride)) {
    return false;
  }
  
  // subPanels 비교
  if (JSON.stringify(prevProps.subPanels) !== JSON.stringify(nextProps.subPanels)) {
    return false;
  }
  
  // indicatorSettings 비교
  if (JSON.stringify(prevProps.indicatorSettings) !== JSON.stringify(nextProps.indicatorSettings)) {
    return false;
  }
  
  // ✅ colorVersion 비교 (색상 설정 변경)
  if (prevProps.colorVersion !== nextProps.colorVersion) {
    console.log(`🔄 [CandlestickChart memo] colorVersion 변경 감지: ${prevProps.colorVersion} → ${nextProps.colorVersion} [BUILD_V6]`);
    return false;
  }
  
  // ✅ settingsVersion 비교 (보조지표 설정 변경)
  if (prevProps.settingsVersion !== nextProps.settingsVersion) {
    console.log(`🔄 [CandlestickChart memo] settingsVersion 변경 감지: ${prevProps.settingsVersion} → ${nextProps.settingsVersion} [BUILD_V6]`);
    return false;
  }

  if (JSON.stringify(prevProps.alertMarkers ?? []) !== JSON.stringify(nextProps.alertMarkers ?? [])) {
    return false;
  }
  
  return true; // 모든 체크를 통과하면 리렌더링 스킵
});
