171 lines
5.5 KiB
TypeScript
171 lines
5.5 KiB
TypeScript
import React, { useEffect, useMemo, useState } from 'react';
|
|
import { INDICATOR_REGISTRY, type IndicatorDef } from '../utils/indicatorRegistry';
|
|
import { MAIN_INDICATOR_LABELS } from '../utils/indicatorMainTab';
|
|
|
|
interface IndicatorCustomTabEditorProps {
|
|
mode: 'create' | 'edit';
|
|
initialName: string;
|
|
initialTypes: string[];
|
|
onSave: (name: string, indicatorTypes: string[]) => void;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function indicatorLabel(def: IndicatorDef): { ko: string; en: string } {
|
|
const main = MAIN_INDICATOR_LABELS[def.type];
|
|
return main ?? { ko: def.description, en: def.name };
|
|
}
|
|
|
|
const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
|
|
mode,
|
|
initialName,
|
|
initialTypes,
|
|
onSave,
|
|
onClose,
|
|
}) => {
|
|
const [name, setName] = useState(initialName);
|
|
const [selected, setSelected] = useState<Set<string>>(() => new Set(initialTypes));
|
|
const [search, setSearch] = useState('');
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
e.stopImmediatePropagation();
|
|
onClose();
|
|
}
|
|
};
|
|
window.addEventListener('keydown', onKey, true);
|
|
return () => window.removeEventListener('keydown', onKey, true);
|
|
}, [onClose]);
|
|
|
|
const filtered = useMemo(() => {
|
|
const q = search.toLowerCase().trim();
|
|
if (!q) return INDICATOR_REGISTRY;
|
|
return INDICATOR_REGISTRY.filter(d => {
|
|
const lbl = indicatorLabel(d);
|
|
return (
|
|
lbl.ko.toLowerCase().includes(q)
|
|
|| lbl.en.toLowerCase().includes(q)
|
|
|| d.shortName.toLowerCase().includes(q)
|
|
|| d.type.toLowerCase().includes(q)
|
|
);
|
|
});
|
|
}, [search]);
|
|
|
|
const toggle = (type: string) => {
|
|
setSelected(prev => {
|
|
const next = new Set(prev);
|
|
if (next.has(type)) next.delete(type);
|
|
else next.add(type);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleSave = () => {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) {
|
|
window.alert('탭 이름을 입력해 주세요.');
|
|
return;
|
|
}
|
|
if (selected.size === 0) {
|
|
window.alert('탭에 표시할 지표를 하나 이상 선택해 주세요.');
|
|
return;
|
|
}
|
|
const ordered = INDICATOR_REGISTRY
|
|
.filter(d => selected.has(d.type))
|
|
.map(d => d.type);
|
|
onSave(trimmed, ordered);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="ind-tab-editor-overlay"
|
|
onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}
|
|
>
|
|
<div className="ind-tab-editor" onMouseDown={e => e.stopPropagation()}>
|
|
<div className="ind-tab-editor-header">
|
|
<span className="ind-tab-editor-title">
|
|
{mode === 'create' ? '탭 추가' : '탭 수정'}
|
|
</span>
|
|
<button type="button" className="gc-popup-close" onClick={onClose} aria-label="닫기">✕</button>
|
|
</div>
|
|
|
|
<div className="ind-tab-editor-body">
|
|
<label className="ind-tab-editor-field">
|
|
<span className="ind-tab-editor-label">탭 이름</span>
|
|
<input
|
|
className="ind-tab-editor-input"
|
|
value={name}
|
|
onChange={e => setName(e.target.value)}
|
|
placeholder="예: 내 지표"
|
|
autoFocus
|
|
/>
|
|
</label>
|
|
|
|
<div className="ind-tab-editor-field">
|
|
<div className="ind-tab-editor-label-row">
|
|
<span className="ind-tab-editor-label">
|
|
지표 선택 ({selected.size}개)
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="ind-tab-editor-link"
|
|
onClick={() => setSelected(new Set(INDICATOR_REGISTRY.map(d => d.type)))}
|
|
>
|
|
전체 선택
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="ind-tab-editor-link"
|
|
onClick={() => setSelected(new Set())}
|
|
>
|
|
전체 해제
|
|
</button>
|
|
</div>
|
|
<input
|
|
className="ind-tab-editor-input"
|
|
value={search}
|
|
onChange={e => setSearch(e.target.value)}
|
|
placeholder="지표 검색..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="ind-tab-editor-list">
|
|
{filtered.length === 0 ? (
|
|
<div className="ind-panel-empty">검색 결과 없음</div>
|
|
) : (
|
|
filtered.map(def => {
|
|
const lbl = indicatorLabel(def);
|
|
const checked = selected.has(def.type);
|
|
return (
|
|
<label key={def.type} className={`ind-tab-editor-item${checked ? ' checked' : ''}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={checked}
|
|
onChange={() => toggle(def.type)}
|
|
/>
|
|
<span className="ind-tab-editor-item-info">
|
|
<span className="ind-tab-editor-item-name">{lbl.ko}</span>
|
|
<span className="ind-tab-editor-item-desc">{lbl.en}</span>
|
|
</span>
|
|
</label>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="ind-tab-editor-footer">
|
|
<button type="button" className="ind-tab-editor-btn cancel" onClick={onClose}>
|
|
취소
|
|
</button>
|
|
<button type="button" className="ind-tab-editor-btn save" onClick={handleSave}>
|
|
저장
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default IndicatorCustomTabEditor;
|