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

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;