import React, { useRef, useEffect, useCallback, useState } from 'react';
import type { ChartManager } from '../utils/ChartManager';
import type { Drawing, DrawingPoint, DrawingToolId, Theme } from '../types';
import { formatUnixDateTime } from '../utils/timezone';

// ─── Hit-test ──────────────────────────────────────────────────────────────
const HIT_RADIUS = 7; // px (CSS 좌표)

export function distToSegment(
  px: number, py: number,
  x0: number, y0: number,
  x1: number, y1: number,
): number {
  const dx = x1 - x0, dy = y1 - y0;
  const lenSq = dx * dx + dy * dy;
  if (lenSq === 0) return Math.hypot(px - x0, py - y0);
  const t = Math.max(0, Math.min(1, ((px - x0) * dx + (py - y0) * dy) / lenSq));
  return Math.hypot(px - x0 - t * dx, py - y0 - t * dy);
}

/** 툴 alias 해소 - 저장된 드로잉의 type을 실제 렌더 type으로 변환 */
function resolveAlias(type: string): string {
  return TOOL_ALIAS[type] ?? type;
}

/** 캔버스 CSS 좌표 (cx, cy)가 주어진 Drawing 위에 있으면 true */
export function hitTestDrawing(
  cx: number, cy: number,
  d: Drawing,
  m: ChartManager,
  cssW: number,
  cssH: number,
): boolean {
  if (d.visible === false) return false;

  const effectiveType = resolveAlias(d.type);
  switch (effectiveType) {
    case 'hline': {
      if (!d.points[0]) return false;
      const y = m.priceToY(d.points[0].price);
      return y !== null && Math.abs(cy - y) <= HIT_RADIUS;
    }
    case 'vline': {
      if (!d.points[0]) return false;
      const x = m.timeToX(d.points[0].time);
      return x !== null && Math.abs(cx - x) <= HIT_RADIUS;
    }
    case 'trendline':
    case 'ray': {
      if (d.points.length < 2) return false;
      const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
      const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
      if (x0 === null || y0 === null || x1 === null || y1 === null) return false;
      const dx = x1 - x0, dy = y1 - y0;
      const extend = d.type === 'trendline' ? 'both' : 'right';
      let sx = x0, sy = y0, ex = x1, ey = y1;
      if (extend === 'right' || extend === 'both') {
        if (Math.abs(dx) > 0.001) {
          const t = (cssW - x0) / dx;
          if (t > 1) { ex = cssW; ey = y0 + dy * t; }
        }
      }
      if (extend === 'both') {
        if (Math.abs(dx) > 0.001) {
          const t = (0 - x0) / dx;
          if (t < 0) { sx = 0; sy = y0 + dy * t; }
        }
      }
      return distToSegment(cx, cy, sx, sy, ex, ey) <= HIT_RADIUS;
    }
    case 'fib': {
      if (d.points.length < 2) return false;
      const [p0, p1] = d.points;
      const range = p0.price - p1.price;
      for (const level of FIB_LEVELS) {
        const price = p0.price - range * level;
        const y = m.priceToY(price);
        if (y !== null && Math.abs(cy - y) <= HIT_RADIUS) return true;
      }
      return false;
    }
    case 'fibtz': {
      if (d.points.length < 1) return false;
      // 기준선(0) 히트 테스트
      const x0tz = m.timeToX(d.points[0].time);
      if (x0tz !== null && Math.abs(cx - x0tz) <= HIT_RADIUS) return true;
      if (d.points.length < 2) return false;
      // 각 피보나치 레벨 수직선 히트 테스트
      const dt = d.points[1].time - d.points[0].time;
      for (const level of FIB_TZ_LEVELS) {
        const xTZ = m.timeToX(d.points[0].time + dt * level);
        if (xTZ !== null && Math.abs(cx - xTZ) <= HIT_RADIUS) return true;
      }
      return false;
    }
    case 'rect': {
      if (d.points.length < 2) return false;
      const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
      const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
      if (x0 === null || y0 === null || x1 === null || y1 === null) return false;
      const lx = Math.min(x0, x1), rx = Math.max(x0, x1);
      const ty = Math.min(y0, y1), by = Math.max(y0, y1);
      // 내부 + 테두리 모두 hit
      return cx >= lx - HIT_RADIUS && cx <= rx + HIT_RADIUS
          && cy >= ty - HIT_RADIUS && cy <= by + HIT_RADIUS;
    }
    case 'measure': {
      if (d.points.length < 2) return false;
      const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
      const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
      if (x0 === null || y0 === null || x1 === null || y1 === null) return false;
      const lx = Math.min(x0, x1) - HIT_RADIUS, rx = Math.max(x0, x1) + HIT_RADIUS;
      const ty = Math.min(y0, y1) - HIT_RADIUS, by = Math.max(y0, y1) + HIT_RADIUS;
      return cx >= lx && cx <= rx && cy >= ty && cy <= by;
    }
    case 'channel': {
      // 첫 번째 선만 hit-test (main line)
      if (d.points.length < 2) return false;
      const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
      const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
      if (x0 === null || y0 === null || x1 === null || y1 === null) return false;
      const dx = x1 - x0, dy = y1 - y0;
      let sx = x0, sy = y0, ex = x1, ey = y1;
      if (Math.abs(dx) > 0.001) {
        const tR = (cssW - x0) / dx; if (tR > 1) { ex = cssW; ey = y0 + dy * tR; }
        const tL = -x0 / dx;        if (tL < 0)  { sx = 0;    sy = y0 + dy * tL; }
      }
      return distToSegment(cx, cy, sx, sy, ex, ey) <= HIT_RADIUS;
    }
    case 'pencil': {
      if (d.points.length < 2) return false;
      for (let i = 0; i < d.points.length - 1; i++) {
        const x0 = m.timeToX(d.points[i].time),     y0 = m.priceToY(d.points[i].price);
        const x1 = m.timeToX(d.points[i + 1].time), y1 = m.priceToY(d.points[i + 1].price);
        if (x0 !== null && y0 !== null && x1 !== null && y1 !== null) {
          if (distToSegment(cx, cy, x0, y0, x1, y1) <= HIT_RADIUS) return true;
        }
      }
      return false;
    }
    case 'text': {
      if (!d.points[0]) return false;
      const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
      if (x === null || y === null) return false;
      return cx >= x - 4 && cx <= x + 120 && cy >= y - 20 && cy <= y + 6;
    }
    case 'circle': {
      if (d.points.length < 2) return false;
      const cx0 = m.timeToX(d.points[0].time), cy0 = m.priceToY(d.points[0].price);
      const ex  = m.timeToX(d.points[1].time), ey  = m.priceToY(d.points[1].price);
      if (cx0 === null || cy0 === null || ex === null || ey === null) return false;
      const r = Math.hypot(ex - cx0, ey - cy0);
      const dist = Math.hypot(cx - cx0, cy - cy0);
      return Math.abs(dist - r) <= HIT_RADIUS;
    }
    case 'triangle': {
      if (d.points.length < 3) return false;
      for (let i = 0; i < 3; i++) {
        const a = d.points[i], b = d.points[(i + 1) % 3];
        const ax = m.timeToX(a.time), ay = m.priceToY(a.price);
        const bx = m.timeToX(b.time), by = m.priceToY(b.price);
        if (ax === null || ay === null || bx === null || by === null) continue;
        if (distToSegment(cx, cy, ax, ay, bx, by) <= HIT_RADIUS) return true;
      }
      return false;
    }
    case 'pitchfork': {
      if (d.points.length < 3) return false;
      const [p0, p1, p2] = d.points;
      const x0 = m.timeToX(p0.time), y0 = m.priceToY(p0.price);
      const x1 = m.timeToX(p1.time), y1 = m.priceToY(p1.price);
      const x2 = m.timeToX(p2.time), y2 = m.priceToY(p2.price);
      if (x0===null||y0===null||x1===null||y1===null||x2===null||y2===null) return false;
      const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
      if (distToSegment(cx, cy, x0, y0, mx, my) <= HIT_RADIUS) return true;
      if (distToSegment(cx, cy, x0+(x1-mx), y0+(y1-my), x1, y1) <= HIT_RADIUS) return true;
      if (distToSegment(cx, cy, x0+(x2-mx), y0+(y2-my), x2, y2) <= HIT_RADIUS) return true;
      return false;
    }
    case 'arrow_mark_up':
    case 'arrow_mark_down':
    case 'arrow_mark_left':
    case 'arrow_mark_right':
    case 'arrow_mark': {
      if (!d.points[0]) return false;
      const ax = m.timeToX(d.points[0].time), ay = m.priceToY(d.points[0].price);
      if (ax === null || ay === null) return false;
      return Math.hypot(cx - ax, cy - ay) <= 14;
    }
    case 'long_position':
    case 'short_position': {
      if (d.points.length < 2) return false;
      const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
      const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
      if (x0===null||y0===null||x1===null||y1===null) return false;
      return cx >= Math.min(x0,x1)-HIT_RADIUS && cx <= Math.max(x0,x1)+HIT_RADIUS
          && cy >= Math.min(y0,y1)-HIT_RADIUS && cy <= Math.max(y0,y1)+HIT_RADIUS;
    }
    // ── 신규 툴 히트 테스트 ──
    case 'cross_line': {
      if (!d.points[0]) return false;
      const xc = m.timeToX(d.points[0].time), yc = m.priceToY(d.points[0].price);
      if (xc === null || yc === null) return false;
      return Math.abs(cy - yc) <= HIT_RADIUS || Math.abs(cx - xc) <= HIT_RADIUS;
    }
    case 'hline_ray':
    case 'info_line':
    case 'guide_line': {
      if (!d.points[0]) return false;
      const xr = m.timeToX(d.points[0].time), yr = m.priceToY(d.points[0].price);
      if (xr === null || yr === null) return false;
      return Math.abs(cy - yr) <= HIT_RADIUS && cx >= xr - HIT_RADIUS;
    }
    case 'arrow': {
      if (d.points.length < 2) return false;
      const xa0 = m.timeToX(d.points[0].time), ya0 = m.priceToY(d.points[0].price);
      const xa1 = m.timeToX(d.points[1].time), ya1 = m.priceToY(d.points[1].price);
      if (xa0===null||ya0===null||xa1===null||ya1===null) return false;
      return distToSegment(cx, cy, xa0, ya0, xa1, ya1) <= HIT_RADIUS;
    }
    case 'trend_angle': {
      if (d.points.length < 2) return false;
      const xt0 = m.timeToX(d.points[0].time), yt0 = m.priceToY(d.points[0].price);
      const xt1 = m.timeToX(d.points[1].time), yt1 = m.priceToY(d.points[1].price);
      if (xt0===null||yt0===null||xt1===null||yt1===null) return false;
      return distToSegment(cx, cy, xt0, yt0, xt1, yt1) <= HIT_RADIUS;
    }
    case 'parallel_channel': {
      if (d.points.length < 2) return false;
      const xp0 = m.timeToX(d.points[0].time), yp0 = m.priceToY(d.points[0].price);
      const xp1 = m.timeToX(d.points[1].time), yp1 = m.priceToY(d.points[1].price);
      if (xp0===null||yp0===null||xp1===null||yp1===null) return false;
      return distToSegment(cx, cy, xp0, yp0, xp1, yp1) <= HIT_RADIUS;
    }
    case 'fib_ext':
    case 'fib_channel':
    case 'fib_speed_fan':
    case 'fib_circles':
    case 'fib_spiral':
    case 'fib_speed_arc': {
      if (d.points.length < 2) return false;
      const xfe0 = m.timeToX(d.points[0].time), yfe0 = m.priceToY(d.points[0].price);
      const xfe1 = m.timeToX(d.points[1].time), yfe1 = m.priceToY(d.points[1].price);
      if (xfe0===null||yfe0===null) return false;
      // 중심점 근처 또는 반지름 선 근처
      if (Math.hypot(cx - xfe0, cy - yfe0) <= HIT_RADIUS * 2) return true;
      if (xfe1!==null && yfe1!==null) {
        const r = Math.hypot(xfe1 - xfe0, yfe1 - yfe0);
        const distFromCenter = Math.hypot(cx - xfe0, cy - yfe0);
        return Math.abs(distFromCenter - r) <= HIT_RADIUS;
      }
      return false;
    }
    case 'gann_fan':
    case 'gann_box':
    case 'gann_square': {
      if (d.points.length < 2) return false;
      const xg0 = m.timeToX(d.points[0].time), yg0 = m.priceToY(d.points[0].price);
      const xg1 = m.timeToX(d.points[1].time), yg1 = m.priceToY(d.points[1].price);
      if (xg0===null||yg0===null||xg1===null||yg1===null) return false;
      const lgx = Math.min(xg0,xg1)-HIT_RADIUS, rgx = Math.max(xg0,xg1)+HIT_RADIUS;
      const tgy = Math.min(yg0,yg1)-HIT_RADIUS, bgy = Math.max(yg0,yg1)+HIT_RADIUS;
      return cx >= lgx && cx <= rgx && cy >= tgy && cy <= bgy;
    }
    case 'forecast': {
      if (d.points.length < 2) return false;
      const xf0 = m.timeToX(d.points[0].time), yf0 = m.priceToY(d.points[0].price);
      const xf1 = m.timeToX(d.points[1].time), yf1 = m.priceToY(d.points[1].price);
      if (xf0===null||yf0===null||xf1===null||yf1===null) return false;
      return cx >= Math.min(xf0,xf1)-HIT_RADIUS && cx <= Math.max(xf0,xf1)+HIT_RADIUS
          && cy >= Math.min(yf0,yf1)-HIT_RADIUS && cy <= Math.max(yf0,yf1)+HIT_RADIUS;
    }
    case 'date_range': {
      if (d.points.length < 2) return false;
      const xd0 = m.timeToX(d.points[0].time);
      const xd1 = m.timeToX(d.points[1].time);
      if (xd0===null||xd1===null) return false;
      return cx >= Math.min(xd0,xd1)-HIT_RADIUS && cx <= Math.max(xd0,xd1)+HIT_RADIUS;
    }
    case 'date_price_range': {
      if (d.points.length < 2) return false;
      const xdp0 = m.timeToX(d.points[0].time), ydp0 = m.priceToY(d.points[0].price);
      const xdp1 = m.timeToX(d.points[1].time), ydp1 = m.priceToY(d.points[1].price);
      if (xdp0===null||ydp0===null||xdp1===null||ydp1===null) return false;
      return cx >= Math.min(xdp0,xdp1)-HIT_RADIUS && cx <= Math.max(xdp0,xdp1)+HIT_RADIUS
          && cy >= Math.min(ydp0,ydp1)-HIT_RADIUS && cy <= Math.max(ydp0,ydp1)+HIT_RADIUS;
    }
    case 'data_window': {
      if (!d.points[0]) return false;
      const xdw = m.timeToX(d.points[0].time), ydw = m.priceToY(d.points[0].price);
      if (xdw===null||ydw===null) return false;
      return cx >= xdw - HIT_RADIUS && cx <= xdw + 180 && Math.abs(cy - ydw) <= 50;
    }
    case 'magnifier': {
      if (!d.points[0]) return false;
      const xmg = m.timeToX(d.points[0].time), ymg = m.priceToY(d.points[0].price);
      if (xmg===null||ymg===null) return false;
      let rmg = 40;
      if (d.points.length >= 2) {
        const xmg1 = m.timeToX(d.points[1].time), ymg1 = m.priceToY(d.points[1].price);
        if (xmg1!==null && ymg1!==null) rmg = Math.max(20, Math.hypot(xmg1-xmg, ymg1-ymg));
      }
      return Math.hypot(cx - xmg, cy - ymg) <= rmg + HIT_RADIUS;
    }
    case 'callout': {
      if (!d.points[0]) return false;
      const xca = m.timeToX(d.points[0].time), yca = m.priceToY(d.points[0].price);
      if (xca===null||yca===null) return false;
      return cx >= xca - 30 && cx <= xca + 150 && cy >= yca - 60 && cy <= yca + 10;
    }
    case 'pin_mark':
    case 'flag_mark': {
      if (!d.points[0]) return false;
      const xpm = m.timeToX(d.points[0].time), ypm = m.priceToY(d.points[0].price);
      if (xpm===null||ypm===null) return false;
      return Math.hypot(cx - xpm, cy - ypm) <= 20;
    }
    case 'note_text': {
      if (!d.points[0]) return false;
      const xnt = m.timeToX(d.points[0].time), ynt = m.priceToY(d.points[0].price);
      if (xnt===null||ynt===null) return false;
      return cx >= xnt && cx <= xnt + 140 && cy >= ynt && cy <= ynt + 60;
    }
    case 'price_note':
    case 'price_label': {
      if (!d.points[0]) return false;
      const xpn = m.timeToX(d.points[0].time), ypn = m.priceToY(d.points[0].price);
      if (xpn===null||ypn===null) return false;
      return Math.hypot(cx - xpn, cy - ypn) <= HIT_RADIUS * 3;
    }
    case 'comment_mark': {
      if (!d.points[0]) return false;
      const xcm = m.timeToX(d.points[0].time), ycm = m.priceToY(d.points[0].price);
      if (xcm===null||ycm===null) return false;
      return cx >= xcm - 40 && cx <= xcm + 40 && cy >= ycm - 50 && cy <= ycm + 10;
    }
    case 'fixed_text': {
      if (!d.points[0]) return false;
      const xft = m.timeToX(d.points[0].time), yft = m.priceToY(d.points[0].price);
      if (xft===null||yft===null) return false;
      return cx >= xft && cx <= xft + 140 && Math.abs(cy - yft) <= 12;
    }
    case 'pitchfork_schiff':
    case 'pitchfork_inside': {
      if (d.points.length < 3) return false;
      const [ps0, ps1, ps2] = d.points;
      const xps0 = m.timeToX(ps0.time), yps0 = m.priceToY(ps0.price);
      const xps1 = m.timeToX(ps1.time), yps1 = m.priceToY(ps1.price);
      const xps2 = m.timeToX(ps2.time), yps2 = m.priceToY(ps2.price);
      if (xps0===null||yps0===null||xps1===null||yps1===null||xps2===null||yps2===null) return false;
      if (distToSegment(cx, cy, xps0, yps0, (xps1+xps2)/2, (yps1+yps2)/2) <= HIT_RADIUS) return true;
      if (distToSegment(cx, cy, xps0, yps0, xps1, yps1) <= HIT_RADIUS) return true;
      return distToSegment(cx, cy, xps0, yps0, xps2, yps2) <= HIT_RADIUS;
    }
    case 'ellipse': {
      if (d.points.length < 2) return false;
      const xel = m.timeToX(d.points[0].time), yel = m.priceToY(d.points[0].price);
      const xel1 = m.timeToX(d.points[1].time), yel1 = m.priceToY(d.points[1].price);
      if (xel===null||yel===null||xel1===null||yel1===null) return false;
      const rx = Math.abs(xel1-xel), ry = Math.abs(yel1-yel);
      if (rx < 1 || ry < 1) return false;
      const norm = Math.hypot((cx-xel)/rx, (cy-yel)/ry);
      return Math.abs(norm - 1) <= HIT_RADIUS / Math.min(rx, ry);
    }
    case 'arc': {
      if (!d.points[0]) return false;
      const xar = m.timeToX(d.points[0].time), yar = m.priceToY(d.points[0].price);
      if (xar===null||yar===null) return false;
      return Math.hypot(cx - xar, cy - yar) <= HIT_RADIUS * 2;
    }
    case 'rotated_rect':
    case 'parallelogram': {
      if (d.points.length < 2) return false;
      // 경계 박스로 근사 히트테스트
      const pts2d = d.points.map(p => ({x:m.timeToX(p.time), y:m.priceToY(p.price)}));
      for (let i = 0; i < pts2d.length - 1; i++) {
        const {x:ax,y:ay} = pts2d[i], {x:bx,y:by} = pts2d[i+1];
        if (ax!==null&&ay!==null&&bx!==null&&by!==null) {
          if (distToSegment(cx,cy,ax,ay,bx,by) <= HIT_RADIUS) return true;
        }
      }
      // 마지막 변 (마지막→첫 번째)
      const first = pts2d[0], last = pts2d[pts2d.length-1];
      if (first.x!==null&&first.y!==null&&last.x!==null&&last.y!==null) {
        if (distToSegment(cx,cy,last.x,last.y,first.x,first.y) <= HIT_RADIUS) return true;
      }
      return false;
    }
    case 'polyline': {
      for (let i = 0; i < d.points.length - 1; i++) {
        const xpl0=m.timeToX(d.points[i].time),   ypl0=m.priceToY(d.points[i].price);
        const xpl1=m.timeToX(d.points[i+1].time), ypl1=m.priceToY(d.points[i+1].price);
        if (xpl0!==null&&ypl0!==null&&xpl1!==null&&ypl1!==null) {
          if (distToSegment(cx,cy,xpl0,ypl0,xpl1,ypl1) <= HIT_RADIUS) return true;
        }
      }
      return false;
    }
    // ── 패턴 (하모닉/엘리엇) ── 임의 선분 클릭
    case 'xabcd': case 'cypher': case 'abcd':
    case 'head_shoulders': case 'wedge_pattern': case 'three_drives':
    case 'elliott_impulse': case 'elliott_correction':
    case 'elliott_triangle_wave': case 'elliott_double_combo': {
      for (let i = 0; i < d.points.length - 1; i++) {
        const xp0 = m.timeToX(d.points[i].time),   yp0 = m.priceToY(d.points[i].price);
        const xp1 = m.timeToX(d.points[i+1].time), yp1 = m.priceToY(d.points[i+1].price);
        if (xp0!==null&&yp0!==null&&xp1!==null&&yp1!==null) {
          if (distToSegment(cx, cy, xp0, yp0, xp1, yp1) <= HIT_RADIUS) return true;
        }
      }
      return false;
    }
    // ── 프로젝션 도구 ──
    case 'ghost_feed': case 'bar_pattern': case 'fixed_volume_profile': case 'forecast': {
      if (d.points.length < 2) return false;
      const xpr0 = m.timeToX(d.points[0].time), ypr0 = m.priceToY(d.points[0].price);
      const xpr1 = m.timeToX(d.points[1].time), ypr1 = m.priceToY(d.points[1].price);
      if (xpr0===null||ypr0===null||xpr1===null||ypr1===null) return false;
      return cx >= Math.min(xpr0,xpr1)-HIT_RADIUS && cx <= Math.max(xpr0,xpr1)+HIT_RADIUS
          && cy >= Math.min(ypr0,ypr1)-HIT_RADIUS && cy <= Math.max(ypr0,ypr1)+HIT_RADIUS;
    }
    case 'anchored_vwap': {
      if (!d.points[0]) return false;
      const xav = m.timeToX(d.points[0].time), yav = m.priceToY(d.points[0].price);
      if (xav===null||yav===null) return false;
      return Math.abs(cy - yav) <= HIT_RADIUS && cx >= xav - HIT_RADIUS;
    }
    default: return false;
  }
}

