전략평가 오류 수정

This commit is contained in:
Macbook
2026-06-16 00:30:25 +09:00
parent db3513a63b
commit 133e3d0413
8 changed files with 176 additions and 54 deletions
+57
View File
@@ -1919,12 +1919,69 @@ html.desktop-client .tmb-logo-version {
}
.ind-tab-editor-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding: 10px 12px;
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.ind-tab-editor-error {
margin-right: auto;
font-size: 12px;
color: #ff6b6b;
}
/* 인앱 확인 모달 (데스크톱 웹뷰에서 window.confirm 미동작 대체) */
.gc-confirm-overlay {
position: fixed;
inset: 0;
z-index: 2200;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(0, 0, 0, 0.55);
}
.gc-confirm-box {
width: min(360px, calc(100vw - 24px));
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
padding: 18px 18px 14px;
}
.gc-confirm-msg {
margin: 0 0 16px;
font-size: 13px;
line-height: 1.55;
color: var(--text);
white-space: pre-line;
}
.gc-confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.gc-confirm-btn {
padding: 7px 16px;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: 1px solid var(--border);
}
.gc-confirm-btn.cancel {
background: var(--bg3);
color: var(--text2);
}
.gc-confirm-btn.cancel:hover { color: var(--text); }
.gc-confirm-btn.ok {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.gc-confirm-btn.ok:hover { filter: brightness(1.08); }
.ind-tab-editor-btn {
padding: 7px 16px;
border-radius: 4px;
@@ -26,6 +26,7 @@ const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
const [name, setName] = useState(initialName);
const [selected, setSelected] = useState<Set<string>>(() => new Set(initialTypes));
const [search, setSearch] = useState('');
const [error, setError] = useState('');
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
@@ -64,13 +65,14 @@ const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
const handleSave = () => {
const trimmed = name.trim();
if (!trimmed) {
window.alert('탭 이름을 입력해 주세요.');
setError('탭 이름을 입력해 주세요.');
return;
}
if (selected.size === 0) {
window.alert('탭에 표시할 지표를 하나 이상 선택해 주세요.');
setError('탭에 표시할 지표를 하나 이상 선택해 주세요.');
return;
}
setError('');
const ordered = INDICATOR_REGISTRY
.filter(d => selected.has(d.type))
.map(d => d.type);
@@ -96,7 +98,7 @@ const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
<input
className="ind-tab-editor-input"
value={name}
onChange={e => setName(e.target.value)}
onChange={e => { setName(e.target.value); if (error) setError(''); }}
placeholder="예: 내 지표"
autoFocus
/>
@@ -156,6 +158,7 @@ const IndicatorCustomTabEditor: React.FC<IndicatorCustomTabEditorProps> = ({
</div>
<div className="ind-tab-editor-footer">
{error && <span className="ind-tab-editor-error">{error}</span>}
<button type="button" className="ind-tab-editor-btn cancel" onClick={onClose}>
</button>
@@ -2,6 +2,7 @@
* 실시간 차트 툴바와 동일한 보조지표 추가·제거 패널 (ind-panel)
*/
import React, { useRef, useEffect, useState, useCallback } from 'react';
import ReactDOM from 'react-dom';
import { useDraggablePanel } from '../hooks/useDraggablePanel';
import { INDICATOR_REGISTRY, type IndicatorDef } from '../utils/indicatorRegistry';
import {
@@ -12,7 +13,12 @@ import {
parseCustomTabKey,
type IndicatorTabKey,
} from '../utils/indicatorMainTab';
import { confirmAndApplyIndicatorTab } from '../utils/indicatorTabApply';
import {
applyIndicatorTab,
resolveIndicatorTabApplyTypes,
resolveIndicatorTabApplyLabel,
type IndicatorTabApplySource,
} from '../utils/indicatorTabApply';
import {
getOrderedMainIndicatorTypes,
saveMainTabOrder,
@@ -85,6 +91,7 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
const [dragOverType, setDragOverType] = useState<string | null>(null);
const [editorOpen, setEditorOpen] = useState(false);
const [editorMode, setEditorMode] = useState<'create' | 'edit'>('create');
const [confirmState, setConfirmState] = useState<{ message: string; onConfirm: () => void } | null>(null);
const searchRef = useRef<HTMLInputElement>(null);
const searchFocusedRef = useRef(false);
@@ -117,10 +124,15 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
const handleDeleteTab = () => {
if (!selectedCustomTab) return;
if (!window.confirm(`"${selectedCustomTab.name}" 탭을 삭제할까요?`)) return;
deleteCustomTab(selectedCustomTab.id);
refreshCustomTabs();
setCategory('All');
const tab = selectedCustomTab;
setConfirmState({
message: `"${tab.name}" 탭을 삭제할까요?`,
onConfirm: () => {
deleteCustomTab(tab.id);
refreshCustomTabs();
setCategory('All');
},
});
};
const handleEditorSave = (name: string, indicatorTypes: string[]) => {
@@ -205,14 +217,24 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
const addAllDisabled = category === 'All' || !addAllContext || addAllContext.types.length === 0;
const requestApplyTab = (source: IndicatorTabApplySource) => {
const validTypes = resolveIndicatorTabApplyTypes(source);
if (validTypes.length === 0) return;
const label = resolveIndicatorTabApplyLabel(source);
setConfirmState({
message: `기존 보조지표를 모두 제거하고 ${label} 탭의 지표 ${validTypes.length}개로 교체하시겠습니까?`,
onConfirm: () => applyIndicatorTab(source, onAddMany, onAdd),
});
};
const handleAddAllInTab = () => {
if (!addAllContext || addAllContext.types.length === 0) return;
if (category === 'Main') {
confirmAndApplyIndicatorTab({ kind: 'main' }, onAddMany, onAdd);
requestApplyTab({ kind: 'main' });
return;
}
if (isCustomTabKey(category) && selectedCustomTab) {
confirmAndApplyIndicatorTab({ kind: 'custom', tab: selectedCustomTab }, onAddMany, onAdd);
requestApplyTab({ kind: 'custom', tab: selectedCustomTab });
}
};
@@ -229,6 +251,10 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
if (confirmState) {
setConfirmState(null);
return;
}
if (editorOpen) {
setEditorOpen(false);
return;
@@ -237,7 +263,7 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose, editorOpen]);
}, [onClose, editorOpen, confirmState]);
const isActive = (type: string) => activeIndicators.some(a => a.type === type);
@@ -535,6 +561,38 @@ const IndicatorDropdownPanel: React.FC<IndicatorDropdownPanelProps> = ({
/>
)}
</div>
{confirmState && ReactDOM.createPortal(
<div
className="gc-confirm-overlay"
onMouseDown={e => { if (e.target === e.currentTarget) setConfirmState(null); }}
>
<div className="gc-confirm-box" onMouseDown={e => e.stopPropagation()}>
<p className="gc-confirm-msg">{confirmState.message}</p>
<div className="gc-confirm-actions">
<button
type="button"
className="gc-confirm-btn cancel"
onClick={() => setConfirmState(null)}
>
</button>
<button
type="button"
className="gc-confirm-btn ok"
onClick={() => {
const fn = confirmState.onConfirm;
setConfirmState(null);
fn();
}}
>
</button>
</div>
</div>
</div>,
document.body,
)}
</div>
);
};
@@ -139,14 +139,10 @@ const StrategyEvaluationChart: React.FC<Props> = ({
return { candle: flex.candle, aux: flex.aux };
}, [auxPaneCount]);
/** 저장된 오버라이드가 있을 때만 백테스트에 전달 — 미설정 시 live-conditions 와 같이 서버 DB 사용 */
/** 전략 DSL 지표 파라미터 — live-conditions/evaluate·차트와 동일 소스 */
const indicatorParams = useMemo(
() => (
strategy && appliedIndicatorParams
? buildEvalParamsFromStrategy(strategy, getParams)
: undefined
),
[strategy, getParams, appliedIndicatorParams],
() => (strategy ? buildEvalParamsFromStrategy(strategy, getParams) : undefined),
[strategy, getParams],
);
const resolveInitialDisplayCount = useCallback(