diff --git a/frontend/src/components/TradeNotificationListPage.tsx b/frontend/src/components/TradeNotificationListPage.tsx index 230212d..e693628 100644 --- a/frontend/src/components/TradeNotificationListPage.tsx +++ b/frontend/src/components/TradeNotificationListPage.tsx @@ -184,10 +184,25 @@ export const TradeNotificationListPage: React.FC = ({ useEffect(() => { let cancelled = false; - void prefetchNotificationStrategies().then(list => { - if (!cancelled) setStrategies(list); - }); - return () => { cancelled = true; }; + let retryTimer: ReturnType | null = null; + + const load = () => { + void prefetchNotificationStrategies().then(list => { + if (cancelled) return; + if (list.length > 0) { + setStrategies(list); + } else { + // 백엔드 미준비(Docker 기동 초기)로 빈 목록이 반환된 경우 5초 후 재시도 + retryTimer = setTimeout(load, 5000); + } + }); + }; + + load(); + return () => { + cancelled = true; + if (retryTimer) clearTimeout(retryTimer); + }; }, []); const tradePrice = useMemo( diff --git a/frontend/src/hooks/useTradeNotificationSignalSnapshot.ts b/frontend/src/hooks/useTradeNotificationSignalSnapshot.ts index ef77448..703445a 100644 --- a/frontend/src/hooks/useTradeNotificationSignalSnapshot.ts +++ b/frontend/src/hooks/useTradeNotificationSignalSnapshot.ts @@ -18,8 +18,11 @@ import { /** 전략을 찾았지만 백엔드가 아직 조건을 평가하지 않은 경우 — warm-up 대기 (10분) */ const MAX_EMPTY_RETRIES_WARM = 200; -/** 전략 자체를 찾지 못한 경우 — 빠르게 "데이터 없음" 표시 */ -const MAX_EMPTY_RETRIES_NO_STRATEGY = 3; +/** + * 전략 자체를 아직 찾지 못한 경우 — 서버 시작 초기(전략 목록 미로드)를 고려해 1분 대기 + * 3 → 20: Docker 기동 직후 백엔드가 전략 목록 API를 응답하기까지 시간이 걸릴 수 있음 + */ +const MAX_EMPTY_RETRIES_NO_STRATEGY = 20; function staticFallback( market: string, diff --git a/frontend/src/utils/resolveNotificationStrategy.ts b/frontend/src/utils/resolveNotificationStrategy.ts index 2c08c8f..90088b5 100644 --- a/frontend/src/utils/resolveNotificationStrategy.ts +++ b/frontend/src/utils/resolveNotificationStrategy.ts @@ -27,16 +27,20 @@ export function normalizeStrategyId(id: unknown): number | null { async function getStrategiesList(force = false): Promise { const now = Date.now(); - if (!force && strategiesCache && now - strategiesCacheAt < CACHE_MS) { + // 빈 배열은 캐시하지 않는다 — 백엔드 미준비로 빈 결과가 반환된 경우 재시도 허용 + if (!force && strategiesCache && strategiesCache.length > 0 && now - strategiesCacheAt < CACHE_MS) { return strategiesCache; } const list = await loadStrategies(); - strategiesCache = list ?? []; - strategiesCacheAt = now; - reconcileStrategyMissingWithValidIds( - strategiesCache.map(s => s.id).filter((id): id is number => id != null && id > 0), - ); - return strategiesCache; + const result = list ?? []; + if (result.length > 0) { + strategiesCache = result; + strategiesCacheAt = now; + reconcileStrategyMissingWithValidIds( + result.map(s => s.id).filter((id): id is number => id != null && id > 0), + ); + } + return result; } function findStrategyByName(list: StrategyDto[], name: string): StrategyDto | undefined { @@ -74,7 +78,8 @@ export async function resolveNotificationStrategy( market?: string | null, ): Promise { const normalizedId = normalizeStrategyId(strategyId); - const list = prefetched ?? await getStrategiesList(); + // prefetched가 빈 배열이면 직접 API로 재시도 (백엔드 미준비 시 prefetch가 빈 채로 캐시될 수 있음) + const list = (prefetched != null && prefetched.length > 0) ? prefetched : await getStrategiesList(); if (normalizedId != null) { const fromList = list.find(s => s.id === normalizedId);