// ─── Tool config ───────────────────────────────────────────────────────────

// 신규 툴 → 기존 툴 렌더/히트테스트 위임 매핑
// (독립 렌더러가 구현된 툴은 여기에 없음)
const TOOL_ALIAS: Record<string, string> = {
  // 선 계열
  extended_line:          'trendline',  // 양방향 확장선 (trendline과 동일)
  fib_trend_time:         'fib',    // 추세기반 피보나치 시간 → 기본 fib 레벨
  fib_wedge:              'fib',    // 피보나치 웻지 → 기본 fib 레벨
  // fib_circles, fib_spiral, fib_speed_arc 는 독립 렌더러로 구현됨
  disjoint_channel:       'channel',
  flat_top_bottom:        'rect',
  // 텍스트 기반 (자체 렌더러 없는 것만 alias 유지)
  table_tool:             'text',   // 테이블은 단순 텍스트 박스로 처리
  // note_text / price_note / comment_mark / fixed_text / price_label → 자체 렌더러
  // 화살표 마크 (arrow_mark / anchored_arrow → 화살촉 있는 arrow 렌더)
  arrow_mark:             'arrow',
  anchored_arrow:         'arrow',
  arrow_mark_left:        'arrow_mark_left',
  arrow_mark_right:       'arrow_mark_right',
  signpost_up:            'arrow_mark_up',
  signpost_down:          'arrow_mark_down',
  // 기타
  highlight:              'pencil',
  price_range:            'measure',
  // date_price_range / data_window → 자체 렌더러
  // magnifier → 자체 렌더러
  bar_pattern:            'rect',
  ghost_feed:             'rect',
  projection_tool:        'trendline',
  anchored_vwap:          'hline',
  fixed_volume_profile:   'rect',
};

// 실제 드로잉을 생성하지 않는 스텁 툴 (UI 선택만)
const STUB_TOOLS = new Set<string>([
  // 현재 모든 툴 구현 완료
]);

const TOOL_COLORS: Record<string, string> = {
  hline:            '#FF6D00',
  vline:            '#2196F3',
  trendline:        '#4CAF50',
  extended_line:    '#4CAF50',
  ray:              '#9C27B0',
  arrow:            '#9C27B0',
  trend_angle:      '#4CAF50',
  cross_line:       '#2196F3',
  hline_ray:        '#FF6D00',
  info_line:        '#FF6D00',
  guide_line:       '#78909C',
  fib:              '#FFC107',
  fib_ext:          '#FF9800',
  fib_channel:      '#FFC107',
  fib_speed_fan:    '#FFB300',
  measure:          '#00BCD4',
  date_range:       '#00BCD4',
  forecast:         '#78909C',
  text:             '#FFEB3B',
  callout:          '#FFEB3B',
  pin_mark:         '#F06292',
  flag_mark:        '#EF5350',
  channel:          '#E91E63',
  parallel_channel: '#E91E63',
  rect:             '#607D8B',
  gann_box:         '#8BC34A',
  gann_square:      '#8BC34A',
  gann_fan:         '#CDDC39',
  pencil:           '#FF5722',
  circle:           '#26C6DA',
  ellipse:          '#26C6DA',
  triangle:         '#AB47BC',
  arc:              '#AB47BC',
  pitchfork:        '#EF5350',
  pitchfork_schiff: '#FF7043',
  pitchfork_inside: '#FF8A65',
  arrow_mark_up:    '#4CAF50',
  arrow_mark_down:  '#EF5350',
  arrow_mark_left:  '#FFC107',
  arrow_mark_right: '#FFC107',
  arrow_mark:       '#9C27B0',
  long_position:    '#4CAF50',
  short_position:   '#EF5350',
  highlight:        '#FFEB3B',
  rotated_rect:     '#AB47BC',
  parallelogram:    '#CE93D8',
  polyline:         '#FF9800',
  note_text:        '#F9A825',
  price_note:       '#26C6DA',
  comment_mark:     '#78909C',
  fixed_text:       '#AED6F1',
  price_label:      '#EF5350',
  date_price_range: '#00BCD4',
  data_window:      '#42A5F5',
  magnifier:        '#80DEEA',
};

const POINTS_NEEDED: Record<string, number> = {
  // ── 1점 ──
  hline: 1, vline: 1, text: 1, callout: 1,
  arrow_mark_up: 1, arrow_mark_down: 1, arrow_mark_left: 1, arrow_mark_right: 1,
  arrow_mark: 1, flag_mark: 1, pin_mark: 1,
  cross_line: 1, hline_ray: 1, info_line: 1, guide_line: 1,
  // 텍스트 별칭 (1점)
  fixed_text: 1, note_text: 1, price_note: 1, price_label: 1,
  comment_mark: 1, table_tool: 1,
  // ── 2점 ──
  trendline: 2, extended_line: 2, ray: 2, arrow: 2, trend_angle: 2,
  anchored_arrow: 2, projection_tool: 2,
  fib: 2, fib_ext: 2, fib_channel: 2, fib_speed_fan: 2,
  fib_trend_time: 2, fib_circles: 2, fib_spiral: 2,
  fib_speed_arc: 2, fib_wedge: 2,
  fibtz: 2, measure: 2, price_range: 2,
  rect: 2, flat_top_bottom: 2,
  channel: 2, disjoint_channel: 2,
  circle: 2, ellipse: 2, long_position: 2, short_position: 2,
  gann_fan: 2, gann_box: 2, gann_square: 2,
  forecast: 2, date_range: 2, date_price_range: 2, data_window: 1, magnifier: 2,
  bar_pattern: 2, ghost_feed: 2, fixed_volume_profile: 2,
  anchored_vwap: 1,
  // ── 3점 ──
  triangle: 3, pitchfork: 3, pitchfork_schiff: 3, pitchfork_inside: 3,
  parallel_channel: 3, arc: 3,
  rotated_rect: 3, parallelogram: 3,
  // ── 다각형 (더블클릭으로 완성, 사실상 무한) ──
  polyline: 999,
  // ── 패턴 스텁 (포인트 수집용) ──
  xabcd: 5, cypher: 5, abcd: 4, head_shoulders: 7, wedge_pattern: 5, three_drives: 6,
  elliott_impulse: 6, elliott_correction: 4, elliott_triangle_wave: 6, elliott_double_combo: 4,
  // pencil, zoom, highlight : drag 기반이므로 별도 처리
};

const FIB_LEVELS  = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.272, 1.618];
const FIB_COLORS  = ['#EF5350','#FF9800','#FFC107','#FFFFFF','#4CAF50','#2196F3','#9C27B0','#607D8B','#607D8B'];

// 피보나치 확장 레벨
const FIB_EXT_LEVELS = [0, 0.236, 0.382, 0.618, 0.786, 1.0, 1.272, 1.618, 2.0, 2.618];
const FIB_EXT_COLORS = ['#EF5350','#FF9800','#FFC107','#4CAF50','#26C6DA','#FFFFFF','#9C27B0','#E91E63','#F44336','#00BCD4'];

// 갠 팬 비율
const GANN_RATIOS = [8, 4, 3, 2, 1, 0.5, 0.333, 0.25, 0.125];
const GANN_LABELS = ['8:1','4:1','3:1','2:1','1:1','1:2','1:3','1:4','1:8'];
const GANN_COLORS_ARR = ['#EF5350','#FF9800','#FFC107','#8BC34A','#FFFFFF','#8BC34A','#FFC107','#FF9800','#EF5350'];

// 피보나치 타임존 수열 (캔들 간격 배수)
const FIB_TZ_LEVELS = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89];
const FIB_TZ_COLORS = [
  '#2196F3','#4CAF50','#FF9800','#E91E63','#9C27B0',
  '#00BCD4','#8BC34A','#FF5722','#607D8B','#795548',
];

