diff --git a/apps/web/app/(pages)/replay/page.tsx b/apps/web/app/(pages)/replay/page.tsx index bdd2d47..53bcf50 100644 --- a/apps/web/app/(pages)/replay/page.tsx +++ b/apps/web/app/(pages)/replay/page.tsx @@ -1,234 +1,17 @@ -import { api } from "@/lib/api"; -import { formatPct, formatScore } from "@/lib/format"; -import { Badge } from "@/components/shared/badge"; import { Card, CardHeader } from "@/components/shared/card"; - -function outcomeVariant(label: string): "ok" | "warn" | "danger" { - if (label === "강한 적중" || label === "양호") { - return "ok"; - } - if (label === "혼조") { - return "warn"; - } - return "danger"; -} - -function tierLabel(tier: string) { - return tier === "leader" ? "주도주" : "2등주"; -} - -export default async function ReplayPage() { - 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)} -

-

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

-
-
-
- -
- {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}

-
-
- -
-

{day.summary}

-
- -
-
- {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)}` - : ""} -

-
-
- -
-
-

예상 주도주 / 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)} -

-
- )) - ) : ( -

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

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

근거 이벤트

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

- {item.categoryLabel} -

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

{item.title}

-

- {item.summary} -

-

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

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

오류 메시지

-

{message}

-
-
-
- ); - } +import { WeeklyReplayClient } from "@/components/replay/weekly-replay-client"; + +export default function ReplayPage() { + return ( +
+ + + + +
+ ); } diff --git a/apps/web/components/replay/weekly-replay-client.tsx b/apps/web/components/replay/weekly-replay-client.tsx new file mode 100644 index 0000000..f08fd37 --- /dev/null +++ b/apps/web/components/replay/weekly-replay-client.tsx @@ -0,0 +1,287 @@ +"use client"; + +import { startTransition, useEffect, useState } from "react"; +import { + type WeeklyReplayPayload, + WeeklyReplayPayloadSchema +} from "@finance-helper/contracts"; +import { formatPct, formatScore } from "@/lib/format"; +import { Badge } from "@/components/shared/badge"; +import { Card } from "@/components/shared/card"; + +function outcomeVariant(label: string): "ok" | "warn" | "danger" { + if (label === "강한 반응" || label === "양호") { + return "ok"; + } + if (label === "보통") { + return "warn"; + } + return "danger"; +} + +function tierLabel(tier: string) { + return tier === "leader" ? "주도주" : "2등주"; +} + +export function WeeklyReplayClient() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), 30000); + + async function load() { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000/api/v1"}/replay/weekly`, + { + cache: "no-store", + headers: { "Content-Type": "application/json" }, + signal: controller.signal + } + ); + + if (!response.ok) { + throw new Error(`API request failed: ${response.status} ${response.statusText}`); + } + + const payload = await response.json(); + const parsed = WeeklyReplayPayloadSchema.parse(payload); + startTransition(() => { + setData(parsed as WeeklyReplayPayload); + setError(null); + }); + } catch (caughtError) { + const message = + caughtError instanceof Error + ? caughtError.name === "AbortError" + ? "주간 리플레이 응답이 지연되고 있습니다. 잠시 후 다시 시도해 주세요." + : caughtError.message + : "알 수 없는 오류"; + startTransition(() => { + setError(message); + setData(null); + }); + } finally { + window.clearTimeout(timeoutId); + startTransition(() => setLoading(false)); + } + } + + void load(); + + return () => { + window.clearTimeout(timeoutId); + controller.abort(); + }; + }, []); + + if (loading) { + return ( + +

Loading

+

+ 리플레이 데이터를 불러오는 중입니다. 최근 7거래일 미국 이벤트와 한국장 결과를 정리하고 있습니다. +

+
+ ); + } + + if (error || !data) { + return ( + +

리플레이 데이터를 불러오지 못했습니다

+

+ {error ?? "알 수 없는 오류"} +

+
+ ); + } + + 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)} +

+
+
+

KOSPI {formatPct(day.marketContext.kospiCloseReturnPct)}

+

KOSDAQ {formatPct(day.marketContext.kosdaqCloseReturnPct)}

+

{day.marketContext.summary}

+
+
+ +
+

{day.summary}

+
+ +
+
+ {day.predictedThemes.length > 0 ? ( + 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)}` + : ""} +

+
+
+ +
+
+

예상 주도주 / 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 > 0 ? ( + theme.actualOutcome.stockResults.map((stock) => ( +
+
+

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

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

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

+
+ )) + ) : ( +

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

+ )} +
+
+
+
+ )) + ) : ( +
+ 해당 날짜에는 현재 규칙 기준으로 의미 있는 한국 테마 후보가 생성되지 않았습니다. +
+ )} +
+ +
+ +

근거 이벤트

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

+ {item.categoryLabel} +

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

{item.title}

+

+ {item.summary} +

+

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

+ + 원문 보기 + +
+ ))} +
+
+
+
+
+ ))} +
+ + ); +}