230 lines
11 KiB
TypeScript
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;
|