// ─── Renderers ────────────────────────────────────────────────────────────
function setLineStyle(ctx: CanvasRenderingContext2D, style?: string) {
  if (style === 'dashed')  ctx.setLineDash([6, 4]);
  else if (style === 'dotted') ctx.setLineDash([2, 4]);
  else ctx.setLineDash([]);
}

function renderHLine(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (!d.points[0]) return;
  const y = m.priceToY(d.points[0].price);
  if (y === null || y < 0) return;
  setLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1;
  ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssW, y); ctx.stroke();
  const lbl = d.points[0].price.toFixed(2);
  ctx.font = '11px monospace';
  const tw = ctx.measureText(lbl).width;
  ctx.fillStyle = d.color + 'cc';
  ctx.fillRect(cssW - tw - 10, y - 10, tw + 8, 14);
  ctx.fillStyle = '#fff';
  ctx.fillText(lbl, cssW - tw - 6, y + 1);
}

function renderVLine(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssH: number) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time);
  if (x === null || x < 0) return;
  setLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1;
  ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, cssH); ctx.stroke();
}

function renderLineOrRay(
  ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager,
  extend: 'none' | 'both' | 'right', cssW: number, cssH: number,
) {
  if (d.points.length < 1) return;
  const p0 = d.points[0];
  const x0 = m.timeToX(p0.time); const y0 = m.priceToY(p0.price);
  if (x0 === null || y0 === null) return;

  if (d.points.length < 2) {
    ctx.beginPath(); ctx.arc(x0, y0, 4, 0, Math.PI * 2);
    ctx.fillStyle = d.color; ctx.fill(); return;
  }

  const p1 = d.points[1];
  const x1 = m.timeToX(p1.time); const y1 = m.priceToY(p1.price);
  if (x1 === null || y1 === null) return;

  setLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1;

  let sx = x0, sy = y0, ex = x1, ey = y1;
  const dx = x1 - x0, dy = y1 - y0;

  if (extend === 'right' || extend === 'both') {
    if (Math.abs(dx) > 0.001) {
      const t = (cssW - x0) / dx;
      if (t > 1) { ex = cssW; ey = y0 + dy * t; }
    }
  }
  if (extend === 'both') {
    if (Math.abs(dx) > 0.001) {
      const t = (0 - x0) / dx;
      if (t < 0) { sx = 0; sy = y0 + dy * t; }
    }
  }

  ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(ex, ey); ctx.stroke();

  const endPrice = m.yToPrice(ey);
  if (endPrice !== null) {
    const lbl = endPrice.toFixed(2);
    ctx.font = '10px monospace';
    const tw = ctx.measureText(lbl).width;
    ctx.fillStyle = d.color + 'cc';
    ctx.fillRect(ex - tw - 8, ey - 10, tw + 6, 13);
    ctx.fillStyle = '#fff';
    ctx.fillText(lbl, ex - tw - 5, ey + 1);
  }
}

function renderFib(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (d.points.length < 2) {
    renderLineOrRay(ctx, d, m, 'none', cssW, 0); return;
  }
  const [p0, p1] = d.points;
  const range = p0.price - p1.price;

  for (let i = 0; i < FIB_LEVELS.length; i++) {
    const price = p0.price - range * FIB_LEVELS[i];
    const y = m.priceToY(price);
    if (y === null) continue;
    const color = FIB_COLORS[i] ?? '#607D8B';
    ctx.strokeStyle = color;
    ctx.lineWidth = (i === 0 || i === 6) ? 1.5 : 0.8;
    ctx.setLineDash(i === 3 ? [6, 4] : [3, 3]);
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssW, y); ctx.stroke();

    const pct = (FIB_LEVELS[i] * 100).toFixed(1);
    const lbl = `${pct}%  ${price.toFixed(2)}`;
    ctx.font = '10px monospace';
    const tw = ctx.measureText(lbl).width;
    ctx.fillStyle = color + 'cc';
    ctx.fillRect(2, y - 12, tw + 6, 14);
    ctx.fillStyle = '#fff';
    ctx.fillText(lbl, 5, y - 1);
  }
}

function applyLineStyle(ctx: CanvasRenderingContext2D, style?: string) {
  if (style === 'dashed')      ctx.setLineDash([6, 4]);
  else if (style === 'dotted') ctx.setLineDash([2, 3]);
  else                         ctx.setLineDash([]);
}

function renderFibTZ(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssH: number) {
  if (!d.points[0]) return;

  const x0 = m.timeToX(d.points[0].time);
  if (x0 === null) return;

  // settings 가 있으면 적용, 없으면 기본값 사용
  const settings = d.fibtzSettings;
  const bl = settings?.baseline;

  ctx.save();
  ctx.textBaseline = 'top';
  ctx.font = 'bold 10px monospace';

  // 기준선 (0)
  ctx.strokeStyle = bl?.color ?? d.color;
  ctx.lineWidth   = bl?.lineWidth ?? (d.lineWidth ?? 1.5);
  applyLineStyle(ctx, bl?.style ?? 'solid');
  ctx.beginPath(); ctx.moveTo(x0, 0); ctx.lineTo(x0, cssH); ctx.stroke();
  ctx.setLineDash([]);
  ctx.fillStyle = bl?.color ?? d.color;
  ctx.fillText('0', x0 + 3, 4);

  if (d.points.length >= 2) {
    // 두 기준점 사이의 시간 간격 = 1단위
    const dt = d.points[1].time - d.points[0].time;
    const lvList = settings?.levels ?? FIB_TZ_LEVELS.map((v, i) => ({
      value:     v,
      enabled:   true,
      color:     FIB_TZ_COLORS[i % FIB_TZ_COLORS.length],
      lineWidth: 1,
      style:     'dashed' as const,
    }));

    for (const lv of lvList) {
      if (!lv.enabled) continue;

      const zoneTime = d.points[0].time + dt * lv.value;
      const xTZ = m.timeToX(zoneTime);
      if (xTZ === null) continue;

      ctx.strokeStyle  = lv.color;
      ctx.lineWidth    = lv.lineWidth;
      ctx.globalAlpha  = 0.85;
      applyLineStyle(ctx, lv.style);
      ctx.beginPath(); ctx.moveTo(xTZ, 0); ctx.lineTo(xTZ, cssH); ctx.stroke();

      ctx.globalAlpha = 1;
      ctx.setLineDash([]);
      ctx.fillStyle = lv.color;
      ctx.fillText(String(lv.value), xTZ + 3, 4);
    }
    ctx.globalAlpha = 1;
  }
  ctx.restore();
}

function renderMeasure(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, theme: Theme) {
  if (d.points.length < 2) {
    const p = d.points[0]; if (!p) return;
    const x = m.timeToX(p.time), y = m.priceToY(p.price);
    if (x !== null && y !== null) { ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI*2); ctx.fillStyle = '#00BCD4'; ctx.fill(); }
    return;
  }
  const [p0, p1] = d.points;
  const x0 = m.timeToX(p0.time), y0 = m.priceToY(p0.price);
  const x1 = m.timeToX(p1.time), y1 = m.priceToY(p1.price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;

  const lx = Math.min(x0, x1), rx = Math.max(x0, x1);
  const ty = Math.min(y0, y1), by = Math.max(y0, y1);

  ctx.setLineDash([]); ctx.strokeStyle = '#00BCD4'; ctx.lineWidth = 1;
  ctx.fillStyle = '#00BCD41a';
  ctx.fillRect(lx, ty, rx - lx, by - ty);
  ctx.strokeRect(lx, ty, rx - lx, by - ty);

  const pct  = ((p1.price - p0.price) / p0.price * 100);
  const sign = pct >= 0 ? '+' : '';
  const cx = (lx + rx) / 2;
  const cy = (ty + by) / 2;

  ctx.font = 'bold 13px monospace';
  ctx.fillStyle = pct >= 0 ? '#4CAF50' : '#EF5350';
  ctx.textAlign = 'center';
  ctx.fillText(`${sign}${pct.toFixed(2)}%`, cx, cy);
  ctx.font = '11px monospace';
  ctx.fillStyle = theme === 'dark' ? '#80DEEA' : '#006064';
  ctx.fillText(`Δ ${Math.abs(p1.price - p0.price).toFixed(2)}`, cx, cy + 15);
  ctx.textAlign = 'left';
}

function renderRect(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) { renderLineOrRay(ctx, d, m, 'none', 0, 0); return; }
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  setLineStyle(ctx, d.style); ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1;
  ctx.fillStyle = d.color + '22';
  const lx = Math.min(x0,x1), ty = Math.min(y0,y1), w = Math.abs(x1-x0), h = Math.abs(y1-y0);
  ctx.fillRect(lx, ty, w, h);
  ctx.strokeRect(lx, ty, w, h);
}

function renderChannel(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  // 첫 번째 선 (추세선)
  renderLineOrRay(ctx, d, m, 'both', cssW, cssH);
  if (d.points.length >= 2) {
    // 3번째 점이 있으면 그 점의 Y 기준으로 평행선, 없으면 기울기 동일한 평행선 자동 생성
    let parallelPoints: DrawingPoint[];
    if (d.points.length >= 3) {
      const [p0, p1, p2] = d.points;
      // p2 에서 같은 기울기로 평행선 생성
      const priceDiff = p1.price - p0.price;
      const timeDiff  = p1.time  - p0.time;
      const endTime   = timeDiff !== 0 ? p2.time + timeDiff : p2.time;
      const endPrice  = timeDiff !== 0
        ? p2.price + priceDiff
        : p2.price;
      parallelPoints = [p2, { time: endTime, price: endPrice }];
    } else {
      // 2점만 있을 때: 가격 차이를 기준으로 자동 평행선
      const [p0, p1] = d.points;
      const dy = p1.price - p0.price;
      const offset = Math.abs(dy) * 0.618 || (p0.price * 0.01);
      parallelPoints = [
        { time: p0.time, price: p0.price - offset },
        { time: p1.time, price: p1.price - offset },
      ];
    }
    renderLineOrRay(
      ctx,
      { ...d, style: 'dashed', points: parallelPoints },
      m, 'both', cssW, cssH,
    );
    // 채널 사이 영역 음영
    if (d.points.length >= 2) {
      const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
      const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
      const px0 = m.timeToX(parallelPoints[0].time), py0 = m.priceToY(parallelPoints[0].price);
      const px1 = m.timeToX(parallelPoints[1].time), py1 = m.priceToY(parallelPoints[1].price);
      if (x0!==null && y0!==null && x1!==null && y1!==null && px0!==null && py0!==null && px1!==null && py1!==null) {
        ctx.beginPath();
        ctx.moveTo(x0, y0); ctx.lineTo(x1, y1);
        ctx.lineTo(px1, py1); ctx.lineTo(px0, py0);
        ctx.closePath();
        ctx.fillStyle = d.color + '18';
        ctx.fill();
      }
    }
  }
}

function renderPencil(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const isHighlight = d.type === 'highlight';
  setLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color;
  ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';
  if (isHighlight) ctx.globalAlpha = 0.35;
  ctx.beginPath();
  for (let i = 0; i < d.points.length; i++) {
    const x = m.timeToX(d.points[i].time);
    const y = m.priceToY(d.points[i].price);
    if (x === null || y === null) continue;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.stroke();
}

function renderText(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0] || !d.text) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  ctx.font = 'bold 13px sans-serif';
  ctx.fillStyle = d.color;
  ctx.fillText(d.text, x, y);
}

function renderCircle(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const cx = m.timeToX(d.points[0].time), cy = m.priceToY(d.points[0].price);
  const ex = m.timeToX(d.points[1].time), ey = m.priceToY(d.points[1].price);
  if (cx===null||cy===null||ex===null||ey===null) return;
  const r = Math.hypot(ex - cx, ey - cy);
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color;
  ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.beginPath();
  ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.fillStyle = d.color + '22';
  ctx.fill();
  ctx.stroke();
}

function renderTriangle(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 3) return;
  const pts = d.points.slice(0, 3).map(p => ({ x: m.timeToX(p.time), y: m.priceToY(p.price) }));
  if (pts.some(p => p.x === null || p.y === null)) return;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color;
  ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.beginPath();
  ctx.moveTo(pts[0].x!, pts[0].y!);
  ctx.lineTo(pts[1].x!, pts[1].y!);
  ctx.lineTo(pts[2].x!, pts[2].y!);
  ctx.closePath();
  ctx.fillStyle = d.color + '22';
  ctx.fill();
  ctx.stroke();
}

function renderPitchfork(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (d.points.length < 3) return;
  const [p0, p1, p2] = d.points;
  const x0 = m.timeToX(p0.time), y0 = m.priceToY(p0.price);
  const x1 = m.timeToX(p1.time), y1 = m.priceToY(p1.price);
  const x2 = m.timeToX(p2.time), y2 = m.priceToY(p2.price);
  if (x0===null||y0===null||x1===null||y1===null||x2===null||y2===null) return;
  const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color;
  ctx.lineWidth = d.lineWidth ?? 1.5;
  // 핸들: p0 → 중간점
  const drawExtendedLine = (sx: number, sy: number, ex: number, ey: number) => {
    const dx = ex - sx, dy = ey - sy;
    const len = Math.hypot(dx, dy);
    if (len < 0.001) return;
    const t = Math.max(cssW, cssH) * 2 / len;
    ctx.beginPath();
    ctx.moveTo(sx, sy);
    ctx.lineTo(sx + dx * t, sy + dy * t);
    ctx.stroke();
  };
  // 핸들선
  ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(mx, my); ctx.stroke();
  // 중간 포크
  drawExtendedLine(mx, my, mx + (mx - x0), my + (my - y0));
  // 좌우 포크
  const offX = x1 - mx, offY = y1 - my;
  drawExtendedLine(x0 + offX, y0 + offY, x1, y1);
  drawExtendedLine(x0 - offX, y0 - offY, x2, y2);
  // 포인트 표시
  [[x0,y0],[x1,y1],[x2,y2]].forEach(([px,py]) => {
    ctx.beginPath(); ctx.arc(px, py, 4, 0, Math.PI*2);
    ctx.fillStyle = d.color; ctx.fill();
  });
}

function renderArrowMark(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, dir: 'up'|'down'|'left'|'right') {
  if (!d.points[0]) return;
  const cx = m.timeToX(d.points[0].time), cy = m.priceToY(d.points[0].price);
  if (cx===null||cy===null) return;
  const s = 10;
  ctx.fillStyle = d.color;
  ctx.beginPath();
  switch (dir) {
    case 'up':
      ctx.moveTo(cx, cy - 20); ctx.lineTo(cx - s, cy - 10); ctx.lineTo(cx + s, cy - 10); break;
    case 'down':
      ctx.moveTo(cx, cy + 20); ctx.lineTo(cx - s, cy + 10); ctx.lineTo(cx + s, cy + 10); break;
    case 'left':
      ctx.moveTo(cx - 20, cy); ctx.lineTo(cx - 10, cy - s); ctx.lineTo(cx - 10, cy + s); break;
    case 'right':
      ctx.moveTo(cx + 20, cy); ctx.lineTo(cx + 10, cy - s); ctx.lineTo(cx + 10, cy + s); break;
  }
  ctx.closePath();
  ctx.fill();
}

