From 2893029d8d15624b73b06d92dbd77ed5701735ff Mon Sep 17 00:00:00 2001 From: hyungi Date: Fri, 15 May 2026 05:04:22 +0000 Subject: [PATCH] feat(digest): Phase 4.5 SvelteKit UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /digest 라우트 신규 — Phase 4 (7일 rolling country×topic batch digest) backend 운영 데이터 사용자 진입점. 최신 1건 (GET /api/digest/latest) 표시 + country pill 탭 + topic 카드 (rank/label/summary/article_count/importance, fallback Badge 조건부). - frontend/src/routes/digest/+page.svelte 신규 (123 LOC) — Svelte 5 runes, Tabs snippet 패턴, 404 EmptyState 흡수, country reload 보호. - frontend/src/routes/+layout.svelte nav 1줄 추가 (아침 브리핑 뒤). 후속 별 PR: date picker, article click 라우팅, 국기+한국어 dictionary, Phase 4.6 feedback loop. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/routes/+layout.svelte | 1 + frontend/src/routes/digest/+page.svelte | 123 ++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 frontend/src/routes/digest/+page.svelte diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 169b508..6393d8c 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -99,6 +99,7 @@ +
+ import { onMount } from 'svelte'; + import { api, type ApiError } from '$lib/api'; + import Card from '$lib/components/ui/Card.svelte'; + import Skeleton from '$lib/components/ui/Skeleton.svelte'; + import EmptyState from '$lib/components/ui/EmptyState.svelte'; + import Tabs from '$lib/components/ui/Tabs.svelte'; + import Badge from '$lib/components/ui/Badge.svelte'; + import Button from '$lib/components/ui/Button.svelte'; + + type Topic = { + topic_rank: number; + topic_label: string; + summary: string; + article_ids: number[]; + article_count: number; + importance_score: number; + llm_fallback_used: boolean; + }; + type Country = { country: string; topics: Topic[] }; + type Digest = { + digest_date: string; + total_articles: number; + total_countries: number; + total_topics: number; + llm_calls: number; + llm_failures: number; + status: string; + countries: Country[]; + }; + + let digest = $state(null); + let loading = $state(true); + let error = $state(null); + let country = $state(''); + + async function load() { + loading = true; + error = null; + try { + digest = await api('/digest/latest'); + const countries = digest?.countries ?? []; + if (!countries.some((c) => c.country === country)) { + country = countries[0]?.country ?? ''; + } + } catch (e) { + const err = e as ApiError; + if (err && err.status === 404) { + digest = null; + error = null; + return; + } + error = err?.detail ?? (e as Error)?.message ?? '알 수 없는 오류'; + } finally { + loading = false; + } + } + + onMount(load); + + let tabs = $derived( + (digest?.countries ?? []).map((c) => ({ id: c.country, label: c.country })), + ); + let topics = $derived( + digest?.countries?.find((c) => c.country === country)?.topics ?? [], + ); + + +
+
+

뉴스 다이제스트

+ {#if digest} + + {digest.digest_date} · {digest.countries.length}국 · {digest.total_topics}주제 + + {/if} +
+ + {#if loading} + + + + {:else if error} + + + + {:else if !digest || digest.countries.length === 0} + + {:else} + + {#snippet children(_activeId)} + {#if topics.length === 0} + + {:else} +
+ {#each topics as t (t.topic_rank)} + +
+

+ {t.topic_rank}. {t.topic_label} +

+ {#if t.llm_fallback_used} + fallback + {/if} +
+

{t.summary}

+
+ {t.article_count} articles · importance {t.importance_score.toFixed(2)} +
+
+ {/each} +
+ {/if} + {/snippet} +
+ {/if} +