diff --git a/frontend/src/components/BacktestHistoryPage.tsx b/frontend/src/components/BacktestHistoryPage.tsx index 5f13bf6..a91444f 100644 --- a/frontend/src/components/BacktestHistoryPage.tsx +++ b/frontend/src/components/BacktestHistoryPage.tsx @@ -50,6 +50,7 @@ import '../styles/strategyEditorTheme.css'; import { getKoreanName } from '../utils/marketNameCache'; import { repairUtf8Mojibake } from '../utils/textEncoding'; import { enrichStrategyNameMap, strategyNamesFromList } from '../utils/strategyNameResolver'; +import { markStrategyIdsMissingUnlessValid } from '../utils/strategyMissingCache'; const LEFT_KEY = 'btd-left-width'; const RIGHT_KEY = 'btd-right-width'; @@ -148,6 +149,10 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) { loadStrategies(), ]); const baseNames = strategyNamesFromList(strategies); + markStrategyIdsMissingUnlessValid( + strategies.map(s => s.id).filter((id): id is number => id != null && id > 0), + trades.map(t => t.strategyId), + ); const strategyNames = await enrichStrategyNameMap( baseNames, trades.map(t => t.strategyId), @@ -166,6 +171,13 @@ export function BacktestHistoryPage({ theme = 'dark' }: Props) { loadStrategies(), ]); const baseNames = strategyNamesFromList(strategies); + markStrategyIdsMissingUnlessValid( + strategies.map(s => s.id).filter((id): id is number => id != null && id > 0), + [ + ...trades.map(t => t.strategyId), + ...list.map(r => r.strategyId), + ], + ); const strategyNames = await enrichStrategyNameMap( baseNames, [ diff --git a/frontend/src/components/backtest/BacktestAnalysisChart.tsx b/frontend/src/components/backtest/BacktestAnalysisChart.tsx index f1dd062..ff87fe0 100644 --- a/frontend/src/components/backtest/BacktestAnalysisChart.tsx +++ b/frontend/src/components/backtest/BacktestAnalysisChart.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState, type MutableR import TradingChart from '../TradingChart'; import type { BacktestSignal, StrategyDto } from '../../utils/backendApi'; import { loadStrategyForNotification } from '../../utils/backendApi'; +import { isStrategyMissingOnServer } from '../../utils/strategyMissingCache'; import type { ChartType, IndicatorConfig, OHLCVBar, Theme, Timeframe } from '../../types'; import { loadAnalysisCandles } from '../../utils/analysisChartData'; import type { ChartManager } from '../../utils/ChartManager'; @@ -149,7 +150,7 @@ const BacktestAnalysisChart: React.FC = ({ }, [strategy, timeframeRaw]); useEffect(() => { - if (!strategyId) { + if (!strategyId || isStrategyMissingOnServer(strategyId)) { setStrategy(undefined); setStrategyLoading(false); return; diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index 66f80c7..6c209cf 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -1113,9 +1113,9 @@ export async function loadStrategy(id: number): Promise { return request(`/strategies/${id}`); } -/** 알림·지표 표시 — missing 캐시 무시, 성공 시 캐시 해제 */ +/** 알림·지표 표시 — missing 캐시 적용, 성공 시 캐시 해제 */ export async function loadStrategyForNotification(id: number): Promise { - if (!hasRegisteredUser() || id <= 0) return null; + if (!hasRegisteredUser() || id <= 0 || isStrategyMissingOnServer(id)) return null; const dto = await request(`/strategies/${id}`); if (dto) unmarkStrategyMissingOnServer(id); return dto; diff --git a/frontend/src/utils/strategyMissingCache.ts b/frontend/src/utils/strategyMissingCache.ts index 6329f8b..f409c92 100644 --- a/frontend/src/utils/strategyMissingCache.ts +++ b/frontend/src/utils/strategyMissingCache.ts @@ -65,4 +65,19 @@ export function markStrategyMissingFromPath(pathOnly: string): void { if (m) markStrategyMissingOnServer(Number(m[1])); } +/** 목록에 없는 참조 ID를 missing 으로 기록 — 삭제된 전략 404 반복 방지 */ +export function markStrategyIdsMissingUnlessValid( + validIds: Iterable, + referencedIds: Iterable, +): void { + const valid = new Set( + [...validIds].map(id => Math.trunc(id)).filter(id => id > 0), + ); + for (const raw of referencedIds) { + if (raw == null) continue; + const id = Math.trunc(raw); + if (id > 0 && !valid.has(id)) markStrategyMissingOnServer(id); + } +} + hydrateStrategyMissingCache(); diff --git a/frontend/src/utils/strategyNameResolver.ts b/frontend/src/utils/strategyNameResolver.ts index d791b93..4f47354 100644 --- a/frontend/src/utils/strategyNameResolver.ts +++ b/frontend/src/utils/strategyNameResolver.ts @@ -2,6 +2,7 @@ * 전략 ID → 이름 해석 (목록에 없거나 삭제된 전략 포함) */ import { loadStrategyForNotification, type StrategyDto } from './backendApi'; +import { isStrategyMissingOnServer } from './strategyMissingCache'; /** 전략 목록 + 누락 ID별 단건 조회로 이름 맵 보강 */ export async function enrichStrategyNameMap( @@ -12,7 +13,7 @@ export async function enrichStrategyNameMap( const missing = [...new Set( [...ids] .map(id => (id != null && id > 0 ? Math.trunc(id) : null)) - .filter((id): id is number => id != null && !out[id]), + .filter((id): id is number => id != null && !out[id] && !isStrategyMissingOnServer(id)), )]; if (!missing.length) return out;