function renderLongShort(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, isLong: boolean) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), entry = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), target = m.priceToY(d.points[1].price);
  if (x0===null||entry===null||x1===null||target===null) return;
  const lx = Math.min(x0, x1), rx = Math.max(x0, x1);
  // 손절선: entry에서 target 반대방향으로 같은 거리의 1/3
  const diff = target - entry;
  const stop = entry - diff * 0.33;
  const profitColor = isLong ? 'rgba(76,175,80,0.25)' : 'rgba(239,83,80,0.25)';
  const lossColor   = isLong ? 'rgba(239,83,80,0.18)' : 'rgba(76,175,80,0.18)';
  const lineColor   = isLong ? '#4CAF50' : '#EF5350';

  // 수익 구간
  ctx.fillStyle = profitColor;
  ctx.fillRect(lx, Math.min(entry, target), rx - lx, Math.abs(target - entry));
  // 손실 구간
  ctx.fillStyle = lossColor;
  ctx.fillRect(lx, Math.min(entry, stop), rx - lx, Math.abs(stop - entry));

  // 진입선
  ctx.strokeStyle = lineColor;
  ctx.lineWidth = 1.5;
  ctx.setLineDash([]);
  ctx.beginPath(); ctx.moveTo(lx, entry); ctx.lineTo(rx, entry); ctx.stroke();
  // 타겟선
  ctx.strokeStyle = isLong ? '#4CAF50' : '#EF5350';
  ctx.setLineDash([4, 3]);
  ctx.beginPath(); ctx.moveTo(lx, target); ctx.lineTo(rx, target); ctx.stroke();
  // 손절선
  ctx.strokeStyle = isLong ? '#EF5350' : '#4CAF50';
  ctx.beginPath(); ctx.moveTo(lx, stop); ctx.lineTo(rx, stop); ctx.stroke();
  ctx.setLineDash([]);

  // 레이블
  const tag = (label: string, y: number, fg: string) => {
    ctx.font = 'bold 11px sans-serif';
    const tw = ctx.measureText(label).width + 8;
    ctx.fillStyle = fg;
    ctx.fillRect(rx - tw - 2, y - 9, tw, 16);
    ctx.fillStyle = '#fff';
    ctx.fillText(label, rx - tw, y + 4);
  };
  tag(isLong ? '목표' : '손절', target, isLong ? '#4CAF50' : '#EF5350');
  tag('진입', entry, '#607D8B');
  tag(isLong ? '손절' : '목표', stop, isLong ? '#EF5350' : '#4CAF50');
}

// ── 신규 렌더 함수들 ────────────────────────────────────────────────────────

function renderCrossLine(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1;
  ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssW, y); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, cssH); ctx.stroke();
}

function renderHlineRay(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1;
  ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(cssW, y); ctx.stroke();
  ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2);
  ctx.fillStyle = d.color; ctx.fill();
}

function renderArrowLine(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const dx = x1 - x0, dy = y1 - y0;
  let ex = x1, ey = y1;
  if (Math.abs(dx) > 0.001) {
    const t = (cssW - x0) / dx;
    if (t > 1) { ex = cssW; ey = y0 + dy * t; }
  }
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(ex, ey); ctx.stroke();
  const angle = Math.atan2(ey - y0, ex - x0);
  const hs = 12;
  ctx.fillStyle = d.color; ctx.setLineDash([]);
  ctx.beginPath();
  ctx.moveTo(ex, ey);
  ctx.lineTo(ex - hs * Math.cos(angle - 0.45), ey - hs * Math.sin(angle - 0.45));
  ctx.lineTo(ex - hs * Math.cos(angle + 0.45), ey - hs * Math.sin(angle + 0.45));
  ctx.closePath(); ctx.fill();
}

function renderTrendAngle(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (d.points.length < 2) return;
  renderLineOrRay(ctx, d, m, 'both', cssW, cssH);
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const lineAngle = Math.atan2(y1 - y0, x1 - x0);
  const angleDeg = Math.round(-lineAngle * 180 / Math.PI);
  const arcR = 28;
  ctx.strokeStyle = d.color; ctx.lineWidth = 1; ctx.setLineDash([]);
  ctx.beginPath(); ctx.arc(x0, y0, arcR, 0, lineAngle, lineAngle > 0); ctx.stroke();
  ctx.font = '11px sans-serif'; ctx.fillStyle = d.color;
  ctx.fillText(`${angleDeg}°`, x0 + arcR + 4, y0 - 4);
}

function renderInfoLine(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (!d.points[0]) return;
  const y = m.priceToY(d.points[0].price);
  if (y === null) return;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1;
  ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssW, y); ctx.stroke();
  const price = d.points[0].price;
  const label = price >= 1000 ? price.toLocaleString('ko-KR') : price.toFixed(4);
  ctx.font = 'bold 10px sans-serif';
  const tw = ctx.measureText(label).width + 8;
  ctx.fillStyle = d.color;
  ctx.fillRect(cssW - tw - 4, y - 9, tw, 18);
  ctx.fillStyle = '#000';
  ctx.fillText(label, cssW - tw, y + 4);
}

function renderParallelChannel3(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const dx = x1 - x0, dy = y1 - y0;
  const len = Math.hypot(dx, dy);
  let offX = 0, offY = 0;
  if (d.points.length >= 3 && len > 0.001) {
    const x2 = m.timeToX(d.points[2].time), y2 = m.priceToY(d.points[2].price);
    if (x2 !== null && y2 !== null) {
      const nx = -dy / len, ny = dx / len;
      const t = (x2 - x0) * nx + (y2 - y0) * ny;
      offX = t * nx; offY = t * ny;
    }
  }
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  const T = (cssW + cssH) * 3;
  const drawExt = (ox: number, oy: number, ls: string) => {
    if (len < 0.001) return;
    const t2 = T / len;
    ctx.setLineDash(ls === 'dashed' ? [6, 4] : []);
    ctx.beginPath(); ctx.moveTo(ox - dx * t2, oy - dy * t2); ctx.lineTo(ox + dx * t2, oy + dy * t2); ctx.stroke();
  };
  drawExt(x0, y0, d.style ?? 'solid');
  if (offX !== 0 || offY !== 0) {
    drawExt(x0 + offX, y0 + offY, 'dashed');
    const t2 = T / Math.max(len, 1);
    ctx.globalAlpha = 0.08; ctx.fillStyle = d.color;
    ctx.beginPath();
    ctx.moveTo(x0 - dx * t2, y0 - dy * t2); ctx.lineTo(x0 + dx * t2, y0 + dy * t2);
    ctx.lineTo(x0 + offX + dx * t2, y0 + offY + dy * t2); ctx.lineTo(x0 + offX - dx * t2, y0 + offY - dy * t2);
    ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1;
  }
  ctx.setLineDash([]);
}

function renderFibExt(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (d.points.length < 2) return;
  const range = d.points[1].price - d.points[0].price;
  for (let i = 0; i < FIB_EXT_LEVELS.length; i++) {
    const price = d.points[1].price + range * FIB_EXT_LEVELS[i];
    const y = m.priceToY(price);
    if (y === null) continue;
    ctx.strokeStyle = FIB_EXT_COLORS[i] ?? '#888';
    ctx.lineWidth = FIB_EXT_LEVELS[i] === 0 || FIB_EXT_LEVELS[i] === 1.0 ? 1.5 : 0.8;
    ctx.setLineDash([3, 3]);
    ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssW, y); ctx.stroke();
    ctx.setLineDash([]);
    ctx.font = '9px monospace'; ctx.fillStyle = FIB_EXT_COLORS[i] ?? '#888';
    ctx.fillText(`${(FIB_EXT_LEVELS[i] * 100).toFixed(1)}%`, 4, y - 2);
  }
}

function renderFibChannel(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const dx = x1 - x0, dy = y1 - y0;
  const len = Math.hypot(dx, dy);
  if (len < 0.001) return;
  const nx = -dy / len, ny = dx / len;
  const height = dy;
  const FIB_CH = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0, 1.618];
  const T = (cssW + cssH) * 2 / Math.max(len, 1);
  for (let i = 0; i < FIB_CH.length; i++) {
    const off = height * FIB_CH[i];
    const ox = nx * off, oy = ny * off;
    ctx.strokeStyle = FIB_COLORS[Math.min(i, FIB_COLORS.length - 1)];
    ctx.lineWidth = i === 0 || i === 6 ? 1.5 : 0.8;
    ctx.setLineDash(i === 0 ? [] : [3, 3]);
    ctx.beginPath();
    ctx.moveTo(x0 + ox - dx * T, y0 + oy - dy * T);
    ctx.lineTo(x0 + ox + dx * T, y0 + oy + dy * T);
    ctx.stroke(); ctx.setLineDash([]);
    ctx.font = '9px monospace'; ctx.fillStyle = FIB_COLORS[Math.min(i, FIB_COLORS.length - 1)];
    ctx.fillText(`${(FIB_CH[i] * 100).toFixed(1)}%`, x0 + ox + 4, y0 + oy - 2);
  }
}

function renderFibSpeedFan(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const dx = x1 - x0, dy = y1 - y0;
  const FIB_FAN = [0.25, 0.333, 0.5, 0.618, 1.0];
  const T = Math.max(cssW, cssH) * 3 / Math.max(Math.abs(dx), 1);
  ctx.setLineDash([]);
  FIB_FAN.forEach((r, i) => {
    ctx.strokeStyle = FIB_COLORS[Math.min(i * 2, FIB_COLORS.length - 1)];
    ctx.lineWidth = r === 1.0 ? 1.5 : 0.8;
    ctx.beginPath(); ctx.moveTo(x0, y0);
    ctx.lineTo(x0 + dx * T, y0 + dy * r * T);
    ctx.stroke();
    ctx.font = '9px monospace'; ctx.fillStyle = FIB_COLORS[Math.min(i * 2, FIB_COLORS.length - 1)];
    ctx.fillText(`${Math.round(r * 100)}%`, x0 + dx * T - 24, y0 + dy * r * T);
  });
}

function renderGannFan(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const tdx = Math.abs(x1 - x0);
  const tdy = Math.abs(y1 - y0);
  if (tdx < 1 || tdy < 1) return;
  const baseRatio = tdy / tdx;
  const dirX = x1 > x0 ? 1 : -1;
  const dirY = y1 > y0 ? 1 : -1;
  const T = Math.max(cssW, cssH) * 2;
  ctx.setLineDash([]);
  GANN_RATIOS.forEach((r, i) => {
    ctx.strokeStyle = GANN_COLORS_ARR[i]; ctx.lineWidth = r === 1 ? 1.8 : 0.8;
    const ex = x0 + dirX * T;
    const ey = y0 + dirY * T * r * baseRatio;
    ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(ex, ey); ctx.stroke();
    ctx.font = '9px monospace'; ctx.fillStyle = GANN_COLORS_ARR[i];
    ctx.fillText(GANN_LABELS[i], ex - 26, ey);
  });
}

function renderGannBox(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const lx = Math.min(x0, x1), rx = Math.max(x0, x1);
  const ty = Math.min(y0, y1), by = Math.max(y0, y1);
  const w = rx - lx, h = by - ty;
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.2;
  ctx.fillStyle = d.color + '11'; ctx.fillRect(lx, ty, w, h);
  ctx.setLineDash([]); ctx.strokeRect(lx, ty, w, h);
  ctx.setLineDash([2, 3]); ctx.globalAlpha = 0.4;
  ctx.beginPath(); ctx.moveTo(lx, ty); ctx.lineTo(rx, by); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(rx, ty); ctx.lineTo(lx, by); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(lx + w / 2, ty); ctx.lineTo(lx + w / 2, by); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(lx, ty + h / 2); ctx.lineTo(rx, ty + h / 2); ctx.stroke();
  for (const fx of [0.25, 0.75]) {
    ctx.beginPath(); ctx.moveTo(lx + w * fx, ty); ctx.lineTo(lx + w * (1 - fx), by); ctx.stroke();
  }
  ctx.setLineDash([]); ctx.globalAlpha = 1;
}

function renderGannSquare(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const s = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0));
  const sx = x1 > x0 ? x0 : x0 - s;
  const sy = y1 > y0 ? y0 : y0 - s;
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.2;
  ctx.fillStyle = d.color + '0A'; ctx.fillRect(sx, sy, s, s); ctx.strokeRect(sx, sy, s, s);
  ctx.setLineDash([2, 3]); ctx.globalAlpha = 0.35;
  for (let i = 1; i < 8; i++) {
    const f = i / 8;
    ctx.beginPath(); ctx.moveTo(sx + s * f, sy); ctx.lineTo(sx + s * (1 - f), sy + s); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(sx, sy + s * f); ctx.lineTo(sx + s, sy + s * (1 - f)); ctx.stroke();
  }
  ctx.setLineDash([]); ctx.globalAlpha = 1;
}

function renderForecast(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0 === null || y0 === null || x1 === null || y1 === null) return;
  const lx = Math.min(x0, x1), rx = Math.max(x0, x1);
  const isUp = y1 < y0;
  const ty = Math.min(y0, y1), by = Math.max(y0, y1);
  const fillColor = isUp ? 'rgba(76,175,80,0.14)' : 'rgba(239,83,80,0.14)';
  const borderColor = isUp ? '#4CAF50' : '#EF5350';
  ctx.fillStyle = fillColor; ctx.fillRect(lx, ty, rx - lx, by - ty);
  ctx.strokeStyle = borderColor; ctx.lineWidth = 1.2;
  ctx.setLineDash([5, 3]); ctx.strokeRect(lx, ty, rx - lx, by - ty); ctx.setLineDash([]);
  ctx.strokeStyle = '#9E9E9E'; ctx.lineWidth = 1;
  ctx.beginPath(); ctx.moveTo(lx, y0); ctx.lineTo(rx, y0); ctx.stroke();
  const pct = d.points[0].price > 0
    ? Math.abs((d.points[1].price - d.points[0].price) / d.points[0].price * 100).toFixed(2)
    : '0';
  ctx.font = 'bold 10px sans-serif'; ctx.fillStyle = borderColor;
  ctx.fillText(`${isUp ? '▲' : '▼'} ${pct}%`, lx + 4, ty + 14);
}

function formatDateRangeLabel(m: ChartManager, t0: number, t1: number): string {
  const secs = Math.abs(t1 - t0);
  const bars = m.countBarsBetweenTime(t0, t1);
  const days = Math.floor(secs / 86400);
  const hrs  = Math.floor((secs % 86400) / 3600);
  const mins = Math.floor((secs % 3600) / 60);
  const timePart = days > 0
    ? `${days}일 ${hrs}시간`
    : hrs > 0
      ? `${hrs}시간 ${mins}분`
      : mins > 0
        ? `${mins}분`
        : `${secs}초`;
  return bars > 0 ? `${bars}봉, ${timePart}` : timePart;
}

function renderDateRange(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssH: number) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time);
  const x1 = m.timeToX(d.points[1].time);
  if (x0 === null || x1 === null) return;
  const lx = Math.min(x0, x1), rx = Math.max(x0, x1);
  ctx.fillStyle = d.color + '1A'; ctx.fillRect(lx, 0, rx - lx, cssH);
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.2; ctx.setLineDash([]);
  ctx.beginPath(); ctx.moveTo(lx, 0); ctx.lineTo(lx, cssH); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(rx, 0); ctx.lineTo(rx, cssH); ctx.stroke();
  const label = formatDateRangeLabel(m, d.points[0].time, d.points[1].time);
  ctx.font = 'bold 11px sans-serif'; ctx.fillStyle = d.color;
  const tw = ctx.measureText(label).width;
  ctx.fillText(label, lx + (rx - lx) / 2 - tw / 2, 16);
}

