전략편집기 템플릿 탭 추가,수정, 삭제 기능 적용

This commit is contained in:
Macbook
2026-06-12 13:30:40 +09:00
parent 52137cf1db
commit cb1bde2563
4 changed files with 278 additions and 81 deletions
+107 -63
View File
@@ -45,6 +45,7 @@ import {
type StartCombineOp,
} from '../utils/strategyStartNodes';
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
import TemplatePaletteTab, { type TemplatePaletteRow } from './strategyEditor/TemplatePaletteTab';
import SidewaysFilterPaletteTab from './strategyEditor/SidewaysFilterPaletteTab';
import { buildSidewaysFilterNode, loadSidewaysPaletteItems } from '../utils/sidewaysFilterPaletteStorage';
import {
@@ -121,6 +122,7 @@ import {
import {
addUserStrategyTemplate,
deleteUserStrategyTemplate,
updateUserStrategyTemplate,
hideBuiltinTemplate,
loadHiddenBuiltinTemplateIds,
loadUserStrategyTemplates,
@@ -280,9 +282,13 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
() => loadHiddenBuiltinTemplateIds(),
);
const [templateSaveOpen, setTemplateSaveOpen] = useState(false);
const [templateModalMode, setTemplateModalMode] = useState<'add' | 'edit'>('add');
const [templateEditId, setTemplateEditId] = useState<string | null>(null);
const [templateSaveUpdateContent, setTemplateSaveUpdateContent] = useState(false);
const [templateSaveLabel, setTemplateSaveLabel] = useState('');
const [templateSaveDesc, setTemplateSaveDesc] = useState('');
const [templateSaveLabelError, setTemplateSaveLabelError] = useState(false);
const [selectedTemplateKey, setSelectedTemplateKey] = useState<string | null>(null);
const [selectedPaletteKey, setSelectedPaletteKey] = useState<string | null>(null);
const [stratName, setStratName] = useState('');
const [stratDesc, setStratDesc] = useState('');
@@ -1416,24 +1422,87 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
showSnack('템플릿을 목록에서 제거했습니다');
}, []);
const openTemplateSaveModal = useCallback(() => {
const openTemplateAddModal = useCallback(() => {
const hasContent = encodeConditionForSave(currentEditorState) != null || currentOrphans.length > 0;
if (!hasContent) {
showSnack('저장할 조건이 없습니다', false);
return;
}
setTemplateModalMode('add');
setTemplateEditId(null);
setTemplateSaveUpdateContent(false);
setTemplateSaveLabel(stratName.trim() || `${signalTab === 'buy' ? '매수' : '매도'} 템플릿`);
setTemplateSaveDesc('');
setTemplateSaveLabelError(false);
setTemplateSaveOpen(true);
}, [currentEditorState, currentOrphans.length, stratName, signalTab]);
const openTemplateEditModal = useCallback(() => {
if (!selectedTemplateKey) {
window.alert('수정할 템플릿을 먼저 선택해 주세요.');
return;
}
const row = templateRows.find(r => r.key === selectedTemplateKey);
if (!row) return;
if (row.source !== 'user' || !row.user) {
window.alert('내 템플릿만 수정할 수 있습니다.');
return;
}
setTemplateModalMode('edit');
setTemplateEditId(row.user.id);
setTemplateSaveUpdateContent(false);
setTemplateSaveLabel(row.user.label);
setTemplateSaveDesc(row.user.description ?? '');
setTemplateSaveLabelError(false);
setTemplateSaveOpen(true);
}, [selectedTemplateKey, templateRows]);
const handleTemplateToolbarDelete = useCallback(() => {
if (!selectedTemplateKey) {
window.alert('삭제할 템플릿을 먼저 선택해 주세요.');
return;
}
const row = templateRows.find(r => r.key === selectedTemplateKey);
if (!row) return;
if (row.source === 'user' && row.user) {
handleDeleteUserTemplate(row.user.id, row.label);
} else if (row.source === 'builtin' && row.builtinId) {
handleDeleteBuiltinTemplate(row.builtinId, row.label);
}
setSelectedTemplateKey(null);
}, [selectedTemplateKey, templateRows, handleDeleteUserTemplate, handleDeleteBuiltinTemplate]);
const handleTemplateApply = useCallback((row: TemplatePaletteRow) => {
if (row.source === 'user' && row.user) applyUserTemplate(row.user);
else if (row.builtin) handleTemplate(row.builtin);
}, [applyUserTemplate, handleTemplate]);
const handleTemplateSaveConfirm = useCallback(() => {
const label = templateSaveLabel.trim();
if (!label) {
setTemplateSaveLabelError(true);
return;
}
if (templateModalMode === 'edit' && templateEditId) {
const patch: Parameters<typeof updateUserStrategyTemplate>[1] = {
label,
description: templateSaveDesc.trim() || undefined,
};
if (templateSaveUpdateContent) {
const hasContent = encodeConditionForSave(currentEditorState) != null || currentOrphans.length > 0;
if (!hasContent) {
showSnack('갱신할 조건이 없습니다', false);
return;
}
patch.signal = signalTab;
patch.editorState = toEditorState(currentRoot, currentLayout);
patch.orphans = currentOrphans;
}
setUserTemplates(updateUserStrategyTemplate(templateEditId, patch));
setTemplateSaveOpen(false);
showSnack(`"${label}" 템플릿을 수정했습니다`);
return;
}
const saved = addUserStrategyTemplate({
label,
description: templateSaveDesc.trim() || undefined,
@@ -1442,16 +1511,21 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
orphans: currentOrphans,
});
setUserTemplates(prev => [...prev, saved]);
setSelectedTemplateKey(saved.id);
setTemplateSaveOpen(false);
setRightTab('templates');
showSnack(`"${saved.label}" 템플릿을 저장했습니다`);
}, [
templateSaveLabel,
templateSaveDesc,
templateModalMode,
templateEditId,
templateSaveUpdateContent,
signalTab,
currentRoot,
currentLayout,
currentOrphans,
currentEditorState,
]);
const handleQuickRun = useCallback(async () => {
@@ -1805,7 +1879,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
<button
type="button"
className="se-btn se-btn--ghost se-btn--sm"
onClick={openTemplateSaveModal}
onClick={openTemplateAddModal}
title="현재 탭(매수/매도) 조건을 템플릿 목록에 저장"
>
릿
@@ -2145,64 +2219,17 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
</>
)}
{rightTab === 'templates' && (
<>
<input
className="se-palette-search"
placeholder="템플릿 검색 (RSI, MA, 매수…)"
value={templateSearch}
onChange={e => setTemplateSearch(e.target.value)}
/>
<div className="se-template-list">
{filteredTemplateRows.length === 0 ? (
<div className="se-template-empty"> </div>
) : (
filteredTemplateRows.map(row => (
<div key={row.key} className="se-template-row">
<button
type="button"
className="se-template-item"
onClick={() => {
if (row.source === 'user') applyUserTemplate(row.user);
else handleTemplate(row.builtin);
}}
>
<span className="se-template-label">
{row.source === 'user' && <span className="se-template-user-badge"> 릿</span>}
{row.label}
</span>
{row.description && (
<span className="se-template-desc">{row.description}</span>
)}
<span className={`se-template-signal se-template-signal--${row.signal}`}>
{row.signal === 'buy' ? '매수' : '매도'}
</span>
</button>
<button
type="button"
className="se-template-delete"
title="템플릿 삭제"
aria-label={`${row.label} 템플릿 삭제`}
onClick={e => {
e.stopPropagation();
if (row.source === 'user') {
handleDeleteUserTemplate(row.user.id, row.label);
} else {
handleDeleteBuiltinTemplate(row.builtinId, row.label);
}
}}
>
<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden>
<path
fill="currentColor"
d="M5.5 2a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v.5H12a.5.5 0 0 1 0 1h-.55l-.62 8.07A1.5 1.5 0 0 1 9.83 13H6.17a1.5 1.5 0 0 1-1.49-1.43L4.05 3.5H4a.5.5 0 0 1 0-1h1.5V2zm1.5 0v.5h2V2H7zm-2.38 1.5l.58 7.53a.5.5 0 0 0 .5.47h3.66a.5.5 0 0 0 .5-.47l.58-7.53H4.62z"
/>
</svg>
</button>
</div>
))
)}
</div>
</>
<TemplatePaletteTab
rows={filteredTemplateRows}
searchQuery={templateSearch}
onSearchChange={setTemplateSearch}
selectedKey={selectedTemplateKey}
onSelectKey={setSelectedTemplateKey}
onApply={handleTemplateApply}
onAdd={openTemplateAddModal}
onEdit={openTemplateEditModal}
onDelete={handleTemplateToolbarDelete}
/>
)}
</div>
</div>
@@ -2213,10 +2240,15 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
</div>
{templateSaveOpen && (
<DraggableModalFrame onClose={() => { setTemplateSaveLabelError(false); setTemplateSaveOpen(false); }} title="템플릿 저장">
<DraggableModalFrame
onClose={() => { setTemplateSaveLabelError(false); setTemplateSaveOpen(false); }}
title={templateModalMode === 'edit' ? '템플릿 수정' : '템플릿 저장'}
>
<div className="se-modal-body">
<p className="se-template-save-hint">
<strong>{signalTab === 'buy' ? '매수' : '매도'}</strong> 릿 .
{templateModalMode === 'edit'
? <> <strong> 릿</strong> · .</>
: <> <strong>{signalTab === 'buy' ? '매수' : '매도'}</strong> 릿 .</>}
</p>
<label className="se-field-lbl">릿 *</label>
<input
@@ -2236,9 +2268,21 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
rows={2}
placeholder="템플릿 설명 (선택)"
/>
{templateModalMode === 'edit' && (
<label className="se-template-save-update">
<input
type="checkbox"
checked={templateSaveUpdateContent}
onChange={e => setTemplateSaveUpdateContent(e.target.checked)}
/>
<span> {signalTab === 'buy' ? '매수' : '매도'} </span>
</label>
)}
<div className="se-modal-actions">
<button type="button" className="se-btn se-btn--ghost" onClick={() => setTemplateSaveOpen(false)}></button>
<button type="button" className="se-btn se-btn--gold" onClick={handleTemplateSaveConfirm}></button>
<button type="button" className="se-btn se-btn--gold" onClick={handleTemplateSaveConfirm}>
{templateModalMode === 'edit' ? '수정' : '저장'}
</button>
</div>
</div>
</DraggableModalFrame>