전략편집기 수정

This commit is contained in:
Macbook
2026-05-25 17:56:25 +09:00
parent aac6454724
commit 02d14e4b2b
39 changed files with 1974 additions and 288 deletions
@@ -0,0 +1,196 @@
import React, { useCallback, useMemo, useState } from 'react';
import PaletteChip from './PaletteChip';
import PaletteItemModal from './PaletteItemModal';
import type { DefType } from '../../utils/strategyEditorShared';
import { getCompositeDefaultPeriods } from '../../utils/compositeIndicators';
import { getDefaultIndicatorPeriod } from '../../utils/conditionPeriods';
import { getIndicatorPeriodLabel } from '../../utils/strategyFlowLayout';
import {
createPaletteItemId,
savePaletteItems,
type PaletteItem,
type PaletteItemKind,
} from '../../utils/strategyPaletteStorage';
function periodLabel(item: PaletteItem, def: DefType): string {
if (item.kind === 'composite') {
const d = getCompositeDefaultPeriods(item.value, def);
const s = item.shortPeriod ?? d.short;
const l = item.longPeriod ?? d.long;
return `${s} / ${l}`;
}
if (item.period != null) return String(item.period);
return getIndicatorPeriodLabel(item.value, def) || String(getDefaultIndicatorPeriod(item.value, def));
}
interface Props {
kind: PaletteItemKind;
items: PaletteItem[];
onItemsChange: (items: PaletteItem[]) => void;
def: DefType;
searchQuery: string;
selectedItemId: string | null;
onSelectItem: (id: string | null) => void;
onAddToCanvas: (item: PaletteItem) => void;
}
export default function IndicatorPaletteTab({
kind, items, onItemsChange, def, searchQuery, selectedItemId, onSelectItem, onAddToCanvas,
}: Props) {
const [modalOpen, setModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<'add' | 'edit'>('add');
const [editTarget, setEditTarget] = useState<PaletteItem | null>(null);
const q = searchQuery.trim().toLowerCase();
const filtered = useMemo(() => {
if (!q) return items;
return items.filter(
i => i.label.toLowerCase().includes(q)
|| i.desc.toLowerCase().includes(q)
|| i.value.toLowerCase().includes(q),
);
}, [items, q]);
const selected = items.find(i => i.id === selectedItemId) ?? null;
const persist = useCallback((next: PaletteItem[]) => {
onItemsChange(next);
savePaletteItems(kind, next);
}, [kind, onItemsChange]);
const openAdd = () => {
setEditTarget(null);
setModalMode('add');
setModalOpen(true);
};
const openEdit = () => {
if (!selected) {
window.alert('수정할 지표 카드를 먼저 선택해 주세요.');
return;
}
setEditTarget(selected);
setModalMode('edit');
setModalOpen(true);
};
const handleDelete = () => {
if (!selected) {
window.alert('삭제할 지표 카드를 먼저 선택해 주세요.');
return;
}
if (!window.confirm(`${selected.label}」을(를) 목록에서 삭제할까요?`)) return;
const next = items.filter(i => i.id !== selected.id);
persist(next);
onSelectItem(null);
};
const handleSave = (draft: Omit<PaletteItem, 'id' | 'kind'> & { id?: string }) => {
if (modalMode === 'add') {
const item: PaletteItem = {
id: createPaletteItemId(),
kind,
value: draft.value,
label: draft.label,
desc: draft.desc,
period: draft.period,
shortPeriod: draft.shortPeriod,
longPeriod: draft.longPeriod,
};
persist([...items, item]);
onSelectItem(item.id);
return;
}
const id = draft.id ?? editTarget?.id;
if (!id) return;
persist(items.map(i => (i.id === id ? {
...i,
label: draft.label,
desc: draft.desc,
period: draft.period,
shortPeriod: draft.shortPeriod,
longPeriod: draft.longPeriod,
} : i)));
};
return (
<div className={`se-palette-section se-palette-section--${kind === 'auxiliary' ? 'aux' : 'composite'} se-palette-section--scroll`}>
<div className="se-palette-toolbar">
<span className="se-palette-toolbar-title">{kind === 'auxiliary' ? '보조지표' : '복합지표'}</span>
<div className="se-palette-toolbar-actions" role="toolbar" aria-label={`${kind === 'auxiliary' ? '보조' : '복합'}지표 관리`}>
<button
type="button"
className="se-palette-tool-btn"
title="지표 추가"
aria-label="지표 추가"
onClick={openAdd}
>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden>
<path d="M12 5v14M5 12h14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
<button
type="button"
className="se-palette-tool-btn"
title="선택 지표 수정"
aria-label="선택 지표 수정"
disabled={!selected}
onClick={openEdit}
>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden>
<path d="M4 20h4l10-10-4-4L4 16v4z" stroke="currentColor" strokeWidth="1.75" fill="none" strokeLinejoin="round" />
<path d="M14 6l4 4" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" />
</svg>
</button>
<button
type="button"
className="se-palette-tool-btn se-palette-tool-btn--danger"
title="선택 지표 삭제"
aria-label="선택 지표 삭제"
disabled={!selected}
onClick={handleDelete}
>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden>
<path d="M6 7h12M9 7V5h6v2M10 11v6M14 11v6M8 7l1 12h6l1-12" stroke="currentColor" strokeWidth="1.75" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</div>
</div>
<div className="se-palette-grid se-palette-grid--3">
{filtered.map(item => (
<PaletteChip
key={item.id}
type="indicator"
value={item.value}
label={item.label}
desc={item.desc}
color={kind === 'composite' ? 'composite' : 'ind'}
composite={kind === 'composite'}
period={periodLabel(item, def)}
periodValue={item.period}
shortPeriod={item.shortPeriod}
longPeriod={item.longPeriod}
selected={selectedItemId === item.id}
onSelect={() => onSelectItem(item.id)}
onAdd={() => onAddToCanvas(item)}
/>
))}
</div>
{filtered.length === 0 && (
<p className="se-palette-empty"> .</p>
)}
<PaletteItemModal
open={modalOpen}
mode={modalMode}
kind={kind}
initial={editTarget}
def={def}
onClose={() => setModalOpen(false)}
onSave={handleSave}
/>
</div>
);
}