/** 날짜&가격 범위 — 날짜 범위(date_range) + 가격 범위(measure) 결합 */
function renderDatePriceRange(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssH: number, theme: Theme) {
  if (d.points.length < 2) {
    const p = d.points[0]; if (!p) return;
    const x = m.timeToX(p.time), y = m.priceToY(p.price);
    if (x !== null && y !== null) { ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI*2); ctx.fillStyle = d.color; ctx.fill(); }
    return;
  }
  const [p0, p1] = d.points;
  const x0 = m.timeToX(p0.time), y0 = m.priceToY(p0.price);
  const x1 = m.timeToX(p1.time), y1 = m.priceToY(p1.price);
  if (x0===null||y0===null||x1===null||y1===null) return;
  const lx = Math.min(x0,x1), rx = Math.max(x0,x1);
  const ty = Math.min(y0,y1), by = Math.max(y0,y1);
  const pct  = (p1.price - p0.price) / p0.price * 100;
  const isUp = pct >= 0;
  const fillCol  = isUp ? 'rgba(76,175,80,0.15)'  : 'rgba(239,83,80,0.15)';
  const lineCol  = isUp ? '#4CAF50' : '#EF5350';
  const textCol  = isUp ? '#4CAF50' : '#EF5350';
  // 배경 사각형
  ctx.fillStyle = fillCol;
  ctx.fillRect(lx, ty, rx - lx, by - ty);
  // 테두리
  ctx.strokeStyle = lineCol; ctx.lineWidth = 1; ctx.setLineDash([]);
  ctx.strokeRect(lx, ty, rx - lx, by - ty);
  // 수직 경계선 (전체 높이)
  ctx.setLineDash([4,3]); ctx.lineWidth = 0.8;
  ctx.beginPath(); ctx.moveTo(lx, 0); ctx.lineTo(lx, cssH); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(rx, 0); ctx.lineTo(rx, cssH); ctx.stroke();
  ctx.setLineDash([]);
  // ── 중앙 정보 배지 ──
  const cx2 = (lx + rx) / 2;
  const cy2 = (ty + by) / 2;
  const secs = Math.abs(p1.time - p0.time);
  const days = Math.floor(secs / 86400);
  const hrs  = Math.floor((secs % 86400) / 3600);
  const dateLabel  = days > 0 ? `${days}일 ${hrs}시간` : `${hrs}시간`;
  const sign       = pct >= 0 ? '+' : '';
  const priceLabel = `${sign}${pct.toFixed(2)}%  Δ${Math.abs(p1.price - p0.price).toFixed(2)}`;
  ctx.font = 'bold 12px sans-serif';
  const dw = ctx.measureText(dateLabel).width;
  ctx.font = '11px monospace';
  const pw = ctx.measureText(priceLabel).width;
  const bw2 = Math.max(dw, pw) + 16;
  // 배지 배경
  ctx.fillStyle = isUp ? 'rgba(76,175,80,0.85)' : 'rgba(239,83,80,0.85)';
  if (ctx.roundRect) ctx.roundRect(cx2 - bw2/2, cy2 - 20, bw2, 40, 5);
  else ctx.rect(cx2 - bw2/2, cy2 - 20, bw2, 40);
  ctx.fill();
  // 날짜 텍스트
  ctx.fillStyle = '#fff'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center';
  ctx.fillText(dateLabel, cx2, cy2 - 4);
  // 가격 텍스트
  ctx.font = '11px monospace';
  ctx.fillStyle = theme === 'dark' ? '#FFFDE7' : '#fff';
  ctx.fillText(priceLabel, cx2, cy2 + 12);
  ctx.textAlign = 'left';
}

/** 데이터 창 — 클릭 지점의 가격 정보 창 */
function renderDataWindow(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x===null||y===null) return;
  const price = d.points[0].price;
  const ts    = d.points[0].time;
  const rows: [string, string][] = [
    ['시간', formatUnixDateTime(ts)],
    ['가격', price.toFixed(2)],
  ];
  const rowH = 20, pad = 10, labelW = 44;
  ctx.font = '11px monospace';
  const valueW = Math.max(...rows.map(([,v]) => ctx.measureText(v).width));
  const bw = labelW + valueW + pad * 2 + 8;
  const bh = rowH * rows.length + pad * 2 + 24; // +24 for header
  const bx = x + 8, by = y - bh / 2;
  // 배경
  ctx.fillStyle = '#1E2A35'; ctx.strokeStyle = d.color; ctx.lineWidth = 1; ctx.setLineDash([]);
  if (ctx.roundRect) ctx.roundRect(bx, by, bw, bh, 5);
  else ctx.rect(bx, by, bw, bh);
  ctx.fill(); ctx.stroke();
  // 헤더
  ctx.fillStyle = d.color;
  if (ctx.roundRect) ctx.roundRect(bx, by, bw, 22, [5, 5, 0, 0]);
  else ctx.rect(bx, by, bw, 22);
  ctx.fill();
  ctx.fillStyle = '#fff'; ctx.font = 'bold 11px monospace'; ctx.textAlign = 'center';
  ctx.fillText('데이터 창', bx + bw / 2, by + 14);
  ctx.textAlign = 'left';
  // 행
  rows.forEach(([label, value], i) => {
    const ry = by + 22 + pad + i * rowH;
    // 구분선
    if (i > 0) {
      ctx.strokeStyle = '#2E3D4D'; ctx.lineWidth = 0.5; ctx.setLineDash([]);
      ctx.beginPath(); ctx.moveTo(bx + 4, ry - rowH/2 + 2); ctx.lineTo(bx + bw - 4, ry - rowH/2 + 2); ctx.stroke();
    }
    ctx.fillStyle = '#78909C'; ctx.font = '10px monospace';
    ctx.fillText(label, bx + pad, ry + 4);
    ctx.fillStyle = '#ECEFF1'; ctx.font = '11px monospace';
    ctx.fillText(value, bx + pad + labelW, ry + 4);
  });
  // 앵커 점
  ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI*2);
  ctx.fillStyle = d.color; ctx.fill();
  ctx.strokeStyle = d.color; ctx.lineWidth = 0.8; ctx.setLineDash([2,2]);
  ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(bx, by + bh/2); ctx.stroke();
  ctx.setLineDash([]);
}

/** 돋보기 — 2점으로 렌즈 크기 지정, 원형 돋보기 아이콘 표시 */
function renderMagnifier(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0]) return;
  const cx = m.timeToX(d.points[0].time), cy = m.priceToY(d.points[0].price);
  if (cx === null || cy === null) return;
  let r = 40;
  if (d.points.length >= 2) {
    const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
    if (x1 !== null && y1 !== null) r = Math.max(20, Math.hypot(x1 - cx, y1 - cy));
  }
  // 렌즈 원형 채우기
  ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2);
  ctx.fillStyle = d.color + '18'; ctx.fill();
  ctx.strokeStyle = d.color; ctx.lineWidth = 2; ctx.setLineDash([]);
  ctx.stroke();
  // 내부 십자선
  ctx.strokeStyle = d.color + '60'; ctx.lineWidth = 0.8;
  ctx.beginPath(); ctx.moveTo(cx - r * 0.7, cy); ctx.lineTo(cx + r * 0.7, cy); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(cx, cy - r * 0.7); ctx.lineTo(cx, cy + r * 0.7); ctx.stroke();
  // 내부 동심원 (3개)
  [0.33, 0.55, 0.78].forEach(ratio => {
    ctx.beginPath(); ctx.arc(cx, cy, r * ratio, 0, Math.PI * 2);
    ctx.strokeStyle = d.color + '30'; ctx.lineWidth = 0.5;
    ctx.stroke();
  });
  // 손잡이 (우하단 45도)
  const hx1 = cx + r * 0.72, hy1 = cy + r * 0.72;
  const hx2 = cx + r * 1.3,  hy2 = cy + r * 1.3;
  ctx.strokeStyle = d.color; ctx.lineWidth = 4; ctx.lineCap = 'round';
  ctx.beginPath(); ctx.moveTo(hx1, hy1); ctx.lineTo(hx2, hy2); ctx.stroke();
  ctx.lineCap = 'butt';
  // 중앙 + 표시
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.5;
  ctx.beginPath(); ctx.moveTo(cx - 5, cy); ctx.lineTo(cx + 5, cy); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(cx, cy - 5); ctx.lineTo(cx, cy + 5); ctx.stroke();
}

// ── 텍스트 & 노트 고유 렌더러 ─────────────────────────────────────────────────

/** 노트 (고정위치문자와 달리 노트패드 스타일 박스) */
function renderNoteText(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  const text = d.text ?? '노트';
  ctx.font = '12px sans-serif';
  const lines = text.split('\n');
  const lh = 16, pad = 8;
  const tw = Math.max(...lines.map(l => ctx.measureText(l).width));
  const bw = Math.max(tw + pad * 2, 80), bh = lines.length * lh + pad * 2;
  // 노트패드 배경 (연노랑)
  ctx.fillStyle = '#FFFDE7';
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.2; ctx.setLineDash([]);
  ctx.beginPath();
  if (ctx.roundRect) ctx.roundRect(x, y, bw, bh, 4);
  else { ctx.rect(x, y, bw, bh); }
  ctx.fill(); ctx.stroke();
  // 줄선 (노트패드 효과)
  ctx.strokeStyle = '#F9A825'; ctx.lineWidth = 0.5;
  for (let i = 1; i <= lines.length; i++) {
    const ly = y + pad + i * lh - lh * 0.3;
    if (ly < y + bh - 4) {
      ctx.beginPath(); ctx.moveTo(x + pad, ly); ctx.lineTo(x + bw - pad, ly); ctx.stroke();
    }
  }
  // 텍스트
  ctx.fillStyle = '#37474F'; ctx.font = '12px sans-serif';
  lines.forEach((line, i) => ctx.fillText(line, x + pad, y + pad + (i + 1) * lh - 3));
  // 핀 앵커
  ctx.beginPath(); ctx.arc(x + 4, y + 4, 3, 0, Math.PI * 2);
  ctx.fillStyle = d.color; ctx.fill();
}

/** 프라이스 노트 (가격 수준 라인과 연결된 라벨) */
function renderPriceNote(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  const text = d.text ?? d.points[0].price.toFixed(2);
  ctx.font = '11px monospace';
  const tw = ctx.measureText(text).width;
  const bw = tw + 16, bh = 22;
  // 박스
  ctx.fillStyle = d.color + 'DD'; ctx.strokeStyle = d.color; ctx.lineWidth = 1; ctx.setLineDash([]);
  ctx.fillRect(x, y - bh / 2, bw, bh);
  ctx.strokeRect(x, y - bh / 2, bw, bh);
  // 우측으로 가격 수준선
  ctx.strokeStyle = d.color; ctx.lineWidth = 0.8; ctx.setLineDash([4, 3]);
  ctx.beginPath(); ctx.moveTo(x + bw, y); ctx.lineTo(cssW, y); ctx.stroke();
  ctx.setLineDash([]);
  // 텍스트
  ctx.fillStyle = '#fff'; ctx.fillText(text, x + 8, y + 4);
  // 앵커
  ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2);
  ctx.fillStyle = d.color; ctx.fill();
}

/** 코멘트 (원형 말풍선 — callout의 사각 말풍선과 구별) */
function renderCommentMark(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  const text = d.text ?? '코멘트';
  ctx.font = '12px sans-serif';
  const tw = ctx.measureText(text).width;
  const pad = 10, r = 8;
  const bw = Math.max(tw + pad * 2, 60), bh = 30;
  const bx = x - bw / 2, by = y - bh - 10;
  // 원형 모서리 말풍선
  ctx.fillStyle = '#37474F'; ctx.strokeStyle = d.color; ctx.lineWidth = 1.5; ctx.setLineDash([]);
  ctx.beginPath();
  if (ctx.roundRect) ctx.roundRect(bx, by, bw, bh, r);
  else { ctx.rect(bx, by, bw, bh); }
  ctx.fill(); ctx.stroke();
  // 꼬리
  ctx.fillStyle = '#37474F'; ctx.strokeStyle = d.color; ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(x - 5, by + bh); ctx.lineTo(x, y); ctx.lineTo(x + 5, by + bh);
  ctx.fill(); ctx.stroke();
  // 작은 원 3개 (채팅 아이콘 느낌)
  ctx.fillStyle = '#90A4AE';
  [bx + bw * 0.3, bx + bw * 0.5, bx + bw * 0.7].forEach(cx2 => {
    ctx.beginPath(); ctx.arc(cx2, by + bh / 2, 3, 0, Math.PI * 2); ctx.fill();
  });
  // 텍스트
  ctx.fillStyle = '#ECEFF1'; ctx.textAlign = 'center';
  ctx.fillText(text, x, by + bh / 2 + 4);
  ctx.textAlign = 'left';
  // 앵커
  ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2);
  ctx.fillStyle = d.color; ctx.fill();
}

/** 고정위치문자 (화면 고정 위치 텍스트 — 위치 앵커 원 표시) */
function renderFixedText(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  const text = d.text ?? '고정 텍스트';
  ctx.font = '12px sans-serif';
  const tw = ctx.measureText(text).width;
  // 텍스트 배경
  ctx.fillStyle = 'rgba(0,0,0,0.55)';
  ctx.fillRect(x + 10, y - 13, tw + 8, 18);
  ctx.fillStyle = d.color; ctx.fillText(text, x + 14, y);
  // 앵커 원 + 십자
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.5; ctx.setLineDash([]);
  ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(x - 9, y); ctx.lineTo(x + 9, y);
  ctx.moveTo(x, y - 9); ctx.lineTo(x, y + 9);
  ctx.stroke();
}

/** 가격 라벨 (우측 가격축 라벨 스타일) */
function renderPriceLabel(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  const text = d.text ?? d.points[0].price.toFixed(2);
  ctx.font = '11px monospace';
  const tw = ctx.measureText(text).width;
  const lbx = cssW - tw - 14;
  // 수평 점선 연결
  ctx.strokeStyle = d.color; ctx.lineWidth = 0.8; ctx.setLineDash([4, 3]);
  ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(lbx - 4, y); ctx.stroke();
  ctx.setLineDash([]);
  // 화살표 뾰족 라벨 박스 (가격축 스타일)
  ctx.fillStyle = d.color;
  ctx.beginPath();
  ctx.moveTo(lbx - 8, y);
  ctx.lineTo(lbx, y - 9);
  ctx.lineTo(cssW, y - 9);
  ctx.lineTo(cssW, y + 9);
  ctx.lineTo(lbx, y + 9);
  ctx.closePath();
  ctx.fill();
  // 텍스트
  ctx.fillStyle = '#fff'; ctx.fillText(text, lbx + 2, y + 4);
  // 앵커
  ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2);
  ctx.fillStyle = d.color; ctx.fill();
}

