Files
goldenChart/frontend/src/components/ObjectTree.tsx
T
2026-05-23 15:11:48 +09:00

230 lines
11 KiB
TypeScript

import React from 'react';
import type { Drawing, DrawingToolId } from '../types';
import { useDraggablePanel } from '../hooks/useDraggablePanel';
interface ObjectTreeProps {
drawings: Drawing[];
onToggleVisible: (id: string) => void;
onRemove: (id: string) => void;
onClose: () => void;
}
// 드로잉 타입 → 한글 이름
const TOOL_LABEL: Partial<Record<DrawingToolId, string>> = {
cursor: '커서',
hline: '수평선',
hline_ray: '수평 레이',
vline: '수직선',
cross_line: '십자선',
info_line: '정보선',
trendline: '추세선',
extended_line: '연장 추세선',
ray: '레이',
arrow: '화살표 추세선',
fib: '피보나치 되돌림',
fib_ext: '피보나치 확장',
fib_channel: '피보나치 채널',
fibtz: '피보나치 타임존',
fib_speed_fan: '스피드 리지스턴스 팬',
fib_trend_time: '추세기반 피보나치 시간',
fib_circles: '피보나치 서클',
fib_spiral: '피보나치 스파이럴',
fib_speed_arc: '스피드 리지스턴스 아크',
fib_wedge: '피보나치 웻지',
pitchfork: '앤드류스 피치포크',
pitchfork_schiff: '쉬프 피치포크',
pitchfork_inside: '인사이드 피치포크',
gann_box: '갠 박스',
gann_square: '갠 스퀘어',
gann_fan: '갠 팬',
xabcd: 'XABCD 패턴',
cypher: '사이퍼 패턴',
abcd: 'ABCD 패턴',
head_shoulders: '헤드 앤 숄더',
channel: '추세 채널',
parallel_channel: '평행 채널',
disjoint_channel: '분리 채널',
flat_top_bottom: '플랫 탑/바텀 채널',
rect: '사각형',
rotated_rect: '회전 사각형',
circle: '원',
ellipse: '타원',
triangle: '삼각형',
parallelogram: '평행사변형',
polyline: '다각형',
arc: '호',
text: '텍스트',
callout: '콜아웃',
price_note: '가격 주석',
arrow_mark_up: '위쪽 화살표',
arrow_mark_down: '아래쪽 화살표',
price_label: '가격 레이블',
pencil: '자유곡선',
highlight: '형광펜',
trend_angle: '추세각',
long_position: '매수 포지션',
short_position: '숏 포지션',
forecast: '예측',
bar_pattern: '봉패턴',
ghost_feed: '고스트피드',
projection_tool: '프로젝션',
anchored_vwap: '앵커드 VWAP',
fixed_volume_profile: '볼륨 프로파일',
price_range: '가격범위',
wedge_pattern: '세모 패턴',
three_drives: '쓰리 드라이브 패턴',
elliott_impulse: '엘리엇 임펄스 파동',
elliott_correction: '엘리엇 코렉션 파동',
elliott_triangle_wave: '엘리엇 트라이앵글 웨이브',
elliott_double_combo: '엘리엇 다블콤보 파동',
arrow_mark: '화살표',
anchored_arrow: '앵커 화살표',
arrow_mark_left: '왼화살표',
arrow_mark_right: '오른화살표',
fixed_text: '고정위치문자',
note_text: '노트',
pin_mark: '핀',
table_tool: '테이블',
comment_mark: '코멘트',
guide_line: '길잡이',
flag_mark: '플래그',
measure: '가격 측정',
date_range: '날짜 범위',
date_price_range: '날짜&가격 범위',
data_window: '데이터 창',
zoom: '확대/축소',
magnifier: '돋보기',
};
// 아이콘 기본값
const DefaultIcon = () => <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><circle cx="6" cy="6" r="4"/></svg>;
// 타입별 아이콘
const ToolIcon: Partial<Record<DrawingToolId, React.ReactNode>> = {
cursor: <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M2 1 L2 9 L4 7 L6 10 L7 9.5 L5 6.5 L8 6.5 Z"/></svg>,
hline: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><line x1="1" y1="6" x2="11" y2="6"/></svg>,
vline: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><line x1="6" y1="1" x2="6" y2="11"/></svg>,
trendline: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><line x1="2" y1="10" x2="10" y2="2"/></svg>,
ray: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><line x1="2" y1="10" x2="11" y2="2"/><circle cx="2" cy="10" r="1.2" fill="currentColor"/></svg>,
fib: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1"><line x1="1" y1="2" x2="11" y2="2"/><line x1="1" y1="5" x2="11" y2="5" strokeOpacity="0.6"/><line x1="1" y1="7" x2="11" y2="7"/><line x1="1" y1="10" x2="11" y2="10"/></svg>,
fibtz: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1"><line x1="1" y1="1" x2="1" y2="11"/><line x1="3" y1="1" x2="3" y2="11" strokeOpacity="0.7"/><line x1="6" y1="1" x2="6" y2="11" strokeOpacity="0.5"/><line x1="9" y1="1" x2="9" y2="11" strokeOpacity="0.6"/><line x1="11" y1="1" x2="11" y2="11" strokeOpacity="0.4"/></svg>,
rect: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><rect x="2" y="3" width="8" height="6"/></svg>,
circle: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><circle cx="6" cy="6" r="4.5"/></svg>,
triangle: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><path d="M6 2 L11 10 L1 10 Z"/></svg>,
channel: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><line x1="1" y1="8" x2="11" y2="3"/><line x1="1" y1="11" x2="11" y2="6" strokeDasharray="2 2"/></svg>,
measure: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><line x1="1" y1="6" x2="11" y2="6"/><line x1="1" y1="4" x2="1" y2="8"/><line x1="11" y1="4" x2="11" y2="8"/></svg>,
text: <svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><text x="2" y="10" fontSize="10" fontWeight="700" fontFamily="sans-serif">T</text></svg>,
pencil: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><path d="M2 10 L2.5 7 L9 1.5 Q10 1 11 2 Q12 3 11 4 L4.5 10 Z"/></svg>,
pitchfork: <svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2"><line x1="2" y1="6" x2="7" y2="5"/><line x1="7" y1="5" x2="11" y2="3"/><line x1="7" y1="5" x2="11" y2="7"/></svg>,
};
const IcEye = ({ visible }: { visible: boolean }) => visible ? (
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" strokeWidth="1.3">
<path d="M1 6.5 Q4 2 6.5 2 Q9 2 12 6.5 Q9 11 6.5 11 Q4 11 1 6.5 Z"/>
<circle cx="6.5" cy="6.5" r="1.8"/>
</svg>
) : (
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" stroke="currentColor" strokeWidth="1.3">
<path d="M1 6.5 Q4 2 6.5 2 Q9 2 12 6.5 Q9 11 6.5 11 Q4 11 1 6.5 Z" strokeOpacity="0.3"/>
<line x1="2" y1="2" x2="11" y2="11"/>
</svg>
);
const IcTrash = () => (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2">
<line x1="2" y1="4" x2="10" y2="4"/>
<path d="M4.5 4 V2.5 Q4.5 2 6 2 Q7.5 2 7.5 2.5 V4"/>
<path d="M3 4 L3.5 10.5 Q3.5 11 4 11 L8 11 Q8.5 11 8.5 10.5 L9 4"/>
</svg>
);
const ObjectTree: React.FC<ObjectTreeProps> = ({ drawings, onToggleVisible, onRemove, onClose }) => {
const {
panelRef,
dragging,
onHeaderPointerDown, headerTouchStyle,
panelStyle,
headerCursor,
} = useDraggablePanel({ centerOnMount: true });
return (
<div className="obj-tree-overlay" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>
<div
ref={panelRef}
className="obj-tree-panel"
style={{
...panelStyle,
zIndex: 5001,
cursor: dragging ? 'grabbing' : undefined,
}}
onMouseDown={e => e.stopPropagation()}
>
<div
className="gc-popup-header obj-tree-header"
onPointerDown={onHeaderPointerDown}
style={{ cursor: headerCursor, ...headerTouchStyle }}
>
<span className="gc-popup-title obj-tree-title">오브젝트 트리</span>
<button type="button" className="gc-popup-close obj-tree-close" onClick={onClose}></button>
</div>
{drawings.length === 0 ? (
<div className="obj-tree-empty">드로잉이 없습니다</div>
) : (
<div className="obj-tree-list">
{[...drawings].reverse().map((d, idx) => {
const visible = d.visible !== false;
return (
<div
key={d.id}
className={`obj-tree-item${!visible ? ' hidden' : ''}`}
>
{/* 색상 스와치 */}
<span
className="obj-tree-swatch"
style={{ background: d.color }}
/>
{/* 타입 아이콘 */}
<span className="obj-tree-icon" style={{ color: d.color }}>
{ToolIcon[d.type as DrawingToolId] ?? <DefaultIcon />}
</span>
{/* 이름 */}
<span className="obj-tree-name">
{TOOL_LABEL[d.type as DrawingToolId] ?? d.type}
{d.text && <span className="obj-tree-text-preview"> "{d.text}"</span>}
</span>
{/* 번호 (역순) */}
<span className="obj-tree-num">#{drawings.length - idx}</span>
{/* 가시성 토글 */}
<button
className="obj-tree-btn"
title={visible ? '숨기기' : '표시'}
onClick={() => onToggleVisible(d.id)}
>
<IcEye visible={visible} />
</button>
{/* 삭제 */}
<button
className="obj-tree-btn obj-tree-del"
title="삭제"
onClick={() => onRemove(d.id)}
>
<IcTrash />
</button>
</div>
);
})}
</div>
)}
</div>
</div>
);
};
export default ObjectTree;