전략평가 오류 수정
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user