function renderCallout(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  const text = d.text ?? '텍스트';
  ctx.font = '12px sans-serif';
  const tw = ctx.measureText(text).width;
  const bx = x - 8, bw = tw + 16, bh = 28, by = y - 44;
  ctx.fillStyle = d.color + 'CC';
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.2; ctx.setLineDash([]);
  ctx.beginPath();
  if (ctx.roundRect) ctx.roundRect(bx, by, bw, bh, 5); else ctx.rect(bx, by, bw, bh);
  ctx.fill(); ctx.stroke();
  ctx.beginPath(); ctx.moveTo(x - 6, by + bh); ctx.lineTo(x, y); ctx.lineTo(x + 6, by + bh); ctx.closePath();
  ctx.fill(); ctx.stroke();
  ctx.fillStyle = '#000'; ctx.fillText(text, bx + 8, by + 18);
}

function renderPinMark(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  ctx.fillStyle = d.color; ctx.strokeStyle = d.color; ctx.lineWidth = 1.5; ctx.setLineDash([]);
  ctx.beginPath(); ctx.arc(x, y - 14, 6, 0, Math.PI * 2); ctx.fill();
  ctx.beginPath(); ctx.moveTo(x, y - 8); ctx.lineTo(x, y); ctx.stroke();
  ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI * 2);
  ctx.fillStyle = '#fff'; ctx.fill();
  ctx.fillStyle = d.color; ctx.strokeStyle = d.color; ctx.stroke();
}

function renderFlagMark(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time), y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  ctx.strokeStyle = d.color; ctx.fillStyle = d.color; ctx.lineWidth = 1.5; ctx.setLineDash([]);
  ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y - 24); ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(x, y - 24); ctx.lineTo(x + 14, y - 20); ctx.lineTo(x, y - 16);
  ctx.closePath(); ctx.fill();
}

function renderGuideLine(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (!d.points[0]) return;
  const y = m.priceToY(d.points[0].price);
  if (y === null) return;
  ctx.strokeStyle = d.color + 'BB'; ctx.lineWidth = d.lineWidth ?? 1;
  ctx.setLineDash([6, 4]);
  ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(cssW, y); ctx.stroke();
  ctx.setLineDash([]);
  const lbl = d.points[0].price.toFixed(2);
  ctx.font = '10px monospace'; ctx.fillStyle = d.color + 'BB';
  ctx.fillText(lbl, 4, y - 3);
}

function renderPitchSchiff(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (d.points.length < 3) return;
  const [p0, p1, p2] = d.points;
  const x0 = m.timeToX(p0.time), y0 = m.priceToY(p0.price);
  const x1 = m.timeToX(p1.time), y1 = m.priceToY(p1.price);
  const x2 = m.timeToX(p2.time), y2 = m.priceToY(p2.price);
  if (x0===null||y0===null||x1===null||y1===null||x2===null||y2===null) return;
  // Schiff: 핸들 시작점 = P0과 P1의 중간점
  const hsx = (x0 + x1) / 2, hsy = (y0 + y1) / 2;
  const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  const T = (cssW + cssH) * 2;
  const drawExt = (sx: number, sy: number, ex: number, ey: number) => {
    const ddx = ex - sx, ddy = ey - sy, ll = Math.hypot(ddx, ddy);
    if (ll < 0.001) return;
    ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx + ddx * T / ll, sy + ddy * T / ll); ctx.stroke();
  };
  ctx.beginPath(); ctx.moveTo(hsx, hsy); ctx.lineTo(mx, my); ctx.stroke();
  drawExt(mx, my, mx + (mx - hsx), my + (my - hsy));
  const offX = x1 - mx, offY = y1 - my;
  drawExt(hsx + offX, hsy + offY, x1, y1);
  drawExt(hsx - offX, hsy - offY, x2, y2);
  [[hsx, hsy], [x1, y1], [x2, y2]].forEach(([px, py]) => {
    ctx.beginPath(); ctx.arc(px, py, 4, 0, Math.PI * 2); ctx.fillStyle = d.color; ctx.fill();
  });
}

function renderPitchInside(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number) {
  if (d.points.length < 3) return;
  const [p0, p1, p2] = d.points;
  const x0 = m.timeToX(p0.time), y0 = m.priceToY(p0.price);
  const x1 = m.timeToX(p1.time), y1 = m.priceToY(p1.price);
  const x2 = m.timeToX(p2.time), y2 = m.priceToY(p2.price);
  if (x0===null||y0===null||x1===null||y1===null||x2===null||y2===null) return;
  // Inside: 핸들 = 중간점(P0,P1) → 중간점(P1,P2)
  const hsx = (x0 + x1) / 2, hsy = (y0 + y1) / 2;
  const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  const T = (cssW + cssH) * 2;
  const drawExt = (sx: number, sy: number, ex: number, ey: number) => {
    const ddx = ex - sx, ddy = ey - sy, ll = Math.hypot(ddx, ddy);
    if (ll < 0.001) return;
    ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(sx + ddx * T / ll, sy + ddy * T / ll); ctx.stroke();
  };
  ctx.beginPath(); ctx.moveTo(hsx, hsy); ctx.lineTo(mx, my); ctx.stroke();
  drawExt(mx, my, mx + (mx - hsx), my + (my - hsy));
  const offX = x1 - mx, offY = y1 - my;
  drawExt(hsx + offX, hsy + offY, x1, y1);
  drawExt(hsx - offX, hsy - offY, x2, y2);
  [[hsx, hsy], [x1, y1], [x2, y2]].forEach(([px, py]) => {
    ctx.beginPath(); ctx.arc(px, py, 4, 0, Math.PI * 2); ctx.fillStyle = d.color; ctx.fill();
  });
}

function renderEllipse(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const cx = m.timeToX(d.points[0].time), cy = m.priceToY(d.points[0].price);
  const ex = m.timeToX(d.points[1].time), ey = m.priceToY(d.points[1].price);
  if (cx===null||cy===null||ex===null||ey===null) return;
  const rx = Math.abs(ex - cx), ry = Math.abs(ey - cy);
  if (rx < 1 || ry < 1) return;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.beginPath();
  ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
  ctx.fillStyle = d.color + '22'; ctx.fill(); ctx.stroke();
}

// ── 피보나치 원형 계열 ────────────────────────────────────────────────────────

function renderFibCircles(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const cx = m.timeToX(d.points[0].time), cy = m.priceToY(d.points[0].price);
  const ex = m.timeToX(d.points[1].time), ey = m.priceToY(d.points[1].price);
  if (cx===null||cy===null||ex===null||ey===null) return;
  const baseR = Math.hypot(ex - cx, ey - cy);
  const levels = [0.236, 0.382, 0.5, 0.618, 0.786, 1.0, 1.618];
  levels.forEach((lv, i) => {
    const r = baseR * lv;
    ctx.strokeStyle = FIB_COLORS[Math.min(i, FIB_COLORS.length - 1)];
    ctx.lineWidth = lv === 1.0 ? 1.5 : 0.8;
    ctx.setLineDash(lv === 1.0 ? [] : [3, 3]);
    ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.stroke();
    ctx.setLineDash([]);
    ctx.font = '9px monospace';
    ctx.fillStyle = FIB_COLORS[Math.min(i, FIB_COLORS.length - 1)];
    ctx.fillText(`${(lv * 100).toFixed(1)}%`, cx + r + 2, cy - 2);
  });
  // 중심점
  ctx.beginPath(); ctx.arc(cx, cy, 3, 0, Math.PI * 2);
  ctx.fillStyle = d.color; ctx.fill();
}

function renderFibSpiral(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const cx = m.timeToX(d.points[0].time), cy = m.priceToY(d.points[0].price);
  const ex = m.timeToX(d.points[1].time), ey = m.priceToY(d.points[1].price);
  if (cx===null||cy===null||ex===null||ey===null) return;
  const baseR = Math.hypot(ex - cx, ey - cy);
  const startAngle = Math.atan2(ey - cy, ex - cx);
  // 황금 비율 나선: r = baseR * e^(b*θ), b = ln(φ)/(π/2)
  const PHI = 1.6180339887;
  const b = Math.log(PHI) / (Math.PI / 2);
  const totalAngle = Math.PI * 3; // 1.5 바퀴
  const steps = 300;
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.5; ctx.setLineDash([]);
  ctx.beginPath();
  for (let i = 0; i <= steps; i++) {
    const theta = -totalAngle * (1 - i / steps);
    const r = baseR * Math.exp(b * theta);
    const x = cx + r * Math.cos(startAngle + theta);
    const y = cy + r * Math.sin(startAngle + theta);
    if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
  }
  ctx.stroke();
  // 외곽원 (기준)
  ctx.strokeStyle = d.color + '44'; ctx.lineWidth = 0.8;
  ctx.beginPath(); ctx.arc(cx, cy, baseR, 0, Math.PI * 2); ctx.stroke();
  // 중심점
  ctx.beginPath(); ctx.arc(cx, cy, 3, 0, Math.PI * 2);
  ctx.fillStyle = d.color; ctx.fill();
}

function renderFibSpeedArc(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0===null||y0===null||x1===null||y1===null) return;
  const baseLen = Math.hypot(x1 - x0, y1 - y0);
  const baseAngle = Math.atan2(y1 - y0, x1 - x0);
  // P0을 중심으로, 각 피보나치 거리에 반원호 그리기
  const arcLevels = [0.236, 0.382, 0.5, 0.618, 0.786, 1.0];
  arcLevels.forEach((lv, i) => {
    const r = baseLen * lv;
    ctx.strokeStyle = FIB_COLORS[Math.min(i, FIB_COLORS.length - 1)];
    ctx.lineWidth = lv === 1.0 ? 1.5 : 0.8;
    ctx.setLineDash(lv === 1.0 ? [] : [3, 2]);
    // 기준선 방향 기준으로 ±90° 범위 호
    ctx.beginPath();
    ctx.arc(x0, y0, r, baseAngle - Math.PI / 2, baseAngle + Math.PI / 2, false);
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.font = '9px monospace';
    ctx.fillStyle = FIB_COLORS[Math.min(i, FIB_COLORS.length - 1)];
    const lx = x0 + r * Math.cos(baseAngle + Math.PI / 2) + 2;
    const ly = y0 + r * Math.sin(baseAngle + Math.PI / 2);
    ctx.fillText(`${(lv * 100).toFixed(1)}%`, lx, ly);
  });
  // 기준선
  ctx.strokeStyle = d.color + '66'; ctx.lineWidth = 0.8; ctx.setLineDash([4, 3]);
  ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke();
  ctx.setLineDash([]);
}

function renderArc3(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 3) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  const x2 = m.timeToX(d.points[2].time), y2 = m.priceToY(d.points[2].price);
  if (x0===null||y0===null||x1===null||y1===null||x2===null||y2===null) return;
  // 세 점을 지나는 원 중심 계산
  const D = 2 * (x0 * (y1 - y2) + x1 * (y2 - y0) + x2 * (y0 - y1));
  if (Math.abs(D) < 0.001) {
    applyLineStyle(ctx, d.style); ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
    ctx.beginPath(); ctx.moveTo(x0,y0); ctx.lineTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke();
    return;
  }
  const ux = ((x0*x0+y0*y0)*(y1-y2)+(x1*x1+y1*y1)*(y2-y0)+(x2*x2+y2*y2)*(y0-y1))/D;
  const uy = ((x0*x0+y0*y0)*(x2-x1)+(x1*x1+y1*y1)*(x0-x2)+(x2*x2+y2*y2)*(x1-x0))/D;
  const r = Math.hypot(x0 - ux, y0 - uy);
  const ang0 = Math.atan2(y0 - uy, x0 - ux);
  const ang2 = Math.atan2(y2 - uy, x2 - ux);
  const cross = (x1 - x0) * (y2 - y0) - (y1 - y0) * (x2 - x0);
  applyLineStyle(ctx, d.style); ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.beginPath(); ctx.arc(ux, uy, r, ang0, ang2, cross < 0); ctx.stroke();
}

// ── 도형 추가 렌더러 ────────────────────────────────────────────────────────────

function renderRotatedRect(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0===null||y0===null||x1===null||y1===null) return;
  const dx = x1 - x0, dy = y1 - y0;
  const len = Math.hypot(dx, dy);
  if (len < 0.001) return;
  // 수직 방향 (단위 법선)
  const nx = -dy / len, ny = dx / len;
  let offW = len * 0.5; // 기본 폭: 한 변 길이의 절반 미리보기
  if (d.points.length >= 3) {
    const x2 = m.timeToX(d.points[2].time), y2 = m.priceToY(d.points[2].price);
    if (x2 !== null && y2 !== null) {
      offW = (x2 - x0) * nx + (y2 - y0) * ny;
    }
  }
  const corners: [number, number][] = [
    [x0, y0],
    [x1, y1],
    [x1 + nx * offW, y1 + ny * offW],
    [x0 + nx * offW, y0 + ny * offW],
  ];
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.fillStyle = d.color + '1A';
  ctx.beginPath();
  ctx.moveTo(corners[0][0], corners[0][1]);
  corners.slice(1).forEach(([cx, cy]) => ctx.lineTo(cx, cy));
  ctx.closePath();
  ctx.fill(); ctx.stroke();
}

function renderParallelogram(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0===null||y0===null||x1===null||y1===null) return;
  // P3 = P0 + (P2 - P1)
  let x2 = x1, y2 = y1;
  let x3 = x0, y3 = y0;
  if (d.points.length >= 3) {
    const px2 = m.timeToX(d.points[2].time), py2 = m.priceToY(d.points[2].price);
    if (px2 !== null && py2 !== null) {
      x2 = px2; y2 = py2;
      x3 = x0 + (x2 - x1);
      y3 = y0 + (y2 - y1);
    }
  }
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.fillStyle = d.color + '1A';
  ctx.beginPath();
  ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.lineTo(x2, y2); ctx.lineTo(x3, y3);
  ctx.closePath();
  ctx.fill(); ctx.stroke();
}

function renderPolyline(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  ctx.beginPath();
  for (let i = 0; i < d.points.length; i++) {
    const x = m.timeToX(d.points[i].time), y = m.priceToY(d.points[i].price);
    if (x === null || y === null) continue;
    if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
  }
  ctx.stroke();
  // 각 꼭짓점 표시
  for (const pt of d.points) {
    const x = m.timeToX(pt.time), y = m.priceToY(pt.price);
    if (x !== null && y !== null) {
      ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2);
      ctx.fillStyle = d.color; ctx.fill();
    }
  }
}

// ── 패턴 & 프로젝션 렌더러 ────────────────────────────────────────────────────

// 각 패턴의 포인트 레이블 정의
const PATTERN_LABELS: Record<string, string[]> = {
  xabcd:                   ['X', 'A', 'B', 'C', 'D'],
  cypher:                  ['X', 'A', 'B', 'C', 'D'],
  abcd:                    ['A', 'B', 'C', 'D'],
  head_shoulders:          ['L', 'LS', 'H', 'RS', 'R', 'NL1', 'NL2'],
  wedge_pattern:           ['1', '2', '3', '4', '5'],
  three_drives:            ['O', 'A', '1', 'B', '2', 'C'],
  elliott_impulse:         ['0', '1', '2', '3', '4', '5'],
  elliott_correction:      ['0', 'A', 'B', 'C'],
  elliott_triangle_wave:   ['0', 'A', 'B', 'C', 'D', 'E'],
  elliott_double_combo:    ['0', 'W', 'X', 'Y'],
};

