전략평가 화면 수정

This commit is contained in:
Macbook
2026-06-12 19:12:47 +09:00
parent a3f0584e72
commit d54dc6e2ff
4 changed files with 70 additions and 38 deletions
@@ -382,6 +382,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) {
appliedParams={appliedIndicatorParams} appliedParams={appliedIndicatorParams}
onSave={handleSaveIndicatorParams} onSave={handleSaveIndicatorParams}
onReset={handleResetIndicatorParams} onReset={handleResetIndicatorParams}
applying={evalLoading || chartLoading}
/> />
)} )}
</div> </div>
@@ -268,11 +268,6 @@ const StrategyEvaluationChart: React.FC<Props> = ({
mgr.applyVisibleTimeRange(bar.time - barSec * 24, bar.time + barSec * 8); mgr.applyVisibleTimeRange(bar.time - barSec * 24, bar.time + barSec * 8);
}, [chartMgr, bars]); }, [chartMgr, bars]);
useEffect(() => {
if (bars.length === 0) return;
scrollToBar(Math.min(Math.max(0, selectedBarIndex), bars.length - 1));
}, [selectedBarIndex, bars, scrollToBar]);
const onManagerReady = useCallback((mgr: ChartManager) => { const onManagerReady = useCallback((mgr: ChartManager) => {
managerRef.current = mgr; managerRef.current = mgr;
setChartMgr(mgr); setChartMgr(mgr);
@@ -299,8 +294,10 @@ const StrategyEvaluationChart: React.FC<Props> = ({
const goLatest = useCallback(() => { const goLatest = useCallback(() => {
if (bars.length === 0) return; if (bars.length === 0) return;
onSelectedBarIndexChange(bars.length - 1); const lastIdx = bars.length - 1;
}, [bars.length, onSelectedBarIndexChange]); onSelectedBarIndexChange(lastIdx);
scrollToBar(lastIdx);
}, [bars.length, onSelectedBarIndexChange, scrollToBar]);
useEffect(() => { useEffect(() => {
const mgr = chartMgr; const mgr = chartMgr;
@@ -20,6 +20,7 @@ interface Props {
appliedParams: EvalIndicatorParams | null; appliedParams: EvalIndicatorParams | null;
onSave: (params: EvalIndicatorParams) => void; onSave: (params: EvalIndicatorParams) => void;
onReset: () => void; onReset: () => void;
applying?: boolean;
} }
function getEditableParamKeys( function getEditableParamKeys(
@@ -47,6 +48,7 @@ const StrategyEvaluationSettingsTab: React.FC<Props> = ({
appliedParams, appliedParams,
onSave, onSave,
onReset, onReset,
applying = false,
}) => { }) => {
const [draft, setDraft] = useState<EvalIndicatorParams>({}); const [draft, setDraft] = useState<EvalIndicatorParams>({});
const [expanded, setExpanded] = useState<string | null>(null); const [expanded, setExpanded] = useState<string | null>(null);
@@ -56,23 +58,31 @@ const StrategyEvaluationSettingsTab: React.FC<Props> = ({
[strategy], [strategy],
); );
const resolveBaseline = useCallback((): EvalIndicatorParams => {
if (!strategy) return {};
return appliedParams ?? buildEvalParamsFromStrategy(strategy, getParams);
}, [strategy, appliedParams, getParams]);
useEffect(() => { useEffect(() => {
if (!strategy) { if (!strategy) {
setDraft({}); setDraft({});
setExpanded(null); setExpanded(null);
return; return;
} }
const baseline = appliedParams ?? buildEvalParamsFromStrategy(strategy, getParams); setDraft(JSON.parse(JSON.stringify(resolveBaseline())) as EvalIndicatorParams);
setDraft(JSON.parse(JSON.stringify(baseline)) as EvalIndicatorParams);
setExpanded(indicatorTypes[0] ?? null); setExpanded(indicatorTypes[0] ?? null);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [strategy?.id, appliedParams, indicatorTypes.join(',')]); }, [strategy?.id, appliedParams, indicatorTypes.join(',')]);
const dirty = useMemo( const dirty = useMemo(
() => hasEvalParamsDiff(draft, appliedParams), () => hasEvalParamsDiff(draft, resolveBaseline()),
[draft, appliedParams], [draft, resolveBaseline],
); );
const handleApply = useCallback(() => {
onSave(JSON.parse(JSON.stringify(draft)) as EvalIndicatorParams);
}, [draft, onSave]);
const patchParam = useCallback((type: string, key: string, raw: string | boolean) => { const patchParam = useCallback((type: string, key: string, raw: string | boolean) => {
setDraft(prev => { setDraft(prev => {
const current = prev[type] ?? {}; const current = prev[type] ?? {};
@@ -115,8 +125,19 @@ const StrategyEvaluationSettingsTab: React.FC<Props> = ({
return ( return (
<div className="seval-settings-tab"> <div className="seval-settings-tab">
<div className="seval-settings-head"> <div className="seval-settings-head">
<p className="seval-settings-strategy">{strategyName}</p> <div className="seval-settings-head-row">
<p className="seval-settings-hint"> · </p> <p className="seval-settings-strategy">{strategyName}</p>
<button
type="button"
className="seval-settings-apply"
disabled={!dirty || applying}
onClick={handleApply}
title="변경된 지표 값을 차트·일치율에 반영"
>
{applying ? '적용 중…' : '적용'}
</button>
</div>
<p className="seval-settings-hint"> · </p>
</div> </div>
<div className="seval-settings-list"> <div className="seval-settings-list">
@@ -163,18 +184,10 @@ const StrategyEvaluationSettingsTab: React.FC<Props> = ({
</div> </div>
<div className="seval-settings-actions"> <div className="seval-settings-actions">
<button
type="button"
className="seval-settings-save"
disabled={!dirty}
onClick={() => onSave(JSON.parse(JSON.stringify(draft)) as EvalIndicatorParams)}
>
·
</button>
<button <button
type="button" type="button"
className="seval-settings-reset" className="seval-settings-reset"
disabled={!appliedParams && !dirty} disabled={(!appliedParams && !dirty) || applying}
onClick={onReset} onClick={onReset}
> >
+37 -16
View File
@@ -1004,11 +1004,43 @@
padding: 4px 2px 8px; padding: 4px 2px 8px;
} }
.seval-settings-head-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.seval-settings-strategy { .seval-settings-strategy {
margin: 0; margin: 0;
flex: 1;
min-width: 0;
font-size: 0.78rem; font-size: 0.78rem;
font-weight: 700; font-weight: 700;
color: var(--se-text); color: var(--se-text);
line-height: 1.35;
}
.seval-settings-apply {
flex-shrink: 0;
padding: 6px 14px;
border-radius: 8px;
border: 1px solid color-mix(in srgb, var(--btd-gold, var(--se-gold)) 45%, var(--se-border));
background: color-mix(in srgb, var(--btd-gold, var(--se-gold)) 16%, var(--se-input-bg));
color: var(--btd-gold, var(--se-gold));
font-size: 0.72rem;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
}
.seval-settings-apply:hover:not(:disabled) {
background: color-mix(in srgb, var(--btd-gold, var(--se-gold)) 24%, var(--se-input-bg));
}
.seval-settings-apply:disabled {
opacity: 0.45;
cursor: not-allowed;
} }
.seval-settings-hint { .seval-settings-hint {
@@ -1089,7 +1121,6 @@
border-top: 1px solid var(--btd-divider, var(--se-border)); border-top: 1px solid var(--btd-divider, var(--se-border));
} }
.seval-settings-save,
.seval-settings-reset { .seval-settings-reset {
flex: 1; flex: 1;
padding: 9px 10px; padding: 9px 10px;
@@ -1097,26 +1128,16 @@
font-size: 0.74rem; font-size: 0.74rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
}
.seval-settings-save {
border: 1px solid color-mix(in srgb, var(--btd-gold, var(--se-gold)) 45%, var(--se-border));
background: color-mix(in srgb, var(--btd-gold, var(--se-gold)) 12%, var(--se-input-bg));
color: var(--btd-gold, var(--se-gold));
}
.seval-settings-save:disabled,
.seval-settings-reset:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.seval-settings-reset {
border: 1px solid var(--btd-divider, var(--se-border)); border: 1px solid var(--btd-divider, var(--se-border));
background: var(--btd-surface, var(--se-input-bg)); background: var(--btd-surface, var(--se-input-bg));
color: var(--se-text-muted); color: var(--se-text-muted);
} }
.seval-settings-reset:disabled {
opacity: 0.45;
cursor: not-allowed;
}
@media (max-width: 960px) { @media (max-width: 960px) {
.seval-chart-hint { .seval-chart-hint {
display: none; display: none;