전략편집기 템플릿 탭 추가,수정, 삭제 기능 적용
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
|||||||
type StartCombineOp,
|
type StartCombineOp,
|
||||||
} from '../utils/strategyStartNodes';
|
} from '../utils/strategyStartNodes';
|
||||||
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
|
import IndicatorPaletteTab from './strategyEditor/IndicatorPaletteTab';
|
||||||
|
import TemplatePaletteTab, { type TemplatePaletteRow } from './strategyEditor/TemplatePaletteTab';
|
||||||
import SidewaysFilterPaletteTab from './strategyEditor/SidewaysFilterPaletteTab';
|
import SidewaysFilterPaletteTab from './strategyEditor/SidewaysFilterPaletteTab';
|
||||||
import { buildSidewaysFilterNode, loadSidewaysPaletteItems } from '../utils/sidewaysFilterPaletteStorage';
|
import { buildSidewaysFilterNode, loadSidewaysPaletteItems } from '../utils/sidewaysFilterPaletteStorage';
|
||||||
import {
|
import {
|
||||||
@@ -121,6 +122,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
addUserStrategyTemplate,
|
addUserStrategyTemplate,
|
||||||
deleteUserStrategyTemplate,
|
deleteUserStrategyTemplate,
|
||||||
|
updateUserStrategyTemplate,
|
||||||
hideBuiltinTemplate,
|
hideBuiltinTemplate,
|
||||||
loadHiddenBuiltinTemplateIds,
|
loadHiddenBuiltinTemplateIds,
|
||||||
loadUserStrategyTemplates,
|
loadUserStrategyTemplates,
|
||||||
@@ -280,9 +282,13 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
() => loadHiddenBuiltinTemplateIds(),
|
() => loadHiddenBuiltinTemplateIds(),
|
||||||
);
|
);
|
||||||
const [templateSaveOpen, setTemplateSaveOpen] = useState(false);
|
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 [templateSaveLabel, setTemplateSaveLabel] = useState('');
|
||||||
const [templateSaveDesc, setTemplateSaveDesc] = useState('');
|
const [templateSaveDesc, setTemplateSaveDesc] = useState('');
|
||||||
const [templateSaveLabelError, setTemplateSaveLabelError] = useState(false);
|
const [templateSaveLabelError, setTemplateSaveLabelError] = useState(false);
|
||||||
|
const [selectedTemplateKey, setSelectedTemplateKey] = useState<string | null>(null);
|
||||||
const [selectedPaletteKey, setSelectedPaletteKey] = useState<string | null>(null);
|
const [selectedPaletteKey, setSelectedPaletteKey] = useState<string | null>(null);
|
||||||
const [stratName, setStratName] = useState('');
|
const [stratName, setStratName] = useState('');
|
||||||
const [stratDesc, setStratDesc] = useState('');
|
const [stratDesc, setStratDesc] = useState('');
|
||||||
@@ -1416,24 +1422,87 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
showSnack('템플릿을 목록에서 제거했습니다');
|
showSnack('템플릿을 목록에서 제거했습니다');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openTemplateSaveModal = useCallback(() => {
|
const openTemplateAddModal = useCallback(() => {
|
||||||
const hasContent = encodeConditionForSave(currentEditorState) != null || currentOrphans.length > 0;
|
const hasContent = encodeConditionForSave(currentEditorState) != null || currentOrphans.length > 0;
|
||||||
if (!hasContent) {
|
if (!hasContent) {
|
||||||
showSnack('저장할 조건이 없습니다', false);
|
showSnack('저장할 조건이 없습니다', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setTemplateModalMode('add');
|
||||||
|
setTemplateEditId(null);
|
||||||
|
setTemplateSaveUpdateContent(false);
|
||||||
setTemplateSaveLabel(stratName.trim() || `${signalTab === 'buy' ? '매수' : '매도'} 템플릿`);
|
setTemplateSaveLabel(stratName.trim() || `${signalTab === 'buy' ? '매수' : '매도'} 템플릿`);
|
||||||
setTemplateSaveDesc('');
|
setTemplateSaveDesc('');
|
||||||
setTemplateSaveLabelError(false);
|
setTemplateSaveLabelError(false);
|
||||||
setTemplateSaveOpen(true);
|
setTemplateSaveOpen(true);
|
||||||
}, [currentEditorState, currentOrphans.length, stratName, signalTab]);
|
}, [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 handleTemplateSaveConfirm = useCallback(() => {
|
||||||
const label = templateSaveLabel.trim();
|
const label = templateSaveLabel.trim();
|
||||||
if (!label) {
|
if (!label) {
|
||||||
setTemplateSaveLabelError(true);
|
setTemplateSaveLabelError(true);
|
||||||
return;
|
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({
|
const saved = addUserStrategyTemplate({
|
||||||
label,
|
label,
|
||||||
description: templateSaveDesc.trim() || undefined,
|
description: templateSaveDesc.trim() || undefined,
|
||||||
@@ -1442,16 +1511,21 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
orphans: currentOrphans,
|
orphans: currentOrphans,
|
||||||
});
|
});
|
||||||
setUserTemplates(prev => [...prev, saved]);
|
setUserTemplates(prev => [...prev, saved]);
|
||||||
|
setSelectedTemplateKey(saved.id);
|
||||||
setTemplateSaveOpen(false);
|
setTemplateSaveOpen(false);
|
||||||
setRightTab('templates');
|
setRightTab('templates');
|
||||||
showSnack(`"${saved.label}" 템플릿을 저장했습니다`);
|
showSnack(`"${saved.label}" 템플릿을 저장했습니다`);
|
||||||
}, [
|
}, [
|
||||||
templateSaveLabel,
|
templateSaveLabel,
|
||||||
templateSaveDesc,
|
templateSaveDesc,
|
||||||
|
templateModalMode,
|
||||||
|
templateEditId,
|
||||||
|
templateSaveUpdateContent,
|
||||||
signalTab,
|
signalTab,
|
||||||
currentRoot,
|
currentRoot,
|
||||||
currentLayout,
|
currentLayout,
|
||||||
currentOrphans,
|
currentOrphans,
|
||||||
|
currentEditorState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleQuickRun = useCallback(async () => {
|
const handleQuickRun = useCallback(async () => {
|
||||||
@@ -1805,7 +1879,7 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="se-btn se-btn--ghost se-btn--sm"
|
className="se-btn se-btn--ghost se-btn--sm"
|
||||||
onClick={openTemplateSaveModal}
|
onClick={openTemplateAddModal}
|
||||||
title="현재 탭(매수/매도) 조건을 템플릿 목록에 저장"
|
title="현재 탭(매수/매도) 조건을 템플릿 목록에 저장"
|
||||||
>
|
>
|
||||||
템플릿 저장
|
템플릿 저장
|
||||||
@@ -2145,64 +2219,17 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{rightTab === 'templates' && (
|
{rightTab === 'templates' && (
|
||||||
<>
|
<TemplatePaletteTab
|
||||||
<input
|
rows={filteredTemplateRows}
|
||||||
className="se-palette-search"
|
searchQuery={templateSearch}
|
||||||
placeholder="템플릿 검색 (RSI, MA, 매수…)"
|
onSearchChange={setTemplateSearch}
|
||||||
value={templateSearch}
|
selectedKey={selectedTemplateKey}
|
||||||
onChange={e => setTemplateSearch(e.target.value)}
|
onSelectKey={setSelectedTemplateKey}
|
||||||
/>
|
onApply={handleTemplateApply}
|
||||||
<div className="se-template-list">
|
onAdd={openTemplateAddModal}
|
||||||
{filteredTemplateRows.length === 0 ? (
|
onEdit={openTemplateEditModal}
|
||||||
<div className="se-template-empty">검색 결과가 없습니다</div>
|
onDelete={handleTemplateToolbarDelete}
|
||||||
) : (
|
/>
|
||||||
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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2213,10 +2240,15 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{templateSaveOpen && (
|
{templateSaveOpen && (
|
||||||
<DraggableModalFrame onClose={() => { setTemplateSaveLabelError(false); setTemplateSaveOpen(false); }} title="템플릿 저장">
|
<DraggableModalFrame
|
||||||
|
onClose={() => { setTemplateSaveLabelError(false); setTemplateSaveOpen(false); }}
|
||||||
|
title={templateModalMode === 'edit' ? '템플릿 수정' : '템플릿 저장'}
|
||||||
|
>
|
||||||
<div className="se-modal-body">
|
<div className="se-modal-body">
|
||||||
<p className="se-template-save-hint">
|
<p className="se-template-save-hint">
|
||||||
현재 <strong>{signalTab === 'buy' ? '매수' : '매도'}</strong> 탭 조건을 템플릿 목록에 저장합니다.
|
{templateModalMode === 'edit'
|
||||||
|
? <>선택한 <strong>내 템플릿</strong>의 이름·설명을 수정합니다.</>
|
||||||
|
: <>현재 <strong>{signalTab === 'buy' ? '매수' : '매도'}</strong> 탭 조건을 템플릿 목록에 저장합니다.</>}
|
||||||
</p>
|
</p>
|
||||||
<label className="se-field-lbl">템플릿명 *</label>
|
<label className="se-field-lbl">템플릿명 *</label>
|
||||||
<input
|
<input
|
||||||
@@ -2236,9 +2268,21 @@ export default function StrategyEditorPage({ theme, onNavigateToBacktest }: Prop
|
|||||||
rows={2}
|
rows={2}
|
||||||
placeholder="템플릿 설명 (선택)"
|
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">
|
<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--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>
|
||||||
</div>
|
</div>
|
||||||
</DraggableModalFrame>
|
</DraggableModalFrame>
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { StrategyTemplateDef } from '../../utils/strategyPresets';
|
||||||
|
import type { UserStrategyTemplate } from '../../utils/strategyTemplateStorage';
|
||||||
|
|
||||||
|
export type TemplatePaletteRow = {
|
||||||
|
key: string;
|
||||||
|
source: 'user' | 'builtin';
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
signal: 'buy' | 'sell';
|
||||||
|
user?: UserStrategyTemplate;
|
||||||
|
builtinId?: string;
|
||||||
|
builtin?: StrategyTemplateDef;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
rows: TemplatePaletteRow[];
|
||||||
|
searchQuery: string;
|
||||||
|
onSearchChange: (query: string) => void;
|
||||||
|
selectedKey: string | null;
|
||||||
|
onSelectKey: (key: string | null) => void;
|
||||||
|
onApply: (row: TemplatePaletteRow) => void;
|
||||||
|
onAdd: () => void;
|
||||||
|
onEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TemplatePaletteTab({
|
||||||
|
rows,
|
||||||
|
searchQuery,
|
||||||
|
onSearchChange,
|
||||||
|
selectedKey,
|
||||||
|
onSelectKey,
|
||||||
|
onApply,
|
||||||
|
onAdd,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<div className="se-palette-section se-palette-section--templates se-palette-section--scroll">
|
||||||
|
<div className="se-palette-toolbar">
|
||||||
|
<span className="se-palette-toolbar-title">템플릿</span>
|
||||||
|
<div className="se-palette-toolbar-actions" role="toolbar" aria-label="템플릿 관리">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="se-palette-tool-btn"
|
||||||
|
title="현재 조건을 템플릿으로 추가"
|
||||||
|
aria-label="템플릿 추가"
|
||||||
|
onClick={onAdd}
|
||||||
|
>
|
||||||
|
<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={!selectedKey}
|
||||||
|
onClick={onEdit}
|
||||||
|
>
|
||||||
|
<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={!selectedKey}
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="se-palette-search"
|
||||||
|
placeholder="템플릿 검색 (RSI, MA, 매수…)"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => onSearchChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="se-template-list">
|
||||||
|
{rows.length === 0 ? (
|
||||||
|
<div className="se-template-empty">검색 결과가 없습니다</div>
|
||||||
|
) : (
|
||||||
|
rows.map(row => (
|
||||||
|
<div
|
||||||
|
key={row.key}
|
||||||
|
className={`se-template-row${selectedKey === row.key ? ' se-template-row--selected' : ''}`}
|
||||||
|
onClick={() => onSelectKey(row.key)}
|
||||||
|
onDoubleClick={() => onApply(row)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
onApply(row);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="se-template-item">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2250,8 +2250,14 @@
|
|||||||
.se-template-row {
|
.se-template-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 4px;
|
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.12s, box-shadow 0.12s;
|
||||||
|
}
|
||||||
|
.se-template-row--selected .se-template-item {
|
||||||
|
border-color: var(--se-gold);
|
||||||
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--se-gold) 35%, transparent);
|
||||||
}
|
}
|
||||||
.se-template-item {
|
.se-template-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2260,36 +2266,27 @@
|
|||||||
gap: 6px 8px;
|
gap: 6px 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--se-border);
|
border: 1px solid var(--se-border);
|
||||||
background: var(--se-palette-card-bg);
|
background: var(--se-palette-card-bg);
|
||||||
color: var(--se-text);
|
color: var(--se-text);
|
||||||
cursor: pointer;
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: border-color 0.12s, box-shadow 0.12s;
|
transition: border-color 0.12s, box-shadow 0.12s;
|
||||||
}
|
}
|
||||||
.se-template-item:hover {
|
.se-template-row:hover .se-template-item {
|
||||||
border-color: var(--se-palette-hover-border);
|
border-color: var(--se-palette-hover-border);
|
||||||
box-shadow: 0 0 12px color-mix(in srgb, var(--se-gold) 10%, transparent);
|
box-shadow: 0 0 12px color-mix(in srgb, var(--se-gold) 10%, transparent);
|
||||||
}
|
}
|
||||||
.se-template-delete {
|
.se-palette-section--templates {
|
||||||
flex-shrink: 0;
|
|
||||||
width: 34px;
|
|
||||||
border: 1px solid var(--se-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--se-palette-card-bg);
|
|
||||||
color: var(--se-text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
flex: 1;
|
||||||
transition: color 0.12s, border-color 0.12s, background 0.12s;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
.se-template-delete:hover {
|
.se-palette-section--templates .se-palette-search {
|
||||||
color: var(--se-danger);
|
flex-shrink: 0;
|
||||||
border-color: color-mix(in srgb, var(--se-danger) 45%, var(--se-border));
|
|
||||||
background: color-mix(in srgb, var(--se-danger) 8%, var(--se-palette-card-bg));
|
|
||||||
}
|
}
|
||||||
.se-template-label {
|
.se-template-label {
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
@@ -2314,6 +2311,18 @@
|
|||||||
color: var(--se-text-muted);
|
color: var(--se-text-muted);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
.se-template-save-update {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
color: var(--se-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.se-template-save-update input {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
.se-template-desc {
|
.se-template-desc {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
font-size: 0.66rem;
|
font-size: 0.66rem;
|
||||||
|
|||||||
@@ -65,6 +65,23 @@ export function deleteUserStrategyTemplate(id: string): UserStrategyTemplate[] {
|
|||||||
return templates;
|
return templates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateUserStrategyTemplate(
|
||||||
|
id: string,
|
||||||
|
patch: Partial<Pick<UserStrategyTemplate, 'label' | 'description' | 'signal' | 'editorState' | 'orphans'>>,
|
||||||
|
): UserStrategyTemplate[] {
|
||||||
|
const templates = loadUserStrategyTemplates().map(t => {
|
||||||
|
if (t.id !== id) return t;
|
||||||
|
return {
|
||||||
|
...t,
|
||||||
|
...patch,
|
||||||
|
...(patch.editorState != null ? { editorState: cloneJson(patch.editorState) } : null),
|
||||||
|
...(patch.orphans != null ? { orphans: cloneJson(patch.orphans) } : null),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
saveUserStrategyTemplates(templates);
|
||||||
|
return templates;
|
||||||
|
}
|
||||||
|
|
||||||
export function loadHiddenBuiltinTemplateIds(): string[] {
|
export function loadHiddenBuiltinTemplateIds(): string[] {
|
||||||
const raw = getUiPreferences().strategyEditor?.hiddenBuiltinTemplateIds;
|
const raw = getUiPreferences().strategyEditor?.hiddenBuiltinTemplateIds;
|
||||||
if (!Array.isArray(raw)) return [];
|
if (!Array.isArray(raw)) return [];
|
||||||
|
|||||||
Reference in New Issue
Block a user