360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
import React, { useState } from 'react';
|
|
import type { Drawing, FibTZSettings, FibTZLevel, FibTZBaseline, DrawingStyle } from '../types';
|
|
import { DEFAULT_FIB_TZ_SETTINGS } from '../types';
|
|
import { useDraggablePanel } from '../hooks/useDraggablePanel';
|
|
|
|
const LINE_STYLES: { value: DrawingStyle; label: string }[] = [
|
|
{ value: 'solid', label: '━━' },
|
|
{ value: 'dashed', label: '╌╌' },
|
|
{ value: 'dotted', label: '···' },
|
|
];
|
|
const LINE_WIDTHS = [1, 2, 3, 4] as const;
|
|
|
|
// ─── Sub-components ──────────────────────────────────────────────────────────
|
|
|
|
function ColorCell({
|
|
color,
|
|
onChange,
|
|
}: {
|
|
color: string;
|
|
onChange: (c: string) => void;
|
|
}) {
|
|
return (
|
|
<label
|
|
title="색상 선택"
|
|
style={{
|
|
display: 'block',
|
|
width: 28,
|
|
height: 22,
|
|
borderRadius: 3,
|
|
background: color,
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|
cursor: 'pointer',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
<input
|
|
type="color"
|
|
value={color}
|
|
onChange={e => onChange(e.target.value)}
|
|
style={{ opacity: 0, position: 'absolute', inset: 0, width: '100%', height: '100%', cursor: 'pointer' }}
|
|
/>
|
|
</label>
|
|
);
|
|
}
|
|
|
|
function StyleSelect({
|
|
value,
|
|
onChange,
|
|
}: {
|
|
value: DrawingStyle;
|
|
onChange: (s: DrawingStyle) => void;
|
|
}) {
|
|
return (
|
|
<select
|
|
value={value}
|
|
onChange={e => onChange(e.target.value as DrawingStyle)}
|
|
style={{
|
|
background: 'rgba(255,255,255,0.06)',
|
|
border: '1px solid rgba(122,162,247,0.25)',
|
|
borderRadius: 3,
|
|
color: 'inherit',
|
|
fontSize: 12,
|
|
padding: '2px 4px',
|
|
width: 54,
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
{LINE_STYLES.map(s => (
|
|
<option key={s.value} value={s.value}>{s.label}</option>
|
|
))}
|
|
</select>
|
|
);
|
|
}
|
|
|
|
function WidthSelect({
|
|
value,
|
|
onChange,
|
|
}: {
|
|
value: number;
|
|
onChange: (w: number) => void;
|
|
}) {
|
|
return (
|
|
<select
|
|
value={value}
|
|
onChange={e => onChange(Number(e.target.value))}
|
|
style={{
|
|
background: 'rgba(255,255,255,0.06)',
|
|
border: '1px solid rgba(122,162,247,0.25)',
|
|
borderRadius: 3,
|
|
color: 'inherit',
|
|
fontSize: 12,
|
|
padding: '2px 4px',
|
|
width: 44,
|
|
cursor: 'pointer',
|
|
}}
|
|
>
|
|
{LINE_WIDTHS.map(w => (
|
|
<option key={w} value={w}>{w}</option>
|
|
))}
|
|
</select>
|
|
);
|
|
}
|
|
|
|
// ─── Main component ───────────────────────────────────────────────────────────
|
|
|
|
interface Props {
|
|
drawing: Drawing;
|
|
onSave: (updated: Drawing) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
const FibTZSettingsModal: React.FC<Props> = ({ drawing, onSave, onCancel }) => {
|
|
const init = drawing.fibtzSettings ?? DEFAULT_FIB_TZ_SETTINGS;
|
|
|
|
const [baseline, setBaseline] = useState<FibTZBaseline>({ ...init.baseline });
|
|
const [levels, setLevels] = useState<FibTZLevel[]>(
|
|
init.levels.map(l => ({ ...l }))
|
|
);
|
|
|
|
const updateBaseline = (patch: Partial<FibTZBaseline>) =>
|
|
setBaseline(prev => ({ ...prev, ...patch }));
|
|
|
|
const updateLevel = (idx: number, patch: Partial<FibTZLevel>) =>
|
|
setLevels(prev => prev.map((l, i) => i === idx ? { ...l, ...patch } : l));
|
|
|
|
const handleReset = () => {
|
|
setBaseline({ ...DEFAULT_FIB_TZ_SETTINGS.baseline });
|
|
setLevels(DEFAULT_FIB_TZ_SETTINGS.levels.map(l => ({ ...l })));
|
|
};
|
|
|
|
const handleSave = () => {
|
|
const settings: FibTZSettings = { baseline, levels };
|
|
onSave({ ...drawing, fibtzSettings: settings });
|
|
};
|
|
|
|
const cellStyle: React.CSSProperties = {
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
};
|
|
|
|
const headerStyle: React.CSSProperties = {
|
|
fontSize: 10,
|
|
color: 'rgba(192,202,245,0.55)',
|
|
textAlign: 'center',
|
|
paddingBottom: 6,
|
|
};
|
|
|
|
const {
|
|
panelRef,
|
|
dragging,
|
|
onHeaderPointerDown, headerTouchStyle,
|
|
panelStyle,
|
|
headerCursor,
|
|
} = useDraggablePanel({ centerOnMount: true });
|
|
|
|
return (
|
|
<div
|
|
className="dsm-overlay"
|
|
style={{ zIndex: 1200 }}
|
|
onMouseDown={e => { if (e.target === e.currentTarget) onCancel(); }}
|
|
>
|
|
<div
|
|
ref={panelRef}
|
|
className="dsm-dialog"
|
|
style={{
|
|
...panelStyle,
|
|
width: 460,
|
|
maxHeight: '90vh',
|
|
cursor: dragging ? 'grabbing' : undefined,
|
|
}}
|
|
onMouseDown={e => e.stopPropagation()}
|
|
>
|
|
<div
|
|
className="gc-popup-header"
|
|
onPointerDown={onHeaderPointerDown}
|
|
style={{ cursor: headerCursor, ...headerTouchStyle }}
|
|
>
|
|
<span className="gc-popup-title">피보나치 타임존 설정</span>
|
|
<button type="button" className="gc-popup-close" onClick={onCancel} aria-label="닫기">✕</button>
|
|
</div>
|
|
<div className="dsm-body" style={{ color: 'var(--text, #c0caf5)' }}>
|
|
|
|
{/* ── 기준선 (0) ── */}
|
|
<div style={{ marginBottom: 14 }}>
|
|
<div style={{ fontSize: 11, color: 'rgba(192,202,245,0.6)', marginBottom: 8, fontWeight: 600 }}>
|
|
기준선 (항상 표시)
|
|
</div>
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: '28px 28px 54px 44px',
|
|
gap: 8,
|
|
alignItems: 'center',
|
|
background: 'rgba(255,255,255,0.04)',
|
|
borderRadius: 5,
|
|
padding: '8px 10px',
|
|
}}
|
|
>
|
|
{/* 항상 활성화 표시 */}
|
|
<div style={{ ...cellStyle }}>
|
|
<div style={{
|
|
width: 14, height: 14, borderRadius: 3,
|
|
background: 'var(--accent, #7aa2f7)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}}>
|
|
<svg width="9" height="9" viewBox="0 0 9 9" fill="none">
|
|
<polyline points="1.5,4.5 3.5,7 7.5,2" stroke="#fff" strokeWidth="1.5" strokeLinecap="round"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<ColorCell color={baseline.color} onChange={c => updateBaseline({ color: c })} />
|
|
<StyleSelect value={baseline.style} onChange={s => updateBaseline({ style: s })} />
|
|
<WidthSelect value={baseline.lineWidth} onChange={w => updateBaseline({ lineWidth: w })} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 레벨 목록 ── */}
|
|
<div>
|
|
<div style={{ fontSize: 11, color: 'rgba(192,202,245,0.6)', marginBottom: 6, fontWeight: 600 }}>
|
|
레벨
|
|
</div>
|
|
|
|
{/* 헤더 행 */}
|
|
<div
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: '28px 60px 28px 54px 44px',
|
|
gap: 8,
|
|
padding: '0 10px',
|
|
marginBottom: 4,
|
|
}}
|
|
>
|
|
<div style={headerStyle}>표시</div>
|
|
<div style={headerStyle}>값</div>
|
|
<div style={headerStyle}>색상</div>
|
|
<div style={headerStyle}>스타일</div>
|
|
<div style={headerStyle}>두께</div>
|
|
</div>
|
|
|
|
{/* 레벨 행들 */}
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
|
{levels.map((lv, idx) => (
|
|
<div
|
|
key={idx}
|
|
style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: '28px 60px 28px 54px 44px',
|
|
gap: 8,
|
|
alignItems: 'center',
|
|
background: idx % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'transparent',
|
|
borderRadius: 4,
|
|
padding: '5px 10px',
|
|
}}
|
|
>
|
|
{/* 표시 체크박스 */}
|
|
<div style={cellStyle}>
|
|
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<input
|
|
type="checkbox"
|
|
checked={lv.enabled}
|
|
onChange={e => updateLevel(idx, { enabled: e.target.checked })}
|
|
style={{ display: 'none' }}
|
|
/>
|
|
<div style={{
|
|
width: 14, height: 14, borderRadius: 3,
|
|
background: lv.enabled ? 'var(--accent, #7aa2f7)' : 'rgba(255,255,255,0.1)',
|
|
border: lv.enabled ? 'none' : '1px solid rgba(255,255,255,0.25)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
flexShrink: 0,
|
|
}}>
|
|
{lv.enabled && (
|
|
<svg width="9" height="9" viewBox="0 0 9 9" fill="none">
|
|
<polyline points="1.5,4.5 3.5,7 7.5,2" stroke="#fff" strokeWidth="1.5" strokeLinecap="round"/>
|
|
</svg>
|
|
)}
|
|
</div>
|
|
</label>
|
|
</div>
|
|
|
|
{/* 값 (편집 가능) */}
|
|
<input
|
|
type="number"
|
|
value={lv.value}
|
|
min={1}
|
|
onChange={e => updateLevel(idx, { value: Math.max(1, Number(e.target.value)) })}
|
|
style={{
|
|
width: '100%',
|
|
background: 'rgba(255,255,255,0.06)',
|
|
border: '1px solid rgba(122,162,247,0.25)',
|
|
borderRadius: 3,
|
|
color: 'inherit',
|
|
fontSize: 12,
|
|
padding: '3px 5px',
|
|
textAlign: 'center',
|
|
}}
|
|
/>
|
|
|
|
{/* 색상 */}
|
|
<ColorCell color={lv.color} onChange={c => updateLevel(idx, { color: c })} />
|
|
|
|
{/* 스타일 */}
|
|
<StyleSelect value={lv.style} onChange={s => updateLevel(idx, { style: s })} />
|
|
|
|
{/* 두께 */}
|
|
<WidthSelect value={lv.lineWidth} onChange={w => updateLevel(idx, { lineWidth: w })} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 버튼 영역 */}
|
|
<div className="dsm-footer" style={{ marginTop: 18 }}>
|
|
<button
|
|
onClick={handleReset}
|
|
style={{
|
|
padding: '6px 14px',
|
|
background: 'transparent',
|
|
border: '1px solid rgba(122,162,247,0.3)',
|
|
borderRadius: 5,
|
|
cursor: 'pointer',
|
|
color: 'inherit',
|
|
fontSize: 13,
|
|
}}
|
|
>초기화</button>
|
|
<button
|
|
onClick={onCancel}
|
|
style={{
|
|
padding: '6px 14px',
|
|
background: 'transparent',
|
|
border: '1px solid rgba(122,162,247,0.3)',
|
|
borderRadius: 5,
|
|
cursor: 'pointer',
|
|
color: 'inherit',
|
|
fontSize: 13,
|
|
}}
|
|
>취소</button>
|
|
<button
|
|
onClick={handleSave}
|
|
style={{
|
|
padding: '6px 16px',
|
|
background: 'var(--accent, #7aa2f7)',
|
|
border: 'none',
|
|
borderRadius: 5,
|
|
cursor: 'pointer',
|
|
color: '#1a1b26',
|
|
fontWeight: 600,
|
|
fontSize: 13,
|
|
}}
|
|
>확인</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FibTZSettingsModal;
|