전략편집기 수정
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user