goldenChat base source add
This commit is contained in:
@@ -0,0 +1,627 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import type { ChartMode } from '../types';
|
||||
|
||||
export interface DrawingToolbarProps {
|
||||
activeTool: string;
|
||||
mode: ChartMode;
|
||||
onTool: (tool: string) => void;
|
||||
onClearAll: () => void;
|
||||
onUndo: () => void;
|
||||
locked: boolean;
|
||||
visible: boolean;
|
||||
onToggleLock: () => void;
|
||||
onToggleEye: () => void;
|
||||
// 신규 확장 props (optional — App.tsx 점진 적용)
|
||||
indicatorsVisible?: boolean;
|
||||
magnetMode?: 'off' | 'weak' | 'strong';
|
||||
snapToIndicator?: boolean;
|
||||
drawingCount?: number;
|
||||
indicatorCount?: number;
|
||||
onMagnetMode?: (m: 'off' | 'weak' | 'strong') => void;
|
||||
onSnapToIndicator?: (v: boolean) => void;
|
||||
onHideDrawings?: () => void;
|
||||
onHideIndicators?: () => void;
|
||||
onHideAll?: () => void;
|
||||
onClearIndicators?: () => void;
|
||||
keepLockedOnDelete?: boolean;
|
||||
onToggleKeepLocked?: () => void;
|
||||
}
|
||||
|
||||
// ─── SVG 헬퍼 ──────────────────────────────────────────────────────────────
|
||||
const S = (p: React.SVGProps<SVGSVGElement>) =>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"
|
||||
stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" {...p}/>;
|
||||
|
||||
// ─── 아이콘 ────────────────────────────────────────────────────────────────
|
||||
const IcCursor = () => <S><path d="M3 2L3 14L6.5 10.5L9.5 16L11 15.3L8 9.5L12.5 9.5Z" strokeWidth="1.4" fill="currentColor" fillOpacity="0.15"/></S>;
|
||||
// 업비트 즐겨찾기 툴바 아이콘
|
||||
const IcUpbitHline = () => <S strokeWidth="1.5"><line x1="2" y1="9" x2="16" y2="9"/><circle cx="9" cy="9" r="1.6" fill="currentColor" stroke="none"/></S>;
|
||||
const IcUpbitVline = () => <S strokeWidth="1.5"><line x1="9" y1="2" x2="9" y2="16"/><circle cx="9" cy="9" r="1.6" fill="currentColor" stroke="none"/></S>;
|
||||
const IcUpbitPeriod = () => <S strokeWidth="1.4"><line x1="5" y1="3" x2="5" y2="15"/><line x1="13" y1="3" x2="13" y2="15"/><circle cx="5" cy="9" r="1.4" fill="currentColor" stroke="none"/><circle cx="13" cy="9" r="1.4" fill="currentColor" stroke="none"/><line x1="6.5" y1="9" x2="11.5" y2="9"/><polyline points="6.5,10 5,9 6.5,8" strokeWidth="1.2"/><polyline points="11.5,10 13,9 11.5,8" strokeWidth="1.2"/></S>;
|
||||
const IcUpbitFibTz = () => <S strokeWidth="1.4"><line x1="4" y1="4" x2="4" y2="16"/><line x1="8" y1="6" x2="8" y2="16" strokeOpacity="0.75"/><line x1="12" y1="3" x2="12" y2="16" strokeOpacity="0.9"/><line x1="15" y1="8" x2="15" y2="16" strokeOpacity="0.55"/><circle cx="4" cy="10" r="1.3" fill="currentColor" stroke="none"/><circle cx="8" cy="11" r="1.3" fill="currentColor" stroke="none"/><circle cx="12" cy="9" r="1.3" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="1.3" fill="currentColor" stroke="none"/></S>;
|
||||
const IcFavorite = () => <S strokeWidth="1.3"><path d="M9 2.5 L11 7 L16 7.5 L12.5 11 L13.5 16 L9 13.5 L4.5 16 L5.5 11 L2 7.5 L7 7 Z" fill="currentColor" fillOpacity="0.25"/></S>;
|
||||
// 선 그룹
|
||||
const IcTrendline = () => <S strokeWidth="1.5"><line x1="3" y1="15" x2="15" y2="3"/><circle cx="3" cy="15" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="3" r="1.5" fill="currentColor" stroke="none"/></S>;
|
||||
const IcRay = () => <S strokeWidth="1.4"><line x1="3" y1="14" x2="16" y2="5"/><circle cx="3" cy="14" r="1.5" fill="currentColor" stroke="none"/><polyline points="14,5.5 16,5 15.5,7" strokeWidth="1.3"/></S>;
|
||||
const IcInfoLine = () => <S strokeWidth="1.4"><line x1="1" y1="9" x2="17" y2="9"/><rect x="5" y="4" width="8" height="4" rx="1" strokeWidth="1.2"/><text x="9" y="7.5" fontSize="4.5" fill="currentColor" stroke="none" textAnchor="middle">i</text></S>;
|
||||
const IcExtLine = () => <S strokeWidth="1.5"><line x1="1" y1="15" x2="17" y2="3"/><polyline points="15,3 17,3 17,5" strokeWidth="1.3"/><polyline points="3,15 1,15 1,13" strokeWidth="1.3"/></S>;
|
||||
const IcTrendAngle = () => <S strokeWidth="1.4"><line x1="3" y1="15" x2="15" y2="7"/><path d="M3 15 Q8 15 10 11" strokeDasharray="2 1.5" strokeOpacity="0.7"/><text x="9" y="15" fontSize="6" fill="currentColor" stroke="none">θ</text></S>;
|
||||
const IcHline = () => <S strokeWidth="1.4"><line x1="1" y1="9" x2="17" y2="9"/><line x1="1" y1="5" x2="17" y2="5" strokeOpacity="0.4" strokeDasharray="2 2"/><line x1="1" y1="13" x2="17" y2="13" strokeOpacity="0.4" strokeDasharray="2 2"/></S>;
|
||||
const IcHlineRay = () => <S strokeWidth="1.4"><line x1="3" y1="9" x2="17" y2="9"/><circle cx="3" cy="9" r="1.5" fill="currentColor" stroke="none"/><polyline points="15,7 17,9 15,11" strokeWidth="1.3"/></S>;
|
||||
const IcVline = () => <S strokeWidth="1.4"><line x1="9" y1="1" x2="9" y2="17"/><polyline points="7,4 9,1 11,4"/><polyline points="7,14 9,17 11,14"/></S>;
|
||||
const IcCrossLine = () => <S strokeWidth="1.4"><line x1="2" y1="9" x2="16" y2="9"/><line x1="9" y1="2" x2="9" y2="16"/><circle cx="9" cy="9" r="1.8" fill="currentColor" stroke="none" fillOpacity="0.7"/></S>;
|
||||
const IcChannel = () => <S strokeWidth="1.4"><line x1="2" y1="13" x2="16" y2="5"/><line x1="2" y1="16" x2="16" y2="8" strokeOpacity="0.5" strokeDasharray="3 2"/></S>;
|
||||
const IcParaCh = () => <S strokeWidth="1.4"><line x1="2" y1="12" x2="16" y2="5"/><line x1="2" y1="15" x2="16" y2="8"/></S>;
|
||||
const IcDisjCh = () => <S strokeWidth="1.3"><line x1="2" y1="13" x2="9" y2="8"/><line x1="2" y1="15" x2="9" y2="10" strokeOpacity="0.6"/><line x1="10" y1="10" x2="16" y2="6"/><line x1="10" y1="13" x2="16" y2="9" strokeOpacity="0.6"/></S>;
|
||||
const IcFlatTop = () => <S strokeWidth="1.4"><line x1="2" y1="5" x2="16" y2="5"/><path d="M2 5 L4 9 L7 5 L10 11 L13 5 L16 5" strokeOpacity="0.7"/><line x1="2" y1="14" x2="16" y2="14" strokeOpacity="0.5"/></S>;
|
||||
// 피보나치 그룹
|
||||
const IcFib = () => <S strokeWidth="1.3"><line x1="2" y1="3" x2="16" y2="3"/><line x1="2" y1="7" x2="16" y2="7" strokeOpacity="0.6"/><line x1="2" y1="9" x2="16" y2="9"/><line x1="2" y1="12" x2="16" y2="12" strokeOpacity="0.6"/><line x1="2" y1="15" x2="16" y2="15"/><circle cx="2" cy="3" r="1.5" fill="currentColor" stroke="none"/><circle cx="2" cy="15" r="1.5" fill="currentColor" stroke="none"/></S>;
|
||||
const IcFibExt = () => <S strokeWidth="1.3"><line x1="2" y1="6" x2="16" y2="6" strokeOpacity="0.5"/><line x1="2" y1="9" x2="16" y2="9"/><line x1="2" y1="12" x2="16" y2="12" strokeOpacity="0.5"/><line x1="2" y1="15" x2="16" y2="15"/><line x1="2" y1="3" x2="16" y2="3" strokeOpacity="0.3" strokeDasharray="2 2"/><circle cx="2" cy="9" r="1.5" fill="currentColor" stroke="none"/><circle cx="2" cy="15" r="1.5" fill="currentColor" stroke="none"/></S>;
|
||||
const IcFibCh = () => <S strokeWidth="1.3"><line x1="2" y1="14" x2="16" y2="5"/><line x1="2" y1="11" x2="16" y2="2" strokeOpacity="0.6"/><line x1="2" y1="17" x2="16" y2="8" strokeOpacity="0.4"/></S>;
|
||||
const IcFibTZ = () => <S strokeWidth="1.3"><line x1="2" y1="2" x2="2" y2="16"/><line x1="5" y1="2" x2="5" y2="16" strokeOpacity="0.7"/><line x1="8" y1="2" x2="8" y2="16" strokeOpacity="0.5"/><line x1="11" y1="2" x2="11" y2="16" strokeOpacity="0.7"/><line x1="15" y1="2" x2="15" y2="16" strokeOpacity="0.4"/></S>;
|
||||
const IcFibFan = () => <S strokeWidth="1.3"><line x1="2" y1="16" x2="16" y2="3"/><line x1="2" y1="16" x2="16" y2="7" strokeOpacity="0.7"/><line x1="2" y1="16" x2="16" y2="11" strokeOpacity="0.5"/><line x1="2" y1="16" x2="16" y2="14" strokeOpacity="0.3"/></S>;
|
||||
const IcFibTrendT = () => <S strokeWidth="1.3"><line x1="3" y1="14" x2="15" y2="6"/><line x1="5" y1="6" x2="5" y2="14" strokeOpacity="0.6"/><line x1="8" y1="6" x2="8" y2="14" strokeOpacity="0.7"/><line x1="11" y1="6" x2="11" y2="14" strokeOpacity="0.5"/></S>;
|
||||
const IcFibCircles = () => <S strokeWidth="1.3"><circle cx="9" cy="9" r="2"/><circle cx="9" cy="9" r="5"/><circle cx="9" cy="9" r="7.5" strokeOpacity="0.4"/></S>;
|
||||
const IcFibSpiral = () => <S strokeWidth="1.3"><path d="M9 9 Q12 6 13 9 Q14 13 9 14 Q4 14 4 9 Q4 4 9 3 Q15 3 16 9" strokeOpacity="0.8"/></S>;
|
||||
const IcFibArc = () => <S strokeWidth="1.3"><line x1="2" y1="14" x2="16" y2="14" strokeOpacity="0.5"/><path d="M2 14 Q9 5 16 14" strokeOpacity="0.9"/><path d="M2 14 Q9 2 16 14" strokeOpacity="0.6"/></S>;
|
||||
const IcFibWedge = () => <S strokeWidth="1.3"><path d="M2 15 L16 9 L16 15 Z"/><path d="M2 15 L16 6" strokeOpacity="0.5"/></S>;
|
||||
const IcPitchfork = () => <S strokeWidth="1.4"><line x1="3" y1="9" x2="10" y2="7"/><line x1="10" y1="7" x2="16" y2="4"/><line x1="10" y1="7" x2="16" y2="10"/><circle cx="3" cy="9" r="1.5" fill="currentColor" stroke="none"/></S>;
|
||||
const IcPitchSchiff = () => <S strokeWidth="1.4"><line x1="3" y1="10" x2="9" y2="7"/><line x1="9" y1="7" x2="16" y2="3" strokeDasharray="3 2"/><line x1="9" y1="7" x2="16" y2="11"/></S>;
|
||||
const IcPitchInside = () => <S strokeWidth="1.4"><line x1="3" y1="9" x2="11" y2="7"/><line x1="11" y1="7" x2="16" y2="5" strokeOpacity="0.6"/><line x1="11" y1="7" x2="16" y2="9" strokeOpacity="0.6"/></S>;
|
||||
const IcGannBox = () => <S strokeWidth="1.3"><rect x="3" y="4" width="12" height="10" rx="0.5"/><line x1="3" y1="4" x2="15" y2="14" strokeOpacity="0.6"/><line x1="15" y1="4" x2="3" y2="14" strokeOpacity="0.4"/></S>;
|
||||
const IcGannSq = () => <S strokeWidth="1.3"><rect x="4" y="4" width="10" height="10" rx="0.5"/><line x1="4" y1="4" x2="14" y2="14" strokeOpacity="0.6"/><line x1="14" y1="4" x2="4" y2="14" strokeOpacity="0.6"/></S>;
|
||||
const IcGannFan = () => <S strokeWidth="1.3"><line x1="2" y1="16" x2="16" y2="2"/><line x1="2" y1="16" x2="16" y2="4" strokeOpacity="0.6"/><line x1="2" y1="16" x2="16" y2="8" strokeOpacity="0.5"/><line x1="2" y1="16" x2="16" y2="13" strokeOpacity="0.4"/></S>;
|
||||
// 패턴 그룹
|
||||
const IcXABCD = () => <S strokeWidth="1.4"><path d="M2 15 L6 5 L10 13 L13 7 L16 11"/><circle cx="2" cy="15" r="1.2" fill="currentColor" stroke="none"/><circle cx="6" cy="5" r="1.2" fill="currentColor" stroke="none"/><circle cx="10" cy="13" r="1.2" fill="currentColor" stroke="none"/><circle cx="13" cy="7" r="1.2" fill="currentColor" stroke="none"/><circle cx="16" cy="11" r="1.2" fill="currentColor" stroke="none"/></S>;
|
||||
const IcCypher = () => <S strokeWidth="1.4"><path d="M2 14 L7 4 L11 12 L14 6 L16 10"/><circle cx="2" cy="14" r="1.2" fill="currentColor" stroke="none"/><circle cx="7" cy="4" r="1.2" fill="currentColor" stroke="none"/><circle cx="11" cy="12" r="1.2" fill="currentColor" stroke="none"/><circle cx="14" cy="6" r="1.2" fill="currentColor" stroke="none"/><circle cx="16" cy="10" r="1.2" fill="currentColor" stroke="none"/></S>;
|
||||
const IcABCD = () => <S strokeWidth="1.4"><path d="M3 14 L8 4 L13 12 L16 6"/><circle cx="3" cy="14" r="1.2" fill="currentColor" stroke="none"/><circle cx="8" cy="4" r="1.2" fill="currentColor" stroke="none"/><circle cx="13" cy="12" r="1.2" fill="currentColor" stroke="none"/><circle cx="16" cy="6" r="1.2" fill="currentColor" stroke="none"/></S>;
|
||||
const IcHeadShould = () => <S strokeWidth="1.4"><path d="M2 13 L5 8 L7 11 L9 4 L11 11 L13 8 L16 13"/></S>;
|
||||
const IcWedgePat = () => <S strokeWidth="1.4"><path d="M2 5 L16 7"/><path d="M2 14 L16 11"/><path d="M5 5.4 L5 13.2" strokeOpacity="0.4" strokeDasharray="2 2"/><path d="M9 5.9 L9 12.5" strokeOpacity="0.4" strokeDasharray="2 2"/><path d="M13 6.4 L13 11.8" strokeOpacity="0.4" strokeDasharray="2 2"/></S>;
|
||||
const IcThreeDrives = () => <S strokeWidth="1.4"><path d="M2 13 L5 8 L8 11 L11 6 L14 9 L16 6"/><circle cx="5" cy="8" r="1.2" fill="currentColor" stroke="none"/><circle cx="11" cy="6" r="1.2" fill="currentColor" stroke="none"/><circle cx="16" cy="6" r="1.2" fill="currentColor" stroke="none"/></S>;
|
||||
const IcElliottImp = () => <S strokeWidth="1.3"><path d="M2 14 L5 6 L8 12 L11 4 L14 10 L16 8"/><text x="3" y="17" fontSize="4" fill="currentColor" stroke="none">1 2 3 4 5</text></S>;
|
||||
const IcElliottCorr = () => <S strokeWidth="1.3"><path d="M2 6 L7 14 L12 8 L16 14"/><text x="3" y="4" fontSize="4.5" fill="currentColor" stroke="none">A B C</text></S>;
|
||||
const IcElliottTri = () => <S strokeWidth="1.3"><path d="M2 8 L5 14 L8 9 L11 14 L14 10 L16 14"/><text x="2" y="4" fontSize="3.5" fill="currentColor" stroke="none">ABCDE</text></S>;
|
||||
const IcElliottDbl = () => <S strokeWidth="1.3"><path d="M2 14 L5 8 L8 13 L11 7 L14 12 L16 10"/><text x="2" y="4" fontSize="4" fill="currentColor" stroke="none">W X Y</text></S>;
|
||||
// 프로젝션 그룹
|
||||
const IcLongPos = () => <S strokeWidth="1.3"><rect x="3" y="5" width="12" height="5" rx="0.5" fill="rgba(76,175,80,0.3)" stroke="#4CAF50"/><rect x="3" y="10" width="12" height="4" rx="0.5" fill="rgba(239,83,80,0.2)" stroke="#EF5350" strokeDasharray="2 1.5"/><line x1="3" y1="10" x2="15" y2="10" stroke="#9E9E9E"/></S>;
|
||||
const IcShortPos = () => <S strokeWidth="1.3"><rect x="3" y="9" width="12" height="5" rx="0.5" fill="rgba(239,83,80,0.3)" stroke="#EF5350"/><rect x="3" y="5" width="12" height="4" rx="0.5" fill="rgba(76,175,80,0.2)" stroke="#4CAF50" strokeDasharray="2 1.5"/><line x1="3" y1="9" x2="15" y2="9" stroke="#9E9E9E"/></S>;
|
||||
const IcForecast = () => <S strokeWidth="1.3"><line x1="2" y1="14" x2="9" y2="8"/><line x1="9" y1="8" x2="16" y2="5" strokeDasharray="3 2" strokeOpacity="0.7"/><circle cx="9" cy="8" r="2" fill="currentColor" stroke="none" fillOpacity="0.5"/></S>;
|
||||
const IcBarPattern = () => <S strokeWidth="1.3"><rect x="2" y="3" width="6" height="12" rx="0.5" strokeOpacity="0.6"/><rect x="2" y="5" width="6" height="8" rx="0.5" fill="currentColor" fillOpacity="0.2"/><line x1="5" y1="3" x2="5" y2="5"/><line x1="5" y1="13" x2="5" y2="15"/><line x1="10" y1="3" x2="16" y2="6" strokeDasharray="2 1.5" strokeOpacity="0.6"/><line x1="10" y1="13" x2="16" y2="14" strokeDasharray="2 1.5" strokeOpacity="0.6"/></S>;
|
||||
const IcGhostFeed = () => <S strokeWidth="1.3"><rect x="2" y="5" width="6" height="8" rx="0.5" strokeOpacity="0.4" strokeDasharray="2 1.5"/><rect x="10" y="4" width="6" height="10" rx="0.5" strokeOpacity="0.4" strokeDasharray="2 1.5"/><line x1="5" y1="5" x2="5" y2="3" strokeOpacity="0.4"/><line x1="13" y1="4" x2="13" y2="2" strokeOpacity="0.4"/></S>;
|
||||
const IcProjection = () => <S strokeWidth="1.3"><path d="M2 13 L8 8"/><path d="M8 8 Q12 5 16 7" strokeDasharray="3 2" strokeOpacity="0.7"/><circle cx="8" cy="8" r="2" fill="currentColor" stroke="none" fillOpacity="0.5"/><circle cx="2" cy="13" r="1.5" fill="currentColor" stroke="none"/></S>;
|
||||
const IcVWAP = () => <S strokeWidth="1.3"><path d="M2 9 Q6 6 9 9 Q12 12 16 9" strokeOpacity="0.8"/><circle cx="2" cy="9" r="2" fill="currentColor" stroke="none" fillOpacity="0.7"/><line x1="2" y1="6" x2="2" y2="12" strokeOpacity="0.5"/></S>;
|
||||
const IcVolProfile = () => <S strokeWidth="1.3"><line x1="3" y1="2" x2="3" y2="16"/><line x1="3" y1="5" x2="9" y2="5" strokeWidth="2.5" strokeOpacity="0.5"/><line x1="3" y1="8" x2="14" y2="8" strokeWidth="2.5" strokeOpacity="0.7"/><line x1="3" y1="11" x2="11" y2="11" strokeWidth="2.5" strokeOpacity="0.5"/><line x1="3" y1="14" x2="7" y2="14" strokeWidth="2.5" strokeOpacity="0.4"/></S>;
|
||||
const IcPriceRange = () => <S strokeWidth="1.4"><line x1="2" y1="9" x2="16" y2="9"/><line x1="2" y1="6" x2="2" y2="12"/><line x1="16" y1="6" x2="16" y2="12"/></S>;
|
||||
// 도형 그룹
|
||||
const IcRect = () => <S strokeWidth="1.4"><rect x="3" y="5" width="12" height="8" rx="0.5"/></S>;
|
||||
const IcRotRect = () => <S strokeWidth="1.4"><path d="M9 2 L16 7 L13 16 L3 14 L2 7 Z"/></S>;
|
||||
const IcCircle = () => <S strokeWidth="1.4"><circle cx="9" cy="9" r="6.5"/></S>;
|
||||
const IcEllipse = () => <S strokeWidth="1.4"><ellipse cx="9" cy="9" rx="7" ry="4.5"/></S>;
|
||||
const IcTriangle = () => <S strokeWidth="1.4"><path d="M9 3 L16 15 L2 15 Z"/></S>;
|
||||
const IcParallelo = () => <S strokeWidth="1.4"><path d="M4 14 L7 4 L16 4 L13 14 Z"/></S>;
|
||||
const IcPolyline = () => <S strokeWidth="1.4"><path d="M2 14 L6 6 L10 11 L14 5"/><circle cx="2" cy="14" r="1.5" fill="currentColor" stroke="none"/><circle cx="14" cy="5" r="1.5" fill="currentColor" stroke="none"/></S>;
|
||||
const IcArc = () => <S strokeWidth="1.4"><path d="M2 15 Q9 1 16 15"/></S>;
|
||||
// 브러시 그룹
|
||||
const IcPencil = () => <S strokeWidth="1.4"><path d="M3 15L3.5 11L13 2.5Q14.5 1 16 2.5Q17.5 4 16 5.5L6.5 15Z"/><line x1="11" y1="4" x2="14" y2="7"/></S>;
|
||||
const IcHighlight = () => <S><path d="M3 13 Q9 8 15 13" strokeOpacity="0.5" strokeWidth="5" stroke="#FFEB3B" fill="none"/><path d="M3 13 Q9 8 15 13" strokeWidth="1.4" stroke="currentColor" fill="none"/></S>;
|
||||
const IcArrowMark = () => <S strokeWidth="1.4"><line x1="3" y1="14" x2="15" y2="5"/><circle cx="3" cy="14" r="1.5" fill="currentColor" stroke="none"/><polyline points="11,4 15,5 14,9" strokeWidth="1.3" fill="currentColor" fillOpacity="0.5"/></S>;
|
||||
const IcArrowAnch = () => <S strokeWidth="1.4"><circle cx="4" cy="14" r="2" fill="currentColor" stroke="none" fillOpacity="0.5"/><line x1="4" y1="12" x2="14" y2="5" strokeDasharray="2 1.5"/><polyline points="11,4 14,5 13,8" fill="currentColor" fillOpacity="0.6"/></S>;
|
||||
const IcArrowUp = () => <S strokeWidth="1.4"><path d="M9 14 L9 7"/><path d="M5 10 L9 6 L13 10" fill="currentColor" fillOpacity="0.5"/></S>;
|
||||
const IcArrowDown = () => <S strokeWidth="1.4"><path d="M9 4 L9 11"/><path d="M5 8 L9 12 L13 8" fill="currentColor" fillOpacity="0.5"/></S>;
|
||||
const IcArrowLeft = () => <S strokeWidth="1.4"><path d="M14 9 L7 9"/><path d="M10 5 L6 9 L10 13" fill="currentColor" fillOpacity="0.5"/></S>;
|
||||
const IcArrowRight = () => <S strokeWidth="1.4"><path d="M4 9 L11 9"/><path d="M8 5 L12 9 L8 13" fill="currentColor" fillOpacity="0.5"/></S>;
|
||||
// 텍스트 그룹
|
||||
const IcText = () => <svg width="18" height="18" viewBox="0 0 18 18" fill="none"><text x="3" y="14" fontSize="13" fontWeight="700" fill="currentColor" fontFamily="sans-serif">T</text></svg>;
|
||||
const IcFixedText = () => <svg width="18" height="18" viewBox="0 0 18 18" fill="none"><text x="3" y="14" fontSize="13" fontWeight="700" fill="currentColor" fontFamily="sans-serif">T</text><circle cx="15" cy="15" r="3" fill="currentColor" fillOpacity="0.5" stroke="none"/></svg>;
|
||||
const IcNote = () => <S strokeWidth="1.3"><rect x="2" y="3" width="14" height="12" rx="1.5"/><line x1="5" y1="7" x2="13" y2="7" strokeOpacity="0.7"/><line x1="5" y1="9.5" x2="13" y2="9.5" strokeOpacity="0.7"/><line x1="5" y1="12" x2="10" y2="12" strokeOpacity="0.7"/><circle cx="16" cy="3" r="2.5" stroke="none" fill="currentColor" fillOpacity="0.5"/></S>;
|
||||
const IcPriceNote = () => <S strokeWidth="1.3"><line x1="2" y1="9" x2="8" y2="9"/><rect x="8" y="5" width="8" height="8" rx="1"/><text x="12" y="10.5" fontSize="5" fill="currentColor" stroke="none" textAnchor="middle">$</text></S>;
|
||||
const IcPin = () => <S strokeWidth="1.3"><path d="M9 3 Q12 3 12 7 Q12 10 9 15 Q6 10 6 7 Q6 3 9 3 Z"/><circle cx="9" cy="7" r="1.8" stroke="none" fill="currentColor" fillOpacity="0.6"/></S>;
|
||||
const IcTable = () => <S strokeWidth="1.3"><rect x="2" y="3" width="14" height="12" rx="1"/><line x1="2" y1="7" x2="16" y2="7"/><line x1="2" y1="11" x2="16" y2="11"/><line x1="7" y1="3" x2="7" y2="15"/><line x1="12" y1="3" x2="12" y2="15"/></S>;
|
||||
const IcCallout = () => <S strokeWidth="1.3"><rect x="2" y="3" width="12" height="8" rx="1.5"/><path d="M7 11 L5 15 L10 11"/></S>;
|
||||
const IcComment = () => <S strokeWidth="1.3"><path d="M2 3 Q2 2 3 2 L15 2 Q16 2 16 3 L16 11 Q16 12 15 12 L8 12 L5 15 L5 12 L3 12 Q2 12 2 11 Z"/></S>;
|
||||
const IcPriceLabel = () => <S strokeWidth="1.3"><path d="M2 9 L6 9 L8 6 L12 6 L14 9 L12 12 L8 12 L6 9"/></S>;
|
||||
const IcGuide = () => <S strokeWidth="1.3"><line x1="1" y1="9" x2="17" y2="9" strokeDasharray="3 2" strokeOpacity="0.8"/><circle cx="9" cy="9" r="3" strokeOpacity="0.6"/></S>;
|
||||
const IcFlag = () => <S strokeWidth="1.3"><line x1="5" y1="3" x2="5" y2="15"/><path d="M5 3 L15 6 L5 9 Z" fill="currentColor" fillOpacity="0.4"/></S>;
|
||||
// 측정 그룹
|
||||
const IcMeasure = () => <S strokeWidth="1.4"><line x1="2" y1="9" x2="16" y2="9"/><line x1="2" y1="6" x2="2" y2="12"/><line x1="16" y1="6" x2="16" y2="12"/></S>;
|
||||
const IcDateRange = () => <S strokeWidth="1.3"><rect x="2" y="4" width="14" height="12" rx="1"/><line x1="2" y1="8" x2="16" y2="8"/><line x1="6" y1="2" x2="6" y2="6"/><line x1="12" y1="2" x2="12" y2="6"/></S>;
|
||||
const IcDatePriceR = () => <S strokeWidth="1.3"><rect x="2" y="3" width="14" height="12" rx="1" strokeOpacity="0.7"/><line x1="2" y1="9" x2="16" y2="9" strokeOpacity="0.6"/><line x1="9" y1="3" x2="9" y2="15" strokeOpacity="0.6"/></S>;
|
||||
const IcDataWindow = () => <S strokeWidth="1.3"><rect x="2" y="3" width="14" height="12" rx="1"/><line x1="2" y1="7" x2="16" y2="7"/><line x1="7" y1="3" x2="7" y2="7"/></S>;
|
||||
const IcZoom = () => <S strokeWidth="1.4"><circle cx="8" cy="8" r="5"/><line x1="12" y1="12" x2="16" y2="16"/><line x1="6" y1="8" x2="10" y2="8"/><line x1="8" y1="6" x2="8" y2="10"/></S>;
|
||||
const IcMagnifier = () => <S strokeWidth="1.4"><circle cx="8" cy="8" r="5"/><line x1="12" y1="12" x2="16" y2="16"/></S>;
|
||||
// 하단 버튼 아이콘
|
||||
const IcMagnetWeak = () => <S strokeWidth="1.4"><path d="M5 14 L5 8 Q5 3 9 3 Q13 3 13 8 L13 14"/><line x1="3" y1="14" x2="7" y2="14" strokeWidth="2"/><line x1="11" y1="14" x2="15" y2="14" strokeWidth="2"/></S>;
|
||||
const IcMagnetStrg = () => <S strokeWidth="1.4"><path d="M5 14 L5 8 Q5 3 9 3 Q13 3 13 8 L13 14"/><line x1="3" y1="14" x2="7" y2="14" strokeWidth="2"/><line x1="11" y1="14" x2="15" y2="14" strokeWidth="2"/><line x1="9" y1="9" x2="9" y2="13" strokeDasharray="1.5 1" strokeOpacity="0.7"/></S>;
|
||||
const IcLock = () => <S strokeWidth="1.4"><rect x="4" y="9" width="10" height="7" rx="1"/><path d="M6 9V6Q6 3 9 3Q12 3 12 6V9"/></S>;
|
||||
const IcEye = () => <S strokeWidth="1.4"><path d="M1 9Q5 3 9 3Q13 3 17 9Q13 15 9 15Q5 15 1 9Z"/><circle cx="9" cy="9" r="2.5"/></S>;
|
||||
const IcEyeOff = () => <S strokeWidth="1.4"><path d="M1 9Q5 3 9 3Q13 3 17 9Q13 15 9 15Q5 15 1 9Z" strokeOpacity="0.3"/><line x1="3" y1="3" x2="15" y2="15"/></S>;
|
||||
const IcTrash = () => <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"><line x1="3" y1="5" x2="13" y2="5"/><path d="M6 5V3Q6 2 8 2Q10 2 10 3V5"/><path d="M4 5L4.5 14Q4.5 14.5 5 14.5L11 14.5Q11.5 14.5 11.5 14L12 5"/></svg>;
|
||||
const IcUndo = () => <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="M3 7C3 4 5.5 2 8.5 2C11.5 2 14 4 14 7C14 10 11.5 12 8.5 12"/><polyline points="1,5 3,7 5,5"/></svg>;
|
||||
const IcLayers = () => <S strokeWidth="1.4"><path d="M2 12 L9 15 L16 12"/><path d="M2 9 L9 12 L16 9"/><path d="M2 6 L9 9 L16 6"/></S>;
|
||||
|
||||
// ─── 타입 ──────────────────────────────────────────────────────────────────
|
||||
type FlyoutItem =
|
||||
| { kind: 'tool'; id: string; title: string; shortcut?: string; icon: React.ReactNode }
|
||||
| { kind: 'sep'; label?: string };
|
||||
|
||||
interface GroupDef {
|
||||
id: number;
|
||||
label: string;
|
||||
defaultIcon: React.ReactNode;
|
||||
items: FlyoutItem[];
|
||||
}
|
||||
|
||||
// ─── 그룹 정의 ──────────────────────────────────────────────────────────────
|
||||
const T = (id: string, title: string, icon: React.ReactNode, shortcut?: string): FlyoutItem =>
|
||||
({ kind: 'tool', id, title, icon, shortcut });
|
||||
const SEP = (label?: string): FlyoutItem => ({ kind: 'sep', label });
|
||||
|
||||
/** 업비트 차트 좌측 즐겨찾기 플로팅 툴바와 동일 4종 */
|
||||
export const FAVORITE_DRAWING_TOOLS = ['hline', 'vline', 'date_range', 'fibtz'] as const;
|
||||
|
||||
const GROUPS: GroupDef[] = [
|
||||
{
|
||||
id: 0, label: '즐겨찾기', defaultIcon: <IcFavorite />,
|
||||
items: [
|
||||
T('hline', '가로줄', <IcUpbitHline />, '⌥H'),
|
||||
T('vline', '세로줄', <IcUpbitVline />, '⌥V'),
|
||||
T('date_range', '기간', <IcUpbitPeriod />),
|
||||
T('fibtz', '피보나치 타임존', <IcUpbitFibTz />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1, label: '라인', defaultIcon: <IcTrendline />,
|
||||
items: [
|
||||
T('trendline', '추세줄', <IcTrendline />, '⌥T'),
|
||||
T('ray', '빛', <IcRay />),
|
||||
T('info_line', '인포 라인', <IcInfoLine />),
|
||||
T('extended_line', '익스텐디드 라인', <IcExtLine />),
|
||||
T('trend_angle', '추세각', <IcTrendAngle />),
|
||||
SEP(),
|
||||
T('hline', '가로줄', <IcHline />, '⌥H'),
|
||||
T('hline_ray', '가로빛', <IcHlineRay />, '⌥J'),
|
||||
T('vline', '수직선', <IcVline />, '⌥V'),
|
||||
T('cross_line', '크로스 라인', <IcCrossLine />, '⌥C'),
|
||||
SEP('채널'),
|
||||
T('channel', '추세 채널', <IcChannel />),
|
||||
T('parallel_channel','패러렐 채널', <IcParaCh />),
|
||||
T('disjoint_channel','디스조인트 채널',<IcDisjCh />),
|
||||
T('flat_top_bottom','플랫 탑/바텀', <IcFlatTop />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2, label: '피보나치', defaultIcon: <IcFib />,
|
||||
items: [
|
||||
T('fib', '피보나치 되돌림', <IcFib />, '⌥F'),
|
||||
T('fib_ext', '추세기반 피보나치 확장', <IcFibExt />),
|
||||
T('fib_channel', '피보나치 채널', <IcFibCh />),
|
||||
T('fibtz', '피보나치 타임존', <IcFibTZ />),
|
||||
T('fib_speed_fan', '스피드 리지스턴스 팬', <IcFibFan />),
|
||||
T('fib_trend_time','추세기반 피보나치 시간', <IcFibTrendT />),
|
||||
T('fib_circles', '피보나치 서클', <IcFibCircles />),
|
||||
T('fib_spiral', '피보나치 스파이럴', <IcFibSpiral />),
|
||||
T('fib_speed_arc', '스피드 리지스턴스 아크', <IcFibArc />),
|
||||
T('fib_wedge', '피보나치 웻지', <IcFibWedge />),
|
||||
SEP('피치포크'),
|
||||
T('pitchfork', '앤드류스 피치포크', <IcPitchfork />),
|
||||
T('pitchfork_schiff', '쉬프 피치포크', <IcPitchSchiff />),
|
||||
T('pitchfork_inside', '인사이드 피치포크', <IcPitchInside />),
|
||||
SEP('갠'),
|
||||
T('gann_box', '갠 박스', <IcGannBox />),
|
||||
T('gann_square', '갠 스퀘어', <IcGannSq />),
|
||||
T('gann_fan', '갠 팬', <IcGannFan />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3, label: '패턴', defaultIcon: <IcXABCD />,
|
||||
items: [
|
||||
T('xabcd', 'XABCD 패턴', <IcXABCD />),
|
||||
T('cypher', '사이퍼 패턴', <IcCypher />),
|
||||
T('head_shoulders', '헤드 앤 숄더', <IcHeadShould />),
|
||||
T('abcd', 'ABCD 패턴', <IcABCD />),
|
||||
T('wedge_pattern', '세모 패턴', <IcWedgePat />),
|
||||
T('three_drives', '쓰리 드라이브 패턴', <IcThreeDrives />),
|
||||
SEP('엘리엇 웨이브'),
|
||||
T('elliott_impulse', '엘리엇 임펄스 파동 (12345)', <IcElliottImp />),
|
||||
T('elliott_correction', '엘리엇 코렉션 파동 (ABC)', <IcElliottCorr />),
|
||||
T('elliott_triangle_wave', '엘리엇 트라이앵글 웨이브 (ABCDE)',<IcElliottTri />),
|
||||
T('elliott_double_combo', '엘리엇 다블콤보 파동 (WXY)', <IcElliottDbl />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4, label: '프로젝션', defaultIcon: <IcLongPos />,
|
||||
items: [
|
||||
T('long_position', '매수 포지션', <IcLongPos />),
|
||||
T('short_position', '숏 포지션', <IcShortPos />),
|
||||
T('forecast', '예측', <IcForecast />),
|
||||
T('bar_pattern', '봉패턴', <IcBarPattern />),
|
||||
T('ghost_feed', '고스트피드', <IcGhostFeed />),
|
||||
T('projection_tool','프로젝션', <IcProjection />),
|
||||
SEP('볼륨-기반'),
|
||||
T('anchored_vwap', '앵커드 VWAP', <IcVWAP />),
|
||||
T('fixed_volume_profile', '픽스트 레인지 볼륨 프로파일', <IcVolProfile />),
|
||||
SEP('계측기'),
|
||||
T('price_range', '가격범위', <IcPriceRange />),
|
||||
T('date_range', '날짜 범위', <IcDateRange />),
|
||||
T('date_price_range','날짜&가격 범위', <IcDatePriceR />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 5, label: '도형', defaultIcon: <IcRect />,
|
||||
items: [
|
||||
T('rect', '네모', <IcRect />, '⌥⇧R'),
|
||||
T('rotated_rect', '회전 사각형', <IcRotRect />),
|
||||
T('circle', '원', <IcCircle />),
|
||||
T('ellipse', '타원', <IcEllipse />),
|
||||
T('triangle', '삼각형', <IcTriangle />),
|
||||
T('parallelogram', '평행사변형', <IcParallelo />),
|
||||
T('polyline', '다각형', <IcPolyline />),
|
||||
T('arc', '호', <IcArc />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6, label: '브러시', defaultIcon: <IcPencil />,
|
||||
items: [
|
||||
T('pencil', '붓', <IcPencil />),
|
||||
T('highlight', '하이라이터', <IcHighlight />),
|
||||
SEP('화살'),
|
||||
T('arrow_mark', '화살표', <IcArrowMark />),
|
||||
T('anchored_arrow', '화살표', <IcArrowAnch />),
|
||||
T('arrow_mark_up', '위화살표', <IcArrowUp />),
|
||||
T('arrow_mark_down', '아래화살표', <IcArrowDown />),
|
||||
T('arrow_mark_left', '왼화살표', <IcArrowLeft />),
|
||||
T('arrow_mark_right', '오른화살표', <IcArrowRight />),
|
||||
SEP('세이프'),
|
||||
T('rect', '네모', <IcRect />, '⌥⇧R'),
|
||||
T('circle', '원', <IcCircle />),
|
||||
T('triangle', '삼각형', <IcTriangle />),
|
||||
T('ellipse', '타원', <IcEllipse />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 7, label: '텍스트 & 노트', defaultIcon: <IcText />,
|
||||
items: [
|
||||
T('text', '텍스트', <IcText />, 'T'),
|
||||
T('fixed_text', '고정위치문자', <IcFixedText />),
|
||||
T('note_text', '노트', <IcNote />),
|
||||
T('price_note', '프라이스 노트', <IcPriceNote />),
|
||||
T('pin_mark', '핀', <IcPin />),
|
||||
T('table_tool', '테이블', <IcTable />),
|
||||
T('callout', '콜아웃', <IcCallout />),
|
||||
T('comment_mark', '코멘트', <IcComment />),
|
||||
T('price_label', '가격라벨', <IcPriceLabel />),
|
||||
T('guide_line', '길잡이', <IcGuide />),
|
||||
T('flag_mark', '플래그 마크', <IcFlag />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 8, label: '측정', defaultIcon: <IcMeasure />,
|
||||
items: [
|
||||
T('measure', '가격 측정', <IcMeasure />, 'M'),
|
||||
T('date_range', '날짜 범위', <IcDateRange />),
|
||||
T('date_price_range', '날짜&가격 범위', <IcDatePriceR />),
|
||||
T('data_window', '데이터 창', <IcDataWindow />),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9, label: '확대/축소', defaultIcon: <IcZoom />,
|
||||
items: [
|
||||
T('zoom', '확대/축소', <IcZoom />),
|
||||
T('magnifier', '돋보기', <IcMagnifier />),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** 차트 모드에서도 사용 가능한 확대 도구 (드로잉 툴바는 chart 모드에서 기본 비활성) */
|
||||
export const CHART_ZOOM_TOOLS = new Set(['zoom', 'magnifier']);
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
const DrawingToolbar: React.FC<DrawingToolbarProps> = ({
|
||||
activeTool, mode, onTool, onClearAll, onUndo,
|
||||
locked, visible, onToggleLock, onToggleEye,
|
||||
indicatorsVisible = true,
|
||||
magnetMode = 'off',
|
||||
snapToIndicator = false,
|
||||
drawingCount = 0,
|
||||
indicatorCount = 0,
|
||||
onMagnetMode,
|
||||
onSnapToIndicator,
|
||||
onHideDrawings,
|
||||
onHideIndicators,
|
||||
onHideAll,
|
||||
onClearIndicators,
|
||||
keepLockedOnDelete = false,
|
||||
onToggleKeepLocked,
|
||||
}) => {
|
||||
const disabled = mode === 'chart';
|
||||
const canSelectTool = (toolId: string) => !disabled || CHART_ZOOM_TOOLS.has(toolId);
|
||||
|
||||
const [selectedPerGroup, setSelectedPerGroup] = useState<Record<number, string>>({});
|
||||
const [openGroup, setOpenGroup] = useState<number | null>(null);
|
||||
const [flyoutKind, setFlyoutKind] = useState<'group' | 'magnet' | 'eye' | 'trash' | null>(null);
|
||||
const [flyoutPos, setFlyoutPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
||||
|
||||
const openTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (openTimer.current) clearTimeout(openTimer.current);
|
||||
if (closeTimer.current) clearTimeout(closeTimer.current);
|
||||
}, []);
|
||||
|
||||
const getRepIcon = (group: GroupDef): React.ReactNode => {
|
||||
const selId = selectedPerGroup[group.id];
|
||||
if (selId) {
|
||||
const it = group.items.find(i => i.kind === 'tool' && i.id === selId);
|
||||
if (it && it.kind === 'tool') return it.icon;
|
||||
}
|
||||
const active = group.items.find(i => i.kind === 'tool' && i.id === activeTool);
|
||||
if (active && active.kind === 'tool') return active.icon;
|
||||
return group.defaultIcon;
|
||||
};
|
||||
|
||||
const isGroupActive = (group: GroupDef) =>
|
||||
group.items.some(i => i.kind === 'tool' && i.id === activeTool);
|
||||
|
||||
const openFlyout = (kind: 'group' | 'magnet' | 'eye' | 'trash', groupId: number | null, el: HTMLElement) => {
|
||||
if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }
|
||||
if (openTimer.current) clearTimeout(openTimer.current);
|
||||
openTimer.current = setTimeout(() => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
setFlyoutPos({ top: rect.top, left: rect.right + 4 });
|
||||
setOpenGroup(groupId);
|
||||
setFlyoutKind(kind);
|
||||
}, 80);
|
||||
};
|
||||
|
||||
const scheduleClose = () => {
|
||||
if (openTimer.current) { clearTimeout(openTimer.current); openTimer.current = null; }
|
||||
closeTimer.current = setTimeout(() => { setFlyoutKind(null); setOpenGroup(null); }, 200);
|
||||
};
|
||||
|
||||
const cancelClose = () => {
|
||||
if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }
|
||||
};
|
||||
|
||||
const selectTool = (groupId: number, toolId: string) => {
|
||||
if (!canSelectTool(toolId)) return;
|
||||
setSelectedPerGroup(prev => ({ ...prev, [groupId]: toolId }));
|
||||
setFlyoutKind(null); setOpenGroup(null);
|
||||
if (openTimer.current) clearTimeout(openTimer.current);
|
||||
if (closeTimer.current) clearTimeout(closeTimer.current);
|
||||
onTool(toolId);
|
||||
};
|
||||
|
||||
const handleGroupClick = (group: GroupDef) => {
|
||||
if (disabled && group.id !== 9) return;
|
||||
const selId = selectedPerGroup[group.id];
|
||||
const toolId = (selId && group.items.find(i => i.kind === 'tool' && i.id === selId))
|
||||
? selId
|
||||
: (group.items.find(i => i.kind === 'tool' && i.id === activeTool) as { kind: 'tool'; id: string } | undefined)?.id
|
||||
?? (group.items.find(i => i.kind === 'tool') as { kind: 'tool'; id: string } | undefined)?.id;
|
||||
if (toolId && canSelectTool(toolId)) onTool(toolId);
|
||||
};
|
||||
|
||||
const closeFlyout = () => { setFlyoutKind(null); setOpenGroup(null); };
|
||||
|
||||
// 현재 마그넷 아이콘
|
||||
const magnetIcon = magnetMode === 'strong' ? <IcMagnetStrg /> : <IcMagnetWeak />;
|
||||
const eyeIsOff = !visible && !indicatorsVisible;
|
||||
|
||||
return (
|
||||
<div className={`tv-drawing-sidebar ${disabled ? 'chart-mode' : 'trading-mode'}`}>
|
||||
|
||||
{/* ── 커서 ── */}
|
||||
<button
|
||||
className={`tv-side-btn ${activeTool === 'cursor' && !disabled ? 'active' : ''}`}
|
||||
title="커서 (Esc)"
|
||||
onClick={() => !disabled && onTool('cursor')}
|
||||
>
|
||||
<IcCursor />
|
||||
</button>
|
||||
|
||||
<div className="tv-side-sep" />
|
||||
|
||||
{/* ── 툴 그룹 (즐겨찾기 → 라인 …) ── */}
|
||||
{GROUPS.map(group => (
|
||||
<div key={group.id} className="tv-side-group">
|
||||
<button
|
||||
className={`tv-side-btn tv-side-group-btn${group.id === 0 ? ' tv-side-group-btn--favorite' : ''} ${isGroupActive(group) && (!disabled || group.id === 9) ? 'active' : ''}`}
|
||||
onMouseEnter={e => openFlyout('group', group.id, e.currentTarget)}
|
||||
onMouseLeave={scheduleClose}
|
||||
onClick={() => handleGroupClick(group)}
|
||||
title={group.label}
|
||||
>
|
||||
{getRepIcon(group)}
|
||||
<span className="tv-side-expand-arrow" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="tv-side-spacer" />
|
||||
<div className="tv-side-sep" />
|
||||
|
||||
{/* ── 하단 버튼: 마그넷 ── */}
|
||||
<button
|
||||
className={`tv-side-btn ${magnetMode !== 'off' ? 'active' : ''}`}
|
||||
title="마그넷"
|
||||
onMouseEnter={e => openFlyout('magnet', null, e.currentTarget)}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
{magnetIcon}
|
||||
</button>
|
||||
|
||||
{/* ── 드로잉 잠금 ── */}
|
||||
<button
|
||||
className={`tv-side-btn ${locked ? 'active tv-side-lock-on' : ''}`}
|
||||
title={locked ? '잠금 해제' : '드로잉 잠금'}
|
||||
onClick={() => !disabled && onToggleLock()}
|
||||
>
|
||||
<IcLock />
|
||||
</button>
|
||||
|
||||
{/* ── 표시/숨기기 토글 (플라이아웃) ── */}
|
||||
<button
|
||||
className={`tv-side-btn ${eyeIsOff ? 'tv-side-eye-off active' : (!visible || !indicatorsVisible) ? 'tv-side-eye-partial' : ''}`}
|
||||
title="표시/숨기기"
|
||||
onMouseEnter={e => openFlyout('eye', null, e.currentTarget)}
|
||||
onMouseLeave={scheduleClose}
|
||||
onClick={() => !disabled && onToggleEye()}
|
||||
>
|
||||
{eyeIsOff ? <IcEyeOff /> : <IcEye />}
|
||||
</button>
|
||||
|
||||
<div className="tv-side-sep" />
|
||||
|
||||
{/* ── 실행 취소 ── */}
|
||||
<button className="tv-side-btn" title="실행 취소 (Ctrl+Z)" onClick={() => !disabled && onUndo()}>
|
||||
<IcUndo />
|
||||
</button>
|
||||
|
||||
{/* ── 삭제 (플라이아웃) ── */}
|
||||
<button
|
||||
className="tv-side-btn tv-side-clear"
|
||||
title="삭제"
|
||||
onMouseEnter={e => openFlyout('trash', null, e.currentTarget)}
|
||||
onMouseLeave={scheduleClose}
|
||||
onClick={() => !disabled && onClearAll()}
|
||||
>
|
||||
<IcTrash />
|
||||
</button>
|
||||
|
||||
{/* ── 레이어 ── */}
|
||||
<button className="tv-side-btn" title="레이어">
|
||||
<IcLayers />
|
||||
</button>
|
||||
|
||||
{/* ════════════════════════════════════════════
|
||||
── 플라이아웃 팝업 (fixed 위치)
|
||||
════════════════════════════════════════════ */}
|
||||
{flyoutKind !== null && (!disabled || (flyoutKind === 'group' && openGroup === 9)) && (
|
||||
<div
|
||||
className="tv-side-flyout"
|
||||
style={{ top: flyoutPos.top, left: flyoutPos.left }}
|
||||
onMouseEnter={cancelClose}
|
||||
onMouseLeave={scheduleClose}
|
||||
>
|
||||
{/* ── 툴 그룹 플라이아웃 ── */}
|
||||
{flyoutKind === 'group' && (() => {
|
||||
const group = GROUPS.find(g => g.id === openGroup);
|
||||
if (!group) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="tv-side-flyout-header">{group.label}</div>
|
||||
{group.items.map((item, idx) =>
|
||||
item.kind === 'sep' ? (
|
||||
<div key={`sep-${idx}`} className="tv-side-flyout-sep">
|
||||
{item.label && <span className="tv-side-flyout-sec-label">{item.label}</span>}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
key={item.id}
|
||||
className={`tv-side-flyout-item ${activeTool === item.id ? 'active' : ''}${item.kind === 'tool' && !canSelectTool(item.id) ? ' disabled' : ''}`}
|
||||
disabled={item.kind === 'tool' && !canSelectTool(item.id)}
|
||||
onClick={() => item.kind === 'tool' && selectTool(group.id, item.id)}
|
||||
>
|
||||
<span className="tv-side-flyout-icon">{item.icon}</span>
|
||||
<span className="tv-side-flyout-title">{item.title}</span>
|
||||
{item.shortcut && <span className="tv-side-flyout-shortcut">{item.shortcut}</span>}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* ── 마그넷 플라이아웃 ── */}
|
||||
{flyoutKind === 'magnet' && (
|
||||
<>
|
||||
<div className="tv-side-flyout-header">마그넷</div>
|
||||
{(['weak','strong'] as const).map(m => (
|
||||
<button
|
||||
key={m}
|
||||
className={`tv-side-flyout-item ${magnetMode === m ? 'active' : ''}`}
|
||||
onClick={() => { onMagnetMode?.(m); closeFlyout(); }}
|
||||
>
|
||||
<span className="tv-side-flyout-icon">{m === 'weak' ? <IcMagnetWeak /> : <IcMagnetStrg />}</span>
|
||||
<span className="tv-side-flyout-title">{m === 'weak' ? '위크 마그넷' : '스트롱 마그넷'}</span>
|
||||
</button>
|
||||
))}
|
||||
{magnetMode !== 'off' && (
|
||||
<button className="tv-side-flyout-item" onClick={() => { onMagnetMode?.('off'); closeFlyout(); }}>
|
||||
<span className="tv-side-flyout-icon"><IcMagnetWeak /></span>
|
||||
<span className="tv-side-flyout-title" style={{ opacity: 0.5 }}>마그넷 끄기</span>
|
||||
</button>
|
||||
)}
|
||||
<div className="tv-side-flyout-sep" />
|
||||
<div className="tv-side-flyout-toggle-row">
|
||||
<span className="tv-side-flyout-title">인디케이터에 스냅</span>
|
||||
<button
|
||||
className={`tv-flyout-toggle ${snapToIndicator ? 'on' : ''}`}
|
||||
onClick={() => onSnapToIndicator?.(!snapToIndicator)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── 눈 플라이아웃 ── */}
|
||||
{flyoutKind === 'eye' && (
|
||||
<>
|
||||
<div className="tv-side-flyout-header">표시/숨기기</div>
|
||||
<button className="tv-side-flyout-item" onClick={() => { onHideDrawings?.(); closeFlyout(); }}>
|
||||
<span className="tv-side-flyout-icon"><IcEye /></span>
|
||||
<span className="tv-side-flyout-title">드로잉 숨기기</span>
|
||||
</button>
|
||||
<button className="tv-side-flyout-item" onClick={() => { onHideIndicators?.(); closeFlyout(); }}>
|
||||
<span className="tv-side-flyout-icon"><IcEye /></span>
|
||||
<span className="tv-side-flyout-title">인디케이터 숨기기</span>
|
||||
</button>
|
||||
<button className="tv-side-flyout-item" onClick={() => { onHideAll?.(); closeFlyout(); }}>
|
||||
<span className="tv-side-flyout-icon"><IcEyeOff /></span>
|
||||
<span className="tv-side-flyout-title">모두 숨기기</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── 삭제 플라이아웃 ── */}
|
||||
{flyoutKind === 'trash' && (
|
||||
<>
|
||||
<div className="tv-side-flyout-header">삭제</div>
|
||||
<button
|
||||
className="tv-side-flyout-item"
|
||||
onClick={() => { onClearAll(); closeFlyout(); }}
|
||||
>
|
||||
<span className="tv-side-flyout-icon"><IcTrash /></span>
|
||||
<span className="tv-side-flyout-title">{drawingCount} 드로잉 없애기</span>
|
||||
</button>
|
||||
<button
|
||||
className="tv-side-flyout-item"
|
||||
onClick={() => { onClearIndicators?.(); closeFlyout(); }}
|
||||
>
|
||||
<span className="tv-side-flyout-icon"><IcLayers /></span>
|
||||
<span className="tv-side-flyout-title">{indicatorCount} 인디케이터 없애기</span>
|
||||
</button>
|
||||
<button
|
||||
className="tv-side-flyout-item"
|
||||
onClick={() => { onClearAll(); onClearIndicators?.(); closeFlyout(); }}
|
||||
>
|
||||
<span className="tv-side-flyout-icon"><IcTrash /></span>
|
||||
<span className="tv-side-flyout-title">{drawingCount} 드로잉 & {indicatorCount} 인디케이터 없애기</span>
|
||||
</button>
|
||||
<div className="tv-side-flyout-sep" />
|
||||
<div className="tv-side-flyout-toggle-row">
|
||||
<span className="tv-side-flyout-title">잠긴 드로잉은 항상 제거하세요</span>
|
||||
<button
|
||||
className={`tv-flyout-toggle ${keepLockedOnDelete ? 'on' : ''}`}
|
||||
onClick={() => onToggleKeepLocked?.()}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawingToolbar;
|
||||
Reference in New Issue
Block a user