// 패턴별 연결 방식 — undefined이면 순서대로 연결
// head_shoulders는 네크라인(마지막 2점)을 별도로 그림
const PATTERN_LINE_ORDER: Record<string, number[][]> = {
  head_shoulders: [[0,1,2,3,4], [5,6]], // 패턴 + 넥라인
};

function renderZigzagPattern(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (!d.points.length) return;
  const labels  = PATTERN_LABELS[d.type] ?? [];
  const lineOrders = PATTERN_LINE_ORDER[d.type];

  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color;
  ctx.lineWidth   = d.lineWidth ?? 1.5;
  ctx.setLineDash([]);

  // 픽셀 좌표 변환
  const pts = d.points.map(p => ({
    x: m.timeToX(p.time),
    y: m.priceToY(p.price),
  }));

  // 선 그리기 (연결 순서 정의 있으면 사용, 없으면 순서대로)
  const drawSegments = (order: number[]) => {
    let started = false;
    ctx.beginPath();
    for (const idx of order) {
      const pt = pts[idx];
      if (!pt || pt.x === null || pt.y === null) break;
      if (!started) { ctx.moveTo(pt.x, pt.y); started = true; }
      else ctx.lineTo(pt.x, pt.y);
    }
    ctx.stroke();
  };

  if (lineOrders) {
    lineOrders.forEach((order, i) => {
      ctx.setLineDash(i > 0 ? [4, 3] : []); // 넥라인은 파선
      drawSegments(order);
    });
    ctx.setLineDash([]);
  } else {
    drawSegments(pts.map((_, i) => i));
  }

  // 포인트 레이블 & 앵커 원
  pts.forEach((pt, i) => {
    if (!pt || pt.x === null || pt.y === null) return;
    const label = labels[i] ?? `${i}`;

    // 앵커 원
    ctx.beginPath(); ctx.arc(pt.x, pt.y, 4, 0, Math.PI * 2);
    ctx.fillStyle = d.color; ctx.fill();

    // 레이블 배경
    ctx.font = 'bold 10px sans-serif';
    const tw = ctx.measureText(label).width + 6;
    const lx = pt.x - tw / 2;
    const ly = pt.y - 18;
    ctx.fillStyle = d.color + 'DD';
    ctx.fillRect(lx, ly, tw, 14);

    // 레이블 텍스트
    ctx.fillStyle = '#000';
    ctx.fillText(label, lx + 3, ly + 11);
  });
}

function renderGhostFeed(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0===null||y0===null||x1===null||y1===null) return;
  const lx=Math.min(x0,x1), rx=Math.max(x0,x1), ty=Math.min(y0,y1), by=Math.max(y0,y1);
  // 반투명 점선 박스
  ctx.fillStyle   = d.color + '0D';
  ctx.fillRect(lx, ty, rx - lx, by - ty);
  ctx.strokeStyle = d.color + '88';
  ctx.lineWidth   = 1.2;
  ctx.setLineDash([5, 4]);
  ctx.strokeRect(lx, ty, rx - lx, by - ty);
  ctx.setLineDash([]);
  // 내부에 캔들 실루엣 3개 (ghost 효과)
  const cw = (rx - lx) / 5;
  for (let i = 1; i <= 3; i++) {
    const cx = lx + i * (rx - lx) / 4;
    const h  = (by - ty) * (0.4 + Math.sin(i) * 0.2);
    const cy = (ty + by) / 2;
    ctx.fillStyle = d.color + '22';
    ctx.fillRect(cx - cw / 2, cy - h / 2, cw, h);
    ctx.strokeStyle = d.color + '44';
    ctx.lineWidth   = 0.8;
    ctx.setLineDash([]);
    ctx.strokeRect(cx - cw / 2, cy - h / 2, cw, h);
  }
}

function renderBarPattern(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0===null||y0===null||x1===null||y1===null) return;
  const lx=Math.min(x0,x1), rx=Math.max(x0,x1), ty=Math.min(y0,y1), by=Math.max(y0,y1);
  ctx.fillStyle   = d.color + '14';
  ctx.fillRect(lx, ty, rx - lx, by - ty);
  ctx.strokeStyle = d.color;
  ctx.lineWidth   = 1.2;
  ctx.setLineDash([]);
  ctx.strokeRect(lx, ty, rx - lx, by - ty);
  // 바 패턴 내부 구분선 (4등분)
  ctx.strokeStyle = d.color + '44';
  ctx.lineWidth   = 0.8;
  ctx.setLineDash([2, 3]);
  for (let i = 1; i <= 3; i++) {
    const xx = lx + (rx - lx) * i / 4;
    ctx.beginPath(); ctx.moveTo(xx, ty); ctx.lineTo(xx, by); ctx.stroke();
  }
  ctx.setLineDash([]);
  ctx.font = '10px monospace'; ctx.fillStyle = d.color + 'BB';
  ctx.fillText('봉패턴', lx + 4, ty + 14);
}

function renderFixedVolumeProfile(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager) {
  if (d.points.length < 2) return;
  const x0 = m.timeToX(d.points[0].time), y0 = m.priceToY(d.points[0].price);
  const x1 = m.timeToX(d.points[1].time), y1 = m.priceToY(d.points[1].price);
  if (x0===null||y0===null||x1===null||y1===null) return;
  const lx=Math.min(x0,x1), rx=Math.max(x0,x1), ty=Math.min(y0,y1), by=Math.max(y0,y1);
  const h = by - ty, w = rx - lx;
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.2; ctx.setLineDash([]);
  ctx.strokeRect(lx, ty, w, h);
  // 수평 볼륨 바 5개
  const rowH = h / 6;
  const widths = [0.55, 0.8, 1.0, 0.7, 0.45].map(r => w * r * 0.6);
  widths.forEach((bw, i) => {
    const ry = ty + rowH * (i + 0.5) - rowH * 0.3;
    ctx.fillStyle = d.color + '44';
    ctx.fillRect(lx + 1, ry, bw, rowH * 0.7);
  });
  ctx.font = '9px monospace'; ctx.fillStyle = d.color + 'BB';
  ctx.fillText('볼륨 프로파일', lx + 2, ty + 12);
}

function renderAnchoredVwap(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number) {
  if (!d.points[0]) return;
  const x = m.timeToX(d.points[0].time);
  const y = m.priceToY(d.points[0].price);
  if (x === null || y === null) return;
  applyLineStyle(ctx, d.style);
  ctx.strokeStyle = d.color; ctx.lineWidth = d.lineWidth ?? 1.5;
  // 앵커 시점부터 우측으로
  ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(cssW, y); ctx.stroke();
  // 앵커 원
  ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2);
  ctx.fillStyle   = d.color + 'CC'; ctx.fill();
  ctx.strokeStyle = d.color; ctx.lineWidth = 1.5; ctx.stroke();
  // VWAP 라벨
  ctx.font = 'bold 10px monospace'; ctx.fillStyle = d.color;
  ctx.fillText('VWAP', x + 8, y - 5);
}

function renderShape(ctx: CanvasRenderingContext2D, d: Drawing, m: ChartManager, cssW: number, cssH: number, theme: Theme) {
  ctx.save();
  const eff = resolveAlias(d.type);
  switch (eff) {
    // ── 기본 선 ──
    case 'hline':              renderHLine(ctx, d, m, cssW); break;
    case 'vline':              renderVLine(ctx, d, m, cssH); break;
    case 'trendline':          renderLineOrRay(ctx, d, m, 'both', cssW, cssH); break;
    case 'ray':                renderLineOrRay(ctx, d, m, 'right', cssW, cssH); break;
    // ── 신규 독립 선 ──
    case 'cross_line':         renderCrossLine(ctx, d, m, cssW, cssH); break;
    case 'hline_ray':          renderHlineRay(ctx, d, m, cssW); break;
    case 'arrow':              renderArrowLine(ctx, d, m, cssW); break;
    case 'trend_angle':        renderTrendAngle(ctx, d, m, cssW, cssH); break;
    case 'info_line':          renderInfoLine(ctx, d, m, cssW); break;
    case 'guide_line':         renderGuideLine(ctx, d, m, cssW); break;
    // ── 피보나치 ──
    case 'fib':                renderFib(ctx, d, m, cssW); break;
    case 'fib_ext':            renderFibExt(ctx, d, m, cssW); break;
    case 'fib_channel':        renderFibChannel(ctx, d, m, cssW, cssH); break;
    case 'fib_speed_fan':      renderFibSpeedFan(ctx, d, m, cssW, cssH); break;
    case 'fib_circles':        renderFibCircles(ctx, d, m); break;
    case 'fib_spiral':         renderFibSpiral(ctx, d, m); break;
    case 'fib_speed_arc':      renderFibSpeedArc(ctx, d, m); break;
    case 'fibtz':              renderFibTZ(ctx, d, m, cssH); break;
    // ── 갠 ──
    case 'gann_fan':           renderGannFan(ctx, d, m, cssW, cssH); break;
    case 'gann_box':           renderGannBox(ctx, d, m); break;
    case 'gann_square':        renderGannSquare(ctx, d, m); break;
    // ── 채널 / 사각형 ──
    case 'measure':            renderMeasure(ctx, d, m, theme); break;
    case 'date_range':         renderDateRange(ctx, d, m, cssH); break;
    case 'date_price_range':   renderDatePriceRange(ctx, d, m, cssH, theme); break;
    case 'data_window':        renderDataWindow(ctx, d, m); break;
    case 'magnifier':          renderMagnifier(ctx, d, m); break;
    case 'rect':               renderRect(ctx, d, m); break;
    case 'channel':            renderChannel(ctx, d, m, cssW, cssH); break;
    case 'parallel_channel':   renderParallelChannel3(ctx, d, m, cssW, cssH); break;
    case 'forecast':           renderForecast(ctx, d, m); break;
    // ── 피치포크 ──
    case 'pitchfork':          renderPitchfork(ctx, d, m, cssW, cssH); break;
    case 'pitchfork_schiff':   renderPitchSchiff(ctx, d, m, cssW, cssH); break;
    case 'pitchfork_inside':   renderPitchInside(ctx, d, m, cssW, cssH); break;
    // ── 도형 ──
    case 'circle':             renderCircle(ctx, d, m); break;
    case 'ellipse':            renderEllipse(ctx, d, m); break;
    case 'triangle':           renderTriangle(ctx, d, m); break;
    case 'arc':                renderArc3(ctx, d, m); break;
    case 'rotated_rect':       renderRotatedRect(ctx, d, m); break;
    case 'parallelogram':      renderParallelogram(ctx, d, m); break;
    case 'polyline':           renderPolyline(ctx, d, m); break;
    // ── 텍스트 / 마커 ──
    case 'pencil':             renderPencil(ctx, d, m); break;
    case 'text':               renderText(ctx, d, m); break;
    case 'callout':            renderCallout(ctx, d, m); break;
    case 'pin_mark':           renderPinMark(ctx, d, m); break;
    case 'flag_mark':          renderFlagMark(ctx, d, m); break;
    // ── 텍스트 & 노트 고유 렌더러 ──
    case 'note_text':          renderNoteText(ctx, d, m); break;
    case 'price_note':         renderPriceNote(ctx, d, m, cssW); break;
    case 'comment_mark':       renderCommentMark(ctx, d, m); break;
    case 'fixed_text':         renderFixedText(ctx, d, m); break;
    case 'price_label':        renderPriceLabel(ctx, d, m, cssW); break;
    // ── 화살표 마크 ──
    case 'arrow_mark_up':      renderArrowMark(ctx, d, m, 'up'); break;
    case 'arrow_mark_down':    renderArrowMark(ctx, d, m, 'down'); break;
    case 'arrow_mark_left':    renderArrowMark(ctx, d, m, 'left'); break;
    case 'arrow_mark_right':   renderArrowMark(ctx, d, m, 'right'); break;
    // ── 포지션 ──
    case 'long_position':      renderLongShort(ctx, d, m, true); break;
    case 'short_position':     renderLongShort(ctx, d, m, false); break;
    // ── 패턴 (하모닉/엘리엇) ──
    case 'xabcd':
    case 'cypher':
    case 'abcd':
    case 'head_shoulders':
    case 'wedge_pattern':
    case 'three_drives':
    case 'elliott_impulse':
    case 'elliott_correction':
    case 'elliott_triangle_wave':
    case 'elliott_double_combo':
      renderZigzagPattern(ctx, d, m); break;
    // ── 프로젝션 도구 ──
    case 'ghost_feed':           renderGhostFeed(ctx, d, m); break;
    case 'bar_pattern':          renderBarPattern(ctx, d, m); break;
    case 'fixed_volume_profile': renderFixedVolumeProfile(ctx, d, m); break;
    case 'anchored_vwap':        renderAnchoredVwap(ctx, d, m, cssW); break;
  }
  ctx.restore();
}

// ─── Component ─────────────────────────────────────────────────────────────
interface Props {
  manager:             ChartManager;
  activeTool:          string;
  drawings:            Drawing[];
  onAddDrawing:        (d: Drawing) => void;
  onZoom:              (x0: number, x1: number) => void;
  theme:               Theme;
  enabled:             boolean;   // trading 모드
  visible:             boolean;   // eye 토글
  locked:              boolean;   // lock 토글
  /** 현재 선택된 드로잉 ID (핸들 표시) */
  selectedDrawingId?:  string | null;
}

