전략평가 화면에서 전략조건 수정
This commit is contained in:
@@ -23,7 +23,8 @@ import { resolveStrategyPrimaryTimeframe } from '../utils/strategyToChartIndicat
|
||||
import { fetchEvaluationSnapshotAtBar } from '../utils/strategyEvaluationSnapshot';
|
||||
import StrategyEvaluationSignalPanel from './strategyEvaluation/StrategyEvaluationSignalPanel';
|
||||
import StrategyEvaluationChart from './strategyEvaluation/StrategyEvaluationChart';
|
||||
import StrategyEvaluationConditionPanel from './strategyEvaluation/StrategyEvaluationConditionPanel';
|
||||
import StrategyEvaluationStrategyPanel from './strategyEvaluation/StrategyEvaluationStrategyPanel';
|
||||
import StrategyEvaluationIndicatorPalettePanel from './strategyEvaluation/StrategyEvaluationIndicatorPalettePanel';
|
||||
import StrategyEvaluationSettingsTab from './strategyEvaluation/StrategyEvaluationSettingsTab';
|
||||
import StrategyEvaluationMarketTab from './strategyEvaluation/StrategyEvaluationMarketTab';
|
||||
import StrategyEditorModal from './strategyEvaluation/StrategyEditorModal';
|
||||
@@ -51,6 +52,9 @@ import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
|
||||
import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup';
|
||||
import StrategyEvaluationAiVerifyPanel from './strategyEvaluation/StrategyEvaluationAiVerifyPanel';
|
||||
import StrategyEvaluationChartSettingsTab from './strategyEvaluation/StrategyEvaluationChartSettingsTab';
|
||||
import PaletteDragOverlay from './strategyEditor/PaletteDragOverlay';
|
||||
import { needsPointerPaletteDrag } from '../utils/paletteDragSession';
|
||||
import { useStrategyEvaluationEditor } from '../hooks/useStrategyEvaluationEditor';
|
||||
import {
|
||||
buildStrategyEvaluationAiVerifyContext,
|
||||
requestStrategyEvaluationAiVerify,
|
||||
@@ -66,7 +70,7 @@ const LEFT_DEFAULT = 380;
|
||||
const RIGHT_DEFAULT = 440;
|
||||
|
||||
type LeftTab = 'strategy' | 'market' | 'settings';
|
||||
type RightTab = 'signal' | 'chart' | 'ai';
|
||||
type RightTab = 'signal' | 'indicators' | 'chart' | 'ai';
|
||||
|
||||
function readMinWidth(key: string, fallback: number): number {
|
||||
return Math.max(readStoredSize(key, fallback), fallback);
|
||||
@@ -139,6 +143,34 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
[baseGetParams, appliedIndicatorParams],
|
||||
);
|
||||
|
||||
const handleStrategyEditorSaved = useCallback((saved: StrategyDto, meta?: { created?: boolean }) => {
|
||||
const hydrated = hydrateStrategyDto(saved);
|
||||
setSelectedStrategy(hydrated);
|
||||
setStrategies(prev => {
|
||||
const exists = prev.some(s => s.id === hydrated.id);
|
||||
if (exists) return prev.map(s => (s.id === hydrated.id ? hydrated : s));
|
||||
return [...prev, hydrated];
|
||||
});
|
||||
if (meta?.created && hydrated.id != null) {
|
||||
setSelectedStrategyId(hydrated.id);
|
||||
}
|
||||
evalSessionRef.current += 1;
|
||||
setSnapshot(undefined);
|
||||
setEvalError(null);
|
||||
setEvalLoading(true);
|
||||
reportCacheKeyRef.current = null;
|
||||
setReportModel(null);
|
||||
}, []);
|
||||
|
||||
const strategyEditor = useStrategyEvaluationEditor({
|
||||
strategy: selectedStrategy,
|
||||
strategies,
|
||||
getParams: baseGetParams,
|
||||
getVisualConfig,
|
||||
settingsRevision,
|
||||
onStrategySaved: handleStrategyEditorSaved,
|
||||
});
|
||||
|
||||
const [leftWidth, setLeftWidth] = useState(() => readMinWidth(LEFT_KEY, LEFT_DEFAULT));
|
||||
const [leftOpen, setLeftOpen] = useState(() => readStoredBool(LEFT_OPEN_KEY, true));
|
||||
const [rightWidth, setRightWidth] = useState(() => readMinWidth(RIGHT_KEY, RIGHT_DEFAULT));
|
||||
@@ -784,10 +816,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
refreshMarketTickers={marketFeed.refreshAllTickers}
|
||||
onMarketSearchOpenChange={setMarketSearchOpen}
|
||||
/>
|
||||
<StrategyEvaluationConditionPanel
|
||||
<StrategyEvaluationStrategyPanel
|
||||
strategy={selectedStrategy}
|
||||
getParams={baseGetParams}
|
||||
appliedParams={appliedIndicatorParams}
|
||||
editor={strategyEditor}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
@@ -800,7 +831,13 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
side="right"
|
||||
open={rightOpen}
|
||||
onToggle={toggleRightPanel}
|
||||
label={rightTab === 'chart' ? '차트설정 패널' : '시그널 패널'}
|
||||
label={
|
||||
rightTab === 'chart'
|
||||
? '차트설정 패널'
|
||||
: rightTab === 'indicators'
|
||||
? '전략지표 패널'
|
||||
: '시그널 패널'
|
||||
}
|
||||
/>
|
||||
{rightOpen && (
|
||||
<div
|
||||
@@ -824,6 +861,17 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
>
|
||||
시그널
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="seval-right-tab-indicators"
|
||||
aria-selected={rightTab === 'indicators'}
|
||||
aria-controls="seval-right-panel-indicators"
|
||||
className={`btd-exec-tab seval-right-tab${rightTab === 'indicators' ? ' btd-exec-tab--on' : ''}`}
|
||||
onClick={() => setRightTab('indicators')}
|
||||
>
|
||||
전략지표
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
@@ -905,6 +953,17 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rightTab === 'indicators' && (
|
||||
<div
|
||||
id="seval-right-panel-indicators"
|
||||
role="tabpanel"
|
||||
aria-labelledby="seval-right-tab-indicators"
|
||||
className="seval-right-tab-panel seval-right-tab-panel--indicators"
|
||||
>
|
||||
<StrategyEvaluationIndicatorPalettePanel editor={strategyEditor} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rightTab === 'chart' && (
|
||||
<div
|
||||
id="seval-right-panel-chart"
|
||||
@@ -947,6 +1006,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{needsPointerPaletteDrag() ? <PaletteDragOverlay /> : null}
|
||||
|
||||
{editorModalOpen && (
|
||||
<StrategyEditorModal
|
||||
theme={theme}
|
||||
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 전략 평가 — 우측 패널 전략지표 (전략편집기 지표 탭과 동일)
|
||||
*/
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import PaletteChip from '../strategyEditor/PaletteChip';
|
||||
import IndicatorPaletteTab from '../strategyEditor/IndicatorPaletteTab';
|
||||
import SidewaysFilterPaletteTab from '../strategyEditor/SidewaysFilterPaletteTab';
|
||||
import { getIndicatorPeriodLabel } from '../../utils/strategyFlowLayout';
|
||||
import {
|
||||
loadPaletteItems,
|
||||
type PaletteItem,
|
||||
} from '../../utils/strategyPaletteStorage';
|
||||
import { loadSidewaysPaletteItems } from '../../utils/sidewaysFilterPaletteStorage';
|
||||
import type { DefType } from '../../utils/strategyEditorShared';
|
||||
import type { useStrategyEvaluationEditor } from '../../hooks/useStrategyEvaluationEditor';
|
||||
|
||||
type EditorApi = Pick<
|
||||
ReturnType<typeof useStrategyEvaluationEditor>,
|
||||
'def' | 'applyPalette' | 'applyPaletteItem' | 'applySidewaysFilter'
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
editor: EditorApi;
|
||||
}
|
||||
|
||||
const OPERATORS = [
|
||||
{ type: 'operator' as const, value: 'AND', label: 'AND', color: 'logic-and' },
|
||||
{ type: 'operator' as const, value: 'OR', label: 'OR', color: 'logic-or' },
|
||||
{ type: 'operator' as const, value: 'NOT', label: 'NOT', color: 'logic-not' },
|
||||
];
|
||||
|
||||
const MA_BAND_ITEMS = [
|
||||
{ type: 'indicator' as const, value: 'MA', label: 'MA', desc: '이동평균', color: 'band' },
|
||||
{ type: 'indicator' as const, value: 'EMA', label: 'EMA', desc: '지수이동평균', color: 'band' },
|
||||
{ type: 'indicator' as const, value: 'BOLLINGER', label: 'Bollinger', desc: '볼린저밴드', color: 'band' },
|
||||
{ type: 'indicator' as const, value: 'DONCHIAN', label: 'Donchian', desc: '돈치안 채널', color: 'band' },
|
||||
{ type: 'indicator' as const, value: 'ICHIMOKU', label: 'Ichimoku', desc: '일목균형표', color: 'band' },
|
||||
];
|
||||
|
||||
function paletteKey(type: string, id: string) {
|
||||
return `${type}:${id}`;
|
||||
}
|
||||
|
||||
const StrategyEvaluationIndicatorPalettePanel: React.FC<Props> = ({ editor }) => {
|
||||
const { def, applyPalette, applyPaletteItem, applySidewaysFilter } = editor;
|
||||
|
||||
const [paletteSearch, setPaletteSearch] = useState('');
|
||||
const [indicatorSubTab, setIndicatorSubTab] = useState<'auxiliary' | 'composite' | 'range'>('auxiliary');
|
||||
const [auxiliaryPalette, setAuxiliaryPalette] = useState<PaletteItem[]>(() => loadPaletteItems('auxiliary'));
|
||||
const [compositePalette, setCompositePalette] = useState<PaletteItem[]>(() => loadPaletteItems('composite'));
|
||||
const [sidewaysPalette, setSidewaysPalette] = useState(() => loadSidewaysPaletteItems());
|
||||
const [selectedPaletteKey, setSelectedPaletteKey] = useState<string | null>(null);
|
||||
|
||||
const q = paletteSearch.trim().toLowerCase();
|
||||
const match = (label: string, desc?: string) =>
|
||||
!q || label.toLowerCase().includes(q) || (desc?.toLowerCase().includes(q) ?? false);
|
||||
|
||||
const isPaletteSelected = (type: string, id: string) =>
|
||||
selectedPaletteKey === paletteKey(type, id);
|
||||
|
||||
const selectPalette = (type: string, id: string) => {
|
||||
setSelectedPaletteKey(paletteKey(type, id));
|
||||
};
|
||||
|
||||
const filteredMaBand = useMemo(
|
||||
() => MA_BAND_ITEMS.filter(i => match(i.label, i.desc)),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[q],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="seval-indicator-palette se-palette-panel">
|
||||
<input
|
||||
className="se-palette-search"
|
||||
placeholder="지표 검색 (RSI, MACD…)"
|
||||
value={paletteSearch}
|
||||
onChange={e => setPaletteSearch(e.target.value)}
|
||||
/>
|
||||
<div className="se-palette-section se-palette-section--logic">
|
||||
<h3>시작 · 논리</h3>
|
||||
<div className="se-palette-grid se-palette-grid--3">
|
||||
<PaletteChip
|
||||
type="start"
|
||||
value="START"
|
||||
label="START"
|
||||
desc="시간봉 시작점"
|
||||
color="logic-start"
|
||||
selected={selectedPaletteKey === 'start:START'}
|
||||
onSelect={() => setSelectedPaletteKey('start:START')}
|
||||
onAdd={() => applyPalette('start', 'START')}
|
||||
/>
|
||||
{OPERATORS.map(op => (
|
||||
<PaletteChip
|
||||
key={op.value}
|
||||
{...op}
|
||||
selected={isPaletteSelected(op.type, op.value)}
|
||||
onSelect={() => selectPalette(op.type, op.value)}
|
||||
onAdd={() => applyPalette(op.type, op.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{filteredMaBand.length > 0 && (
|
||||
<div className="se-palette-section se-palette-section--band">
|
||||
<h3>밴드 · 추세</h3>
|
||||
<div className="se-palette-grid se-palette-grid--3">
|
||||
{filteredMaBand.map(item => (
|
||||
<PaletteChip
|
||||
key={item.value}
|
||||
{...item}
|
||||
period={getIndicatorPeriodLabel(item.value, def as DefType)}
|
||||
selected={isPaletteSelected(item.type, item.value)}
|
||||
onSelect={() => selectPalette(item.type, item.value)}
|
||||
onAdd={() => applyPalette(item.type, item.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="se-palette-subtabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`se-palette-subtab se-palette-subtab--aux${indicatorSubTab === 'auxiliary' ? ' se-palette-subtab--on' : ''}`}
|
||||
onClick={() => {
|
||||
setIndicatorSubTab('auxiliary');
|
||||
setSelectedPaletteKey(null);
|
||||
}}
|
||||
>
|
||||
보조지표
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`se-palette-subtab se-palette-subtab--composite${indicatorSubTab === 'composite' ? ' se-palette-subtab--on' : ''}`}
|
||||
onClick={() => {
|
||||
setIndicatorSubTab('composite');
|
||||
setSelectedPaletteKey(null);
|
||||
}}
|
||||
>
|
||||
복합지표
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`se-palette-subtab se-palette-subtab--range${indicatorSubTab === 'range' ? ' se-palette-subtab--on' : ''}`}
|
||||
onClick={() => {
|
||||
setIndicatorSubTab('range');
|
||||
setSelectedPaletteKey(null);
|
||||
}}
|
||||
>
|
||||
횡보필터
|
||||
</button>
|
||||
</div>
|
||||
{indicatorSubTab === 'auxiliary' ? (
|
||||
<IndicatorPaletteTab
|
||||
kind="auxiliary"
|
||||
items={auxiliaryPalette}
|
||||
onItemsChange={setAuxiliaryPalette}
|
||||
def={def as DefType}
|
||||
searchQuery={paletteSearch}
|
||||
selectedItemId={
|
||||
selectedPaletteKey?.startsWith('auxiliary:')
|
||||
? selectedPaletteKey.slice('auxiliary:'.length)
|
||||
: null
|
||||
}
|
||||
onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('auxiliary', id) : null)}
|
||||
onAddToCanvas={item => {
|
||||
selectPalette('auxiliary', item.id);
|
||||
applyPaletteItem(item);
|
||||
}}
|
||||
/>
|
||||
) : indicatorSubTab === 'composite' ? (
|
||||
<IndicatorPaletteTab
|
||||
kind="composite"
|
||||
items={compositePalette}
|
||||
onItemsChange={setCompositePalette}
|
||||
def={def as DefType}
|
||||
searchQuery={paletteSearch}
|
||||
selectedItemId={
|
||||
selectedPaletteKey?.startsWith('composite:')
|
||||
? selectedPaletteKey.slice('composite:'.length)
|
||||
: null
|
||||
}
|
||||
onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('composite', id) : null)}
|
||||
onAddToCanvas={item => {
|
||||
selectPalette('composite', item.id);
|
||||
applyPaletteItem(item);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SidewaysFilterPaletteTab
|
||||
def={def as DefType}
|
||||
items={sidewaysPalette}
|
||||
onItemsChange={setSidewaysPalette}
|
||||
searchQuery={paletteSearch}
|
||||
selectedItemId={
|
||||
selectedPaletteKey?.startsWith('range:')
|
||||
? selectedPaletteKey.slice('range:'.length)
|
||||
: null
|
||||
}
|
||||
onSelectItem={id => setSelectedPaletteKey(id ? paletteKey('range', id) : null)}
|
||||
onAddToCanvas={item => {
|
||||
selectPalette('range', item.id);
|
||||
applySidewaysFilter(item.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyEvaluationIndicatorPalettePanel;
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 전략 평가 — 중앙 하단 목록 방식 전략 설정
|
||||
*/
|
||||
import React from 'react';
|
||||
import type { StrategyDto } from '../../utils/backendApi';
|
||||
import { repairUtf8Mojibake } from '../../utils/textEncoding';
|
||||
import StrategyListEditor from '../strategyEditor/StrategyListEditor';
|
||||
import StartCombineOpControl from '../strategyEditor/StartCombineOpControl';
|
||||
import DraggableModalFrame from '../DraggableModalFrame';
|
||||
import { hasMultipleStartSections, normalizeStartCombineOp } from '../../utils/strategyConditionSerde';
|
||||
import type { useStrategyEvaluationEditor } from '../../hooks/useStrategyEvaluationEditor';
|
||||
|
||||
type EditorApi = ReturnType<typeof useStrategyEvaluationEditor>;
|
||||
|
||||
interface Props {
|
||||
strategy: StrategyDto | null;
|
||||
editor: EditorApi;
|
||||
}
|
||||
|
||||
const StrategyEvaluationStrategyPanel: React.FC<Props> = ({ strategy, editor }) => {
|
||||
const {
|
||||
def,
|
||||
signalTab,
|
||||
switchSignalTab,
|
||||
currentEditorState,
|
||||
handleEditorStateChange,
|
||||
handleStartCombineOpChange,
|
||||
handleAddStartSection,
|
||||
currentOrphans,
|
||||
handleOrphansChange,
|
||||
saving,
|
||||
saveNow,
|
||||
openSaveDialog,
|
||||
closeSaveDialog,
|
||||
confirmSaveDialog,
|
||||
saveDialogOpen,
|
||||
saveDialogMode,
|
||||
saveDraftName,
|
||||
setSaveDraftName,
|
||||
saveDraftDesc,
|
||||
setSaveDraftDesc,
|
||||
saveNameError,
|
||||
saveDuplicateError,
|
||||
saveToast,
|
||||
clearSaveFieldErrors,
|
||||
} = editor;
|
||||
|
||||
if (!strategy) {
|
||||
return (
|
||||
<div className="seval-strategy-panel seval-strategy-panel--empty">
|
||||
<p className="seval-strategy-panel-empty">전략을 선택하면 목록 방식으로 조건을 편집할 수 있습니다.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const strategyName = repairUtf8Mojibake(strategy.name ?? `전략 #${strategy.id}`);
|
||||
|
||||
return (
|
||||
<div className="seval-strategy-panel">
|
||||
<header className="seval-strategy-panel-head">
|
||||
<div className="seval-strategy-panel-head-main">
|
||||
<div className="se-signal-tabs">
|
||||
<button
|
||||
type="button"
|
||||
className={`se-signal-tab${signalTab === 'buy' ? ' se-signal-tab--buy-on' : ''}`}
|
||||
onClick={() => switchSignalTab('buy')}
|
||||
>
|
||||
매수 조건 (Entry)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`se-signal-tab${signalTab === 'sell' ? ' se-signal-tab--sell-on' : ''}`}
|
||||
onClick={() => switchSignalTab('sell')}
|
||||
>
|
||||
매도 조건 (Exit)
|
||||
</button>
|
||||
</div>
|
||||
<div className="seval-strategy-panel-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="se-btn se-btn--gold se-btn--sm"
|
||||
disabled={saving}
|
||||
onClick={() => { void saveNow(); }}
|
||||
>
|
||||
{saving ? '저장 중…' : '저장'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="se-btn se-btn--ghost se-btn--sm"
|
||||
disabled={saving}
|
||||
onClick={() => openSaveDialog('saveAs')}
|
||||
title="현재 조건을 다른 이름으로 새 전략으로 저장 (기존 전략은 유지)"
|
||||
>
|
||||
새로저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="seval-strategy-panel-meta">
|
||||
{strategyName && <span className="se-editing-name">{strategyName}</span>}
|
||||
{saving && <span className="seval-strategy-panel-saving">저장 중…</span>}
|
||||
</div>
|
||||
{hasMultipleStartSections(currentEditorState) && (
|
||||
<StartCombineOpControl
|
||||
value={normalizeStartCombineOp(currentEditorState.startCombineOp)}
|
||||
onChange={handleStartCombineOpChange}
|
||||
/>
|
||||
)}
|
||||
</header>
|
||||
<div className="seval-strategy-panel-body se-center-work se-center-work--list">
|
||||
<StrategyListEditor
|
||||
editorState={currentEditorState}
|
||||
signalTab={signalTab}
|
||||
def={def}
|
||||
onEditorStateChange={handleEditorStateChange}
|
||||
onAddStart={handleAddStartSection}
|
||||
orphans={currentOrphans}
|
||||
onOrphansChange={handleOrphansChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{saveToast && (
|
||||
<div className="seval-strategy-save-toast" role="status">{saveToast}</div>
|
||||
)}
|
||||
|
||||
{saveDialogOpen && (
|
||||
<DraggableModalFrame
|
||||
onClose={closeSaveDialog}
|
||||
title={saveDialogMode === 'saveAs' ? '새 이름으로 저장' : '전략 저장'}
|
||||
>
|
||||
<div className="se-modal-body">
|
||||
{saveDialogMode === 'saveAs' && (
|
||||
<p className="se-template-save-hint">
|
||||
현재 편집 중인 조건을 <strong>새 전략</strong>으로 저장합니다.
|
||||
기존 전략「{strategyName}」은 변경되지 않습니다.
|
||||
</p>
|
||||
)}
|
||||
<label className="se-field-lbl">전략명 *</label>
|
||||
<input
|
||||
className={`se-field-inp${saveNameError || saveDuplicateError ? ' se-field-inp--error' : ''}`}
|
||||
value={saveDraftName}
|
||||
onChange={e => {
|
||||
setSaveDraftName(e.target.value);
|
||||
clearSaveFieldErrors();
|
||||
}}
|
||||
placeholder="예: Golden_RSI_V1"
|
||||
/>
|
||||
{saveNameError && (
|
||||
<p className="se-field-error" role="alert">전략 이름을 입력하세요</p>
|
||||
)}
|
||||
{saveDuplicateError && (
|
||||
<p className="se-field-error" role="alert">같은 이름의 전략이 이미 있습니다</p>
|
||||
)}
|
||||
<label className="se-field-lbl">설명</label>
|
||||
<textarea
|
||||
className="se-field-ta"
|
||||
value={saveDraftDesc}
|
||||
onChange={e => setSaveDraftDesc(e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
<div className="se-modal-actions">
|
||||
<button type="button" className="se-btn se-btn--ghost" onClick={closeSaveDialog}>취소</button>
|
||||
<button
|
||||
type="button"
|
||||
className="se-btn se-btn--gold"
|
||||
disabled={saving}
|
||||
onClick={() => { void confirmSaveDialog(); }}
|
||||
>
|
||||
{saving ? '저장 중…' : (saveDialogMode === 'saveAs' ? '새로저장' : '저장')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DraggableModalFrame>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyEvaluationStrategyPanel;
|
||||
Reference in New Issue
Block a user