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) => ; // ─── 아이콘 ──────────────────────────────────────────────────────────────── const IcCursor = () => ; // 업비트 즐겨찾기 툴바 아이콘 const IcUpbitHline = () => ; const IcUpbitVline = () => ; const IcUpbitPeriod = () => ; const IcUpbitFibTz = () => ; const IcFavorite = () => ; // 선 그룹 const IcTrendline = () => ; const IcRay = () => ; const IcInfoLine = () => i; const IcExtLine = () => ; const IcTrendAngle = () => θ; const IcHline = () => ; const IcHlineRay = () => ; const IcVline = () => ; const IcCrossLine = () => ; const IcChannel = () => ; const IcParaCh = () => ; const IcDisjCh = () => ; const IcFlatTop = () => ; // 피보나치 그룹 const IcFib = () => ; const IcFibExt = () => ; const IcFibCh = () => ; const IcFibTZ = () => ; const IcFibFan = () => ; const IcFibTrendT = () => ; const IcFibCircles = () => ; const IcFibSpiral = () => ; const IcFibArc = () => ; const IcFibWedge = () => ; const IcPitchfork = () => ; const IcPitchSchiff = () => ; const IcPitchInside = () => ; const IcGannBox = () => ; const IcGannSq = () => ; const IcGannFan = () => ; // 패턴 그룹 const IcXABCD = () => ; const IcCypher = () => ; const IcABCD = () => ; const IcHeadShould = () => ; const IcWedgePat = () => ; const IcThreeDrives = () => ; const IcElliottImp = () => 1 2 3 4 5; const IcElliottCorr = () => A B C; const IcElliottTri = () => ABCDE; const IcElliottDbl = () => W X Y; // 프로젝션 그룹 const IcLongPos = () => ; const IcShortPos = () => ; const IcForecast = () => ; const IcBarPattern = () => ; const IcGhostFeed = () => ; const IcProjection = () => ; const IcVWAP = () => ; const IcVolProfile = () => ; const IcPriceRange = () => ; // 도형 그룹 const IcRect = () => ; const IcRotRect = () => ; const IcCircle = () => ; const IcEllipse = () => ; const IcTriangle = () => ; const IcParallelo = () => ; const IcPolyline = () => ; const IcArc = () => ; // 브러시 그룹 const IcPencil = () => ; const IcHighlight = () => ; const IcArrowMark = () => ; const IcArrowAnch = () => ; const IcArrowUp = () => ; const IcArrowDown = () => ; const IcArrowLeft = () => ; const IcArrowRight = () => ; // 텍스트 그룹 const IcText = () => T; const IcFixedText = () => T; const IcNote = () => ; const IcPriceNote = () => $; const IcPin = () => ; const IcTable = () => ; const IcCallout = () => ; const IcComment = () => ; const IcPriceLabel = () => ; const IcGuide = () => ; const IcFlag = () => ; // 측정 그룹 const IcMeasure = () => ; const IcDateRange = () => ; const IcDatePriceR = () => ; const IcDataWindow = () => ; const IcZoom = () => ; const IcMagnifier = () => ; // 하단 버튼 아이콘 const IcMagnetWeak = () => ; const IcMagnetStrg = () => ; const IcLock = () => ; const IcEye = () => ; const IcEyeOff = () => ; const IcTrash = () => ; const IcUndo = () => ; const IcLayers = () => ; // ─── 타입 ────────────────────────────────────────────────────────────────── 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: , items: [ T('hline', '가로줄', , '⌥H'), T('vline', '세로줄', , '⌥V'), T('date_range', '기간', ), T('fibtz', '피보나치 타임존', ), ], }, { id: 1, label: '라인', defaultIcon: , items: [ T('trendline', '추세줄', , '⌥T'), T('ray', '빛', ), T('info_line', '인포 라인', ), T('extended_line', '익스텐디드 라인', ), T('trend_angle', '추세각', ), SEP(), T('hline', '가로줄', , '⌥H'), T('hline_ray', '가로빛', , '⌥J'), T('vline', '수직선', , '⌥V'), T('cross_line', '크로스 라인', , '⌥C'), SEP('채널'), T('channel', '추세 채널', ), T('parallel_channel','패러렐 채널', ), T('disjoint_channel','디스조인트 채널',), T('flat_top_bottom','플랫 탑/바텀', ), ], }, { id: 2, label: '피보나치', defaultIcon: , items: [ T('fib', '피보나치 되돌림', , '⌥F'), T('fib_ext', '추세기반 피보나치 확장', ), T('fib_channel', '피보나치 채널', ), T('fibtz', '피보나치 타임존', ), T('fib_speed_fan', '스피드 리지스턴스 팬', ), T('fib_trend_time','추세기반 피보나치 시간', ), T('fib_circles', '피보나치 서클', ), T('fib_spiral', '피보나치 스파이럴', ), T('fib_speed_arc', '스피드 리지스턴스 아크', ), T('fib_wedge', '피보나치 웻지', ), SEP('피치포크'), T('pitchfork', '앤드류스 피치포크', ), T('pitchfork_schiff', '쉬프 피치포크', ), T('pitchfork_inside', '인사이드 피치포크', ), SEP('갠'), T('gann_box', '갠 박스', ), T('gann_square', '갠 스퀘어', ), T('gann_fan', '갠 팬', ), ], }, { id: 3, label: '패턴', defaultIcon: , items: [ T('xabcd', 'XABCD 패턴', ), T('cypher', '사이퍼 패턴', ), T('head_shoulders', '헤드 앤 숄더', ), T('abcd', 'ABCD 패턴', ), T('wedge_pattern', '세모 패턴', ), T('three_drives', '쓰리 드라이브 패턴', ), SEP('엘리엇 웨이브'), T('elliott_impulse', '엘리엇 임펄스 파동 (12345)', ), T('elliott_correction', '엘리엇 코렉션 파동 (ABC)', ), T('elliott_triangle_wave', '엘리엇 트라이앵글 웨이브 (ABCDE)',), T('elliott_double_combo', '엘리엇 다블콤보 파동 (WXY)', ), ], }, { id: 4, label: '프로젝션', defaultIcon: , items: [ T('long_position', '매수 포지션', ), T('short_position', '숏 포지션', ), T('forecast', '예측', ), T('bar_pattern', '봉패턴', ), T('ghost_feed', '고스트피드', ), T('projection_tool','프로젝션', ), SEP('볼륨-기반'), T('anchored_vwap', '앵커드 VWAP', ), T('fixed_volume_profile', '픽스트 레인지 볼륨 프로파일', ), SEP('계측기'), T('price_range', '가격범위', ), T('date_range', '날짜 범위', ), T('date_price_range','날짜&가격 범위', ), ], }, { id: 5, label: '도형', defaultIcon: , items: [ T('rect', '네모', , '⌥⇧R'), T('rotated_rect', '회전 사각형', ), T('circle', '원', ), T('ellipse', '타원', ), T('triangle', '삼각형', ), T('parallelogram', '평행사변형', ), T('polyline', '다각형', ), T('arc', '호', ), ], }, { id: 6, label: '브러시', defaultIcon: , items: [ T('pencil', '붓', ), T('highlight', '하이라이터', ), SEP('화살'), T('arrow_mark', '화살표', ), T('anchored_arrow', '화살표', ), T('arrow_mark_up', '위화살표', ), T('arrow_mark_down', '아래화살표', ), T('arrow_mark_left', '왼화살표', ), T('arrow_mark_right', '오른화살표', ), SEP('세이프'), T('rect', '네모', , '⌥⇧R'), T('circle', '원', ), T('triangle', '삼각형', ), T('ellipse', '타원', ), ], }, { id: 7, label: '텍스트 & 노트', defaultIcon: , items: [ T('text', '텍스트', , 'T'), T('fixed_text', '고정위치문자', ), T('note_text', '노트', ), T('price_note', '프라이스 노트', ), T('pin_mark', '핀', ), T('table_tool', '테이블', ), T('callout', '콜아웃', ), T('comment_mark', '코멘트', ), T('price_label', '가격라벨', ), T('guide_line', '길잡이', ), T('flag_mark', '플래그 마크', ), ], }, { id: 8, label: '측정', defaultIcon: , items: [ T('measure', '가격 측정', , 'M'), T('date_range', '날짜 범위', ), T('date_price_range', '날짜&가격 범위', ), T('data_window', '데이터 창', ), ], }, { id: 9, label: '확대/축소', defaultIcon: , items: [ T('zoom', '확대/축소', ), T('magnifier', '돋보기', ), ], }, ]; /** 차트 모드에서도 사용 가능한 확대 도구 (드로잉 툴바는 chart 모드에서 기본 비활성) */ export const CHART_ZOOM_TOOLS = new Set(['zoom', 'magnifier']); // ─── Component ──────────────────────────────────────────────────────────────── const DrawingToolbar: React.FC = ({ 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>({}); const [openGroup, setOpenGroup] = useState(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 | null>(null); const closeTimer = useRef | 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' ? : ; const eyeIsOff = !visible && !indicatorsVisible; return (
{/* ── 커서 ── */}
{/* ── 툴 그룹 (즐겨찾기 → 라인 …) ── */} {GROUPS.map(group => (
))}
{/* ── 하단 버튼: 마그넷 ── */} {/* ── 드로잉 잠금 ── */} {/* ── 표시/숨기기 토글 (플라이아웃) ── */}
{/* ── 실행 취소 ── */} {/* ── 삭제 (플라이아웃) ── */} {/* ── 레이어 ── */} {/* ════════════════════════════════════════════ ── 플라이아웃 팝업 (fixed 위치) ════════════════════════════════════════════ */} {flyoutKind !== null && (!disabled || (flyoutKind === 'group' && openGroup === 9)) && (
{/* ── 툴 그룹 플라이아웃 ── */} {flyoutKind === 'group' && (() => { const group = GROUPS.find(g => g.id === openGroup); if (!group) return null; return ( <>
{group.label}
{group.items.map((item, idx) => item.kind === 'sep' ? (
{item.label && {item.label}}
) : ( ) )} ); })()} {/* ── 마그넷 플라이아웃 ── */} {flyoutKind === 'magnet' && ( <>
마그넷
{(['weak','strong'] as const).map(m => ( ))} {magnetMode !== 'off' && ( )}
인디케이터에 스냅
)} {/* ── 눈 플라이아웃 ── */} {flyoutKind === 'eye' && ( <>
표시/숨기기
)} {/* ── 삭제 플라이아웃 ── */} {flyoutKind === 'trash' && ( <>
삭제
잠긴 드로잉은 항상 제거하세요
)}
)}
); }; export default DrawingToolbar;