const DrawingCanvas: React.FC<Props> = ({
  manager, activeTool, drawings, onAddDrawing, onZoom,
  theme, enabled, visible, locked,
  selectedDrawingId,
}) => {
  const outerRef    = useRef<HTMLDivElement>(null);
  const canvasRef   = useRef<HTMLCanvasElement>(null);
  const cssSize     = useRef({ w: 0, h: 0 });
  const animRef     = useRef(0);
  const pendingPts  = useRef<DrawingPoint[]>([]);
  const mousePoint  = useRef<DrawingPoint | null>(null);

  // pencil 드래그 상태
  const isPencilDragging = useRef(false);
  const pencilPts        = useRef<DrawingPoint[]>([]);

  // zoom 드래그 상태
  const isZoomDragging = useRef(false);
  const zoomStartX     = useRef(0);
  const zoomCurrentX   = useRef(0);

  const [textInput, setTextInput] = useState<{ x: number; y: number; point: DrawingPoint; toolType: string } | null>(null);
  const [textVal,   setTextVal]   = useState('');

  // ── Core redraw ──────────────────────────────────────────────────────
  const redraw = useCallback(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const { w, h } = cssSize.current;
    if (w === 0 || h === 0) return;

    const dpr = window.devicePixelRatio || 1;
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // 숨김 모드: drawings 자체를 표시 안 함
    if (!visible) return;

    ctx.save();
    ctx.scale(dpr, dpr);

    // 확정된 드로잉 (visible === false 이면 숨김)
    for (const d of drawings) {
      if (d.visible === false) continue;
      renderShape(ctx, d, manager, w, h, theme);
    }

    // 선택된 드로잉: 앵커 핸들 표시
    if (selectedDrawingId) {
      const sel = drawings.find(d => d.id === selectedDrawingId && d.visible !== false);
      if (sel) {
        // 선택 강조: 도형 위에 흰 외곽선 오버레이
        ctx.save();
        ctx.globalAlpha = 0.25;
        ctx.strokeStyle = '#FFFFFF';
        ctx.lineWidth   = (sel.lineWidth ?? 1) + 4;
        ctx.setLineDash([]);
        renderShape(ctx, { ...sel, style: 'solid' }, manager, w, h, theme);
        ctx.restore();

        // 앵커 포인트마다 흰 원형 핸들
        for (const pt of sel.points) {
          const x = manager.timeToX(pt.time);
          const y = manager.priceToY(pt.price);
          if (x === null || y === null) continue;
          ctx.beginPath();
          ctx.arc(x, y, 5, 0, Math.PI * 2);
          ctx.fillStyle   = '#FFFFFF';
          ctx.strokeStyle = sel.color;
          ctx.lineWidth   = 1.5;
          ctx.globalAlpha = 0.9;
          ctx.fill();
          ctx.stroke();
          ctx.globalAlpha = 1;
        }
      }
    }

    if (enabled && !locked) {
      // pencil 실시간 경로 미리보기
      if (activeTool === 'pencil' && isPencilDragging.current && pencilPts.current.length >= 2) {
        ctx.save();
        ctx.strokeStyle = TOOL_COLORS['pencil'];
        ctx.lineWidth = 1.5;
        ctx.lineJoin = 'round';
        ctx.lineCap = 'round';
        ctx.globalAlpha = 0.7;
        ctx.beginPath();
        for (let i = 0; i < pencilPts.current.length; i++) {
          const x = manager.timeToX(pencilPts.current[i].time);
          const y = manager.priceToY(pencilPts.current[i].price);
          if (x === null || y === null) continue;
          if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
        }
        ctx.stroke();
        ctx.restore();
      }

      // zoom 선택 영역 미리보기
      if (activeTool === 'zoom' && isZoomDragging.current) {
        const lx = Math.min(zoomStartX.current, zoomCurrentX.current);
        const rx = Math.max(zoomStartX.current, zoomCurrentX.current);
        ctx.save();
        ctx.fillStyle = 'rgba(33,150,243,0.12)';
        ctx.fillRect(lx, 0, rx - lx, h);
        ctx.strokeStyle = '#2196F3';
        ctx.lineWidth = 1;
        ctx.setLineDash([4, 3]);
        ctx.strokeRect(lx, 0, rx - lx, h);
        ctx.restore();
      }

      // 돋보기: 1점 찍은 뒤 두 번째 점 미리보기
      if (activeTool === 'magnifier' && pendingPts.current.length === 1 && mousePoint.current) {
        const x0 = manager.timeToX(pendingPts.current[0].time);
        const x1 = manager.timeToX(mousePoint.current.time);
        if (x0 !== null && x1 !== null) {
          const lx = Math.min(x0, x1);
          const rx = Math.max(x0, x1);
          ctx.save();
          ctx.fillStyle = 'rgba(128,222,234,0.12)';
          ctx.fillRect(lx, 0, rx - lx, h);
          ctx.strokeStyle = TOOL_COLORS['magnifier'] ?? '#80DEEA';
          ctx.lineWidth = 1;
          ctx.setLineDash([4, 3]);
          ctx.strokeRect(lx, 0, rx - lx, h);
          ctx.restore();
        }
      }

      // 일반 도구 미리보기 (cursor/pencil/zoom/magnifier 제외)
      if (activeTool !== 'cursor' && activeTool !== 'pencil' && activeTool !== 'zoom' && activeTool !== 'magnifier' && mousePoint.current) {
        const previewPts = [...pendingPts.current, mousePoint.current];
        if (previewPts.length > 0) {
          const preview: Drawing = {
            id: '__preview',
            type: activeTool as DrawingToolId,
            points: previewPts,
            color: TOOL_COLORS[activeTool] ?? '#FFFFFF',
            lineWidth: 1, style: 'dashed',
          };
          ctx.globalAlpha = 0.65;
          renderShape(ctx, preview, manager, w, h, theme);
          ctx.globalAlpha = 1;
        }
        for (const pt of pendingPts.current) {
          const x = manager.timeToX(pt.time), y = manager.priceToY(pt.price);
          if (x !== null && y !== null) {
            ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2);
            ctx.fillStyle = TOOL_COLORS[activeTool] ?? '#FFFFFF';
            ctx.fill();
          }
        }
      }
    }

    ctx.restore();
  }, [drawings, activeTool, manager, theme, enabled, visible, locked, selectedDrawingId]);

  const scheduleRedraw = useCallback(() => {
    cancelAnimationFrame(animRef.current);
    animRef.current = requestAnimationFrame(redraw);
  }, [redraw]);

  // ── Subscribe to LWC viewport changes ────────────────────────────────
  useEffect(() => {
    const unsub = manager.subscribeViewport(scheduleRedraw);
    return unsub;
  }, [manager, scheduleRedraw]);

  useEffect(() => { scheduleRedraw(); }, [scheduleRedraw]);

  // ── Size canvas to match container ────────────────────────────────────
  useEffect(() => {
    const outer = outerRef.current;
    if (!outer) return;

    const applySize = (w: number, h: number) => {
      const canvas = canvasRef.current;
      if (!canvas || w === 0 || h === 0) return;
      cssSize.current = { w, h };
      const dpr = window.devicePixelRatio || 1;
      canvas.width  = Math.round(w * dpr);
      canvas.height = Math.round(h * dpr);
      canvas.style.width  = `${w}px`;
      canvas.style.height = `${h}px`;
      scheduleRedraw();
    };

    const rect = outer.getBoundingClientRect();
    applySize(rect.width, rect.height);

    const ro = new ResizeObserver(([entry]) => {
      const { width, height } = entry.contentRect;
      applySize(width, height);
    });
    ro.observe(outer);
    return () => ro.disconnect();
  }, [scheduleRedraw]);

  // Reset pending when tool changes
  useEffect(() => {
    pendingPts.current  = [];
    mousePoint.current  = null;
    isPencilDragging.current = false;
    pencilPts.current   = [];
    isZoomDragging.current   = false;
    scheduleRedraw();
  }, [activeTool, scheduleRedraw]);

  // ── Coordinate helpers ───────────────────────────────────────────────
  const getPointFromXY = useCallback((clientX: number, clientY: number): DrawingPoint | null => {
    const outer = outerRef.current;
    if (!outer) return null;
    const rect  = outer.getBoundingClientRect();
    const x = clientX - rect.left;
    const y = clientY - rect.top;
    const time  = manager.xToTime(x);
    const price = manager.yToPrice(y);
    return (time !== null && price !== null) ? { time, price } : null;
  }, [manager]);

  const getPoint = useCallback((e: React.PointerEvent | React.MouseEvent): DrawingPoint | null => {
    return getPointFromXY(e.clientX, e.clientY);
  }, [getPointFromXY]);

  const getLocalX = useCallback((e: React.PointerEvent): number => {
    const outer = outerRef.current;
    if (!outer) return 0;
    return e.clientX - outer.getBoundingClientRect().left;
  }, []);

  // ── Pointer handlers (마우스·터치) ───────────────────────────────────
  const handlePointerMove = useCallback((e: React.PointerEvent) => {
    if ((activeTool === 'pencil' || activeTool === 'highlight') && isPencilDragging.current) {
      const pt = getPoint(e);
      if (pt) { pencilPts.current.push(pt); scheduleRedraw(); }
      return;
    }
    if (activeTool === 'zoom' && isZoomDragging.current) {
      zoomCurrentX.current = getLocalX(e);
      scheduleRedraw();
      return;
    }
    const pt = getPoint(e);
    if (!pt) return;
    mousePoint.current = pt;
    scheduleRedraw();
  }, [activeTool, getPoint, getLocalX, scheduleRedraw]);

  const handlePointerLeave = useCallback(() => {
    mousePoint.current = null;
    scheduleRedraw();
  }, [scheduleRedraw]);

  const handlePointerCancel = useCallback(() => {
    mousePoint.current = null;
    if (isPencilDragging.current) {
      isPencilDragging.current = false;
      pencilPts.current = [];
    }
    if (isZoomDragging.current) {
      isZoomDragging.current = false;
    }
    scheduleRedraw();
  }, [scheduleRedraw]);

  const handlePointerDown = useCallback((e: React.PointerEvent) => {
    if (e.button !== 0) return;
    if (!enabled || locked) return;
    if (activeTool === 'pencil' || activeTool === 'highlight') {
      isPencilDragging.current = true;
      pencilPts.current = [];
      const pt = getPoint(e);
      if (pt) pencilPts.current.push(pt);
    }
    if (activeTool === 'zoom') {
      isZoomDragging.current = true;
      const x = getLocalX(e);
      zoomStartX.current   = x;
      zoomCurrentX.current = x;
    }
  }, [enabled, locked, activeTool, getPoint, getLocalX]);

  const handlePointerUp = useCallback((e: React.PointerEvent) => {
    if ((activeTool === 'pencil' || activeTool === 'highlight') && isPencilDragging.current) {
      isPencilDragging.current = false;
      if (pencilPts.current.length >= 3) {
        const pts = pencilPts.current;
        const step = Math.max(1, Math.floor(pts.length / 200));
        const sampled: DrawingPoint[] = [];
        for (let i = 0; i < pts.length; i += step) sampled.push(pts[i]);
        if (sampled[sampled.length - 1] !== pts[pts.length - 1]) sampled.push(pts[pts.length - 1]);

        onAddDrawing({
          id: `d_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
          type: activeTool as DrawingToolId,
          points: sampled,
          color: activeTool === 'highlight' ? '#FFEB3B' : TOOL_COLORS['pencil'],
          lineWidth: activeTool === 'highlight' ? 12 : 1.5,
          style: 'solid',
        });
      }
      pencilPts.current = [];
      scheduleRedraw();
      return;
    }
    if (activeTool === 'zoom' && isZoomDragging.current) {
      isZoomDragging.current = false;
      const x0 = zoomStartX.current;
      const x1 = getLocalX(e);
      scheduleRedraw();
      if (Math.abs(x1 - x0) > 10) {
        onZoom(x0, x1);
      }
    }
  }, [activeTool, onAddDrawing, onZoom, getLocalX, scheduleRedraw]);

  const handleClick = useCallback((e: React.MouseEvent) => {
    if (!enabled || locked) return;
    // drag 기반 툴과 커서는 click 처리 안 함
    if (['pencil','highlight','zoom','cursor'].includes(activeTool)) return;
    // 스텁 툴은 드로잉 생성 안 함 (선택만)
    if (STUB_TOOLS.has(activeTool)) return;

    const pt = getPoint(e);
    if (!pt) return;

    // 텍스트 입력이 필요한 모든 툴 처리
    const TEXT_INPUT_TOOLS = new Set(['text', 'callout', 'fixed_text', 'note_text', 'comment_mark', 'table_tool', 'price_note', 'price_label']);
    if (TEXT_INPUT_TOOLS.has(activeTool)) {
      const outer = outerRef.current!.getBoundingClientRect();
      setTextInput({ x: e.clientX - outer.left, y: e.clientY - outer.top, point: pt, toolType: activeTool });
      setTextVal('');
      return;
    }

    const newPts = [...pendingPts.current, pt];
    const needed = POINTS_NEEDED[activeTool] ?? 1;

    // 돋보기: 두 점으로 시간 구간 지정 → 메인 차트 확대 (TradingView 동작)
    if (activeTool === 'magnifier' && newPts.length >= 2) {
      const x0 = manager.timeToX(newPts[0].time);
      const x1 = manager.timeToX(newPts[1].time);
      pendingPts.current = [];
      scheduleRedraw();
      if (x0 !== null && x1 !== null && Math.abs(x1 - x0) > 10) {
        onZoom(x0, x1);
      }
      return;
    }

    if (newPts.length >= needed) {
      const d: Drawing = {
        id:        `d_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
        type:      activeTool as DrawingToolId,
        points:    newPts.slice(0, needed),
        color:     TOOL_COLORS[activeTool] ?? '#FFFFFF',
        lineWidth: 1,
        style:     'solid',
      };
      onAddDrawing(d);
      pendingPts.current = [];
    } else {
      pendingPts.current = newPts;
    }
    scheduleRedraw();
  }, [enabled, locked, activeTool, getPoint, onAddDrawing, onZoom, manager, scheduleRedraw]);

  const handleDoubleClick = useCallback((e: React.MouseEvent) => {
    if (!enabled || locked) return;
    if (activeTool !== 'polyline') return;
    // dblclick 직전 두 번의 click 이벤트가 점을 2개 추가함
    // 그 중 마지막 1개(dblclick 2번째 click)를 제거하고 확정
    const pts = pendingPts.current;
    if (pts.length >= 2) {
      pts.pop(); // 더블클릭의 2번째 click으로 추가된 중복 제거
      const finalPts = [...pts];
      pendingPts.current = [];
      onAddDrawing({
        id:        `d_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
        type:      'polyline' as DrawingToolId,
        points:    finalPts,
        color:     TOOL_COLORS['polyline'] ?? '#FF9800',
        lineWidth: 1.5,
        style:     'solid',
      });
      scheduleRedraw();
    }
  }, [enabled, locked, activeTool, onAddDrawing, scheduleRedraw]);

  const commitText = useCallback(() => {
    if (!textInput) return;
    if (textVal.trim()) {
      const toolType = textInput.toolType ?? 'text';
      onAddDrawing({
        id: `d_${Date.now()}`,
        type: toolType as DrawingToolId,
        points: [textInput.point],
        color: TOOL_COLORS[toolType] ?? TOOL_COLORS['text'] ?? '#FFEB3B',
        lineWidth: 1, style: 'solid',
        text: textVal.trim(),
      });
    }
    setTextInput(null);
  }, [textInput, textVal, onAddDrawing]);

  const isInteractive = enabled && !locked && activeTool !== 'cursor';

  // zoom 드래그 중에는 cursor 변경
  const getCursor = () => {
    if (!isInteractive) return 'default';
    if (activeTool === 'zoom') return isZoomDragging.current ? 'ew-resize' : 'zoom-in';
    if (activeTool === 'magnifier') return 'zoom-in';
    if (activeTool === 'pencil') return 'crosshair';
    return 'crosshair';
  };

  return (
    <div
      ref={outerRef}
      style={{
        position:      'absolute',
        inset:         0,
        pointerEvents: 'none',
        zIndex:        10,
      }}
    >
      <canvas
        ref={canvasRef}
        style={{
          display:       'block',
          position:      'absolute',
          top:           0,
          left:          0,
          pointerEvents: isInteractive ? 'auto' : 'none',
          cursor:        getCursor(),
          touchAction:   isInteractive ? 'none' : 'auto',
        }}
        onPointerMove={handlePointerMove}
        onPointerLeave={handlePointerLeave}
        onPointerCancel={handlePointerCancel}
        onPointerDown={handlePointerDown}
        onPointerUp={handlePointerUp}
        onClick={handleClick}
        onDoubleClick={handleDoubleClick}
      />

      {textInput && (
        <input
          autoFocus
          style={{
            position:     'absolute',
            left:         textInput.x,
            top:          textInput.y - 22,
            background:   'rgba(0,0,0,0.8)',
            color:        '#fff',
            border:       '1px solid #FFD',
            borderRadius: 3,
            padding:      '2px 6px',
            fontSize:     13,
            minWidth:     90,
            pointerEvents:'auto',
            zIndex:       20,
          }}
          value={textVal}
          onChange={e => setTextVal(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') commitText(); if (e.key === 'Escape') setTextInput(null); }}
          onBlur={commitText}
          placeholder="텍스트 입력…"
        />
      )}
    </div>
  );
};

export default DrawingCanvas;
