From fa5736727206eff6fa63239553c81436c28544bc Mon Sep 17 00:00:00 2001 From: user040131 Date: Fri, 10 Apr 2026 00:11:41 +0900 Subject: [PATCH] fix: restore replay and evaluation page rendering --- apps/web/app/(pages)/evaluation/page.tsx | 171 ++++++----- apps/web/app/(pages)/replay/page.tsx | 372 ++++++++++++----------- 2 files changed, 294 insertions(+), 249 deletions(-) diff --git a/apps/web/app/(pages)/evaluation/page.tsx b/apps/web/app/(pages)/evaluation/page.tsx index da77630..69dde9c 100644 --- a/apps/web/app/(pages)/evaluation/page.tsx +++ b/apps/web/app/(pages)/evaluation/page.tsx @@ -4,88 +4,109 @@ import { Badge } from "@/components/shared/badge"; import { formatPct, formatScore } from "@/lib/format"; export default async function EvaluationPage() { - const data = await api.evaluation(); + try { + const data = await api.evaluation(); - return ( -
- - -
- -

테마 적중률

-

{formatPct(data.rollingMetrics.themeHitRate * 100)}

-
- -

주도주 적중률

-

{formatPct(data.rollingMetrics.leaderHitRate * 100)}

-
- -

오탐 비율

-

{formatPct(data.rollingMetrics.falsePositiveRate * 100)}

-
- -

갭페이드 실패

-

{formatPct(data.rollingMetrics.gapFadeMissRate * 100)}

-
-
-
- -
+ return ( +
- -
- {data.promptLeaderboard.map((item) => ( -
-
-
-

{item.version}

-

- {item.description} -

-
- - {item.promotable ? "승격 가능" : "보류"} - -
-
-

종합 점수 {formatScore(item.score)}

-

테마 적중률 {formatPct(item.themeHitRate * 100)}

-

주도주 적중률 {formatPct(item.leaderHitRate * 100)}

-
-
- ))} + +
+ +

테마 적중률

+

{formatPct(data.rollingMetrics.themeHitRate * 100)}

+
+ +

주도주 적중률

+

{formatPct(data.rollingMetrics.leaderHitRate * 100)}

+
+ +

오탐 비율

+

{formatPct(data.rollingMetrics.falsePositiveRate * 100)}

+
+ +

갭 페이드 실패

+

{formatPct(data.rollingMetrics.gapFadeMissRate * 100)}

+
- - -
- {data.modelRoleLeaderboard.map((item) => ( -
-
-
-

{item.role}

-

{item.model}

+ +
+ + +
+ {data.promptLeaderboard.map((item) => ( +
+
+
+

{item.version}

+

+ {item.description} +

+
+ + {item.promotable ? "승격 가능" : "보류"} +
- {formatScore(item.score)} +
+

종합 점수 {formatScore(item.score)}

+

테마 적중률 {formatPct(item.themeHitRate * 100)}

+

주도주 적중률 {formatPct(item.leaderHitRate * 100)}

+
+
+ ))} +
+
+ + + +
+ {data.modelRoleLeaderboard.map((item) => ( +
+
+
+

{item.role}

+

{item.model}

+
+ {formatScore(item.score)} +
+

+ 평균 지연 {item.avgLatencyMs}ms | 평균 비용 ${item.avgCostUsd.toFixed(4)} +

-

- 평균 지연 {item.avgLatencyMs}ms | 평균 비용 ${item.avgCostUsd.toFixed(4)} -

-
- ))} + ))} +
+ +
+
+ ); + } catch (error) { + const message = error instanceof Error ? error.message : "알 수 없는 오류"; + + return ( +
+ + +
+

오류 메시지

+

{message}

-
- ); + ); + } } diff --git a/apps/web/app/(pages)/replay/page.tsx b/apps/web/app/(pages)/replay/page.tsx index 8037fee..bdd2d47 100644 --- a/apps/web/app/(pages)/replay/page.tsx +++ b/apps/web/app/(pages)/replay/page.tsx @@ -13,198 +13,222 @@ function outcomeVariant(label: string): "ok" | "warn" | "danger" { return "danger"; } +function tierLabel(tier: string) { + return tier === "leader" ? "주도주" : "2등주"; +} + export default async function ReplayPage() { - const data = await api.weeklyReplay(); + try { + const data = await api.weeklyReplay(); - return ( -
- - -
- -

Window

-

{data.windowLabel}

-

- 현재 추론 규칙과 동일한 로직을 과거 구간에 그대로 적용합니다. -

-
- -

Hit Days

-

- {data.aggregate.positiveHitDays} / {data.aggregate.daysAnalyzed} -

-

- 상위 테마 주도주 평균 수익률이 양호 이상이었던 날짜 수입니다. -

-
- -

Avg Leader Return

-

- {formatPct(data.aggregate.avgLeaderCloseReturnPct)} -

-

- 상위 테마 기준 주도주 평균 종가 수익률입니다. -

-
-
-
+ return ( +
+ + +
+ +

Window

+

{data.windowLabel}

+

+ 현재 운영 중인 추론 규칙을 같은 방식으로 과거 구간에 재적용한 결과입니다. +

+
+ +

Hit Days

+

+ {data.aggregate.positiveHitDays} / {data.aggregate.daysAnalyzed} +

+

+ 상위 테마 주도주의 평균 종가 수익률이 양호 이상이었던 날짜 수입니다. +

+
+ +

Avg Leader Return

+

+ {formatPct(data.aggregate.avgLeaderCloseReturnPct)} +

+

+ 전체 리플레이 기준 주도주 평균 종가 수익률입니다. +

+
+
+
-
- {data.days.map((day) => ( - -
-
-

Replay Day

-

- {day.marketDateLabel} 한국장 -

-

- 추론 기준 시각 {day.asOfLabel} / evidence hash {day.evidencePackHash.slice(0, 12)} -

+
+ {data.days.map((day) => ( + +
+
+

Replay Day

+

+ {day.marketDateLabel} 한국장 +

+

+ 추론 기준 시각 {day.asOfLabel} / evidence hash {day.evidencePackHash.slice(0, 12)} +

+
+
+

KOSPI {formatPct(day.marketContext.kospiCloseReturnPct)}

+

KOSDAQ {formatPct(day.marketContext.kosdaqCloseReturnPct)}

+

{day.marketContext.summary}

+
-
-

KOSPI {formatPct(day.marketContext.kospiCloseReturnPct)}

-

KOSDAQ {formatPct(day.marketContext.kosdaqCloseReturnPct)}

-

{day.marketContext.summary}

-
-
-
-

{day.summary}

-
+
+

{day.summary}

+
-
-
- {day.predictedThemes.length ? ( - day.predictedThemes.map((theme) => ( -
-
-
-
- {theme.name} - - {theme.actualOutcome.outcomeLabel} - - = 0.7 ? "ok" : "warn"}> - 확신 {formatScore(theme.confidence)} - +
+
+ {day.predictedThemes.length ? ( + day.predictedThemes.map((theme) => ( +
+
+
+
+ {theme.name} + + {theme.actualOutcome.outcomeLabel} + + = 0.7 ? "ok" : "warn"}> + 확신 {formatScore(theme.confidence)} + +
+

+ {theme.rationale} +

+
+
+

주도주 평균 {formatPct(theme.actualOutcome.avgLeaderCloseReturnPct)}

+

+ 최고 수익 {theme.actualOutcome.bestStockLabel ?? "-"} + {theme.actualOutcome.bestCloseReturnPct !== null + ? ` / ${formatPct(theme.actualOutcome.bestCloseReturnPct)}` + : ""} +

-

- {theme.rationale} -

-
-
-

주도주 평균 {formatPct(theme.actualOutcome.avgLeaderCloseReturnPct)}

-

- 최고 수익 {theme.actualOutcome.bestStockLabel ?? "-"} - {theme.actualOutcome.bestCloseReturnPct !== null - ? ` / ${formatPct(theme.actualOutcome.bestCloseReturnPct)}` - : ""} -

-
-
-
-

예상 주도주 / 2등주

-

- 주도주: {theme.leaders.map((item) => `${item.ticker} ${item.name}`).join(", ") || "-"} -

-

- 2등주: {theme.secondTier.map((item) => `${item.ticker} ${item.name}`).join(", ") || "-"} -

-

- 무효화 조건: {theme.invalidationCondition} -

-
-
-

실제 종목 반응

-
- {theme.actualOutcome.stockResults.length ? ( - theme.actualOutcome.stockResults.map((stock) => ( -
-
-

- {stock.ticker} {stock.name} +

+
+

예상 주도주 / 2등주

+

+ 주도주: {theme.leaders.map((item) => `${item.ticker} ${item.name}`).join(", ") || "-"} +

+

+ 2등주: {theme.secondTier.map((item) => `${item.ticker} ${item.name}`).join(", ") || "-"} +

+

+ 무효화 조건: {theme.invalidationCondition} +

+
+
+

실제 종목 반응

+
+ {theme.actualOutcome.stockResults.length ? ( + theme.actualOutcome.stockResults.map((stock) => ( +
+
+

+ {stock.ticker} {stock.name} +

+ = 0 ? "ok" : "danger"}> + {tierLabel(stock.tier)} + +
+

+ 시가 갭 {formatPct(stock.openGapPct)} / 종가 수익률 {formatPct(stock.closeReturnPct)} / 장중 변화{" "} + {formatPct(stock.intradayMovePct)}

- = 0 ? "ok" : "danger"}> - {stock.tier === "leader" ? "주도주" : "2등주"} -
-

- 시가 갭 {formatPct(stock.openGapPct)} / 종가 수익률 {formatPct(stock.closeReturnPct)} / - 장중 변화 {formatPct(stock.intradayMovePct)} -

-
- )) - ) : ( -

- 실제 종목 데이터를 불러오지 못했습니다. -

- )} + )) + ) : ( +

+ 실제 종목 데이터를 불러오지 못했습니다. +

+ )} +
+ )) + ) : ( +
+ 해당 날짜에는 현재 규칙 기준으로 상위 한국 테마 예측이 생성되지 않았습니다.
- )) - ) : ( -
- 해당 날짜에는 현재 규칙 기준으로 상위 한국 테마 예측이 생성되지 않았습니다. -
- )} -
+ )} +
-
- -

근거 이벤트

-
- {day.evidenceItems.map((item) => ( -
-
-

- {item.categoryLabel} +

+ +

근거 이벤트

+
+ {day.evidenceItems.map((item) => ( +
+
+

+ {item.categoryLabel} +

+ = 0.35 ? "ok" : "warn"}> + 시장 확인 {formatScore(item.marketConfirmationScore)} + +
+

{item.title}

+

+ {item.summary}

- = 0.35 ? "ok" : "warn"}> - 시장 확인 {formatScore(item.marketConfirmationScore)} - +

+ {item.sourceName} / {item.publishedAtLabel} +

+ + 원문 보기 +
-

{item.title}

-

- {item.summary} -

-

- {item.sourceName} / {item.publishedAtLabel} -

- - 원문 보기 - -
- ))} -
- + ))} +
+ +
-
- - ))} + + ))} +
+
+ ); + } catch (error) { + const message = error instanceof Error ? error.message : "알 수 없는 오류"; + + return ( +
+ + +
+

오류 메시지

+

{message}

+
+
-
- ); + ); + } }