4b8120d83f
사용자 요청 (2026-05-13):
- 오늘 briefing 만 보여주고 과거 못 보는 게 아쉬움 → 날짜 선택 UI
- 시간대 별 나열은 오히려 불편 → date dropdown 1단계 선택
- 각 카드에 읽음/하이라이트 토글
Schema (migrations 263~266, 단일 statement):
- briefing_topics.is_read BOOL NOT NULL DEFAULT false
- briefing_topics.read_at TIMESTAMPTZ
- briefing_topics.highlighted BOOL NOT NULL DEFAULT false
- briefing_topics.highlighted_at TIMESTAMPTZ
API (app/api/briefing.py):
- TopicResponse 에 id / is_read / read_at / highlighted / highlighted_at 추가
- GET /api/briefing/dates → 사용 가능 날짜 목록 (60일 cap)
· briefing_date / total_topics / total_articles / status / read_count / highlighted_count
- PATCH /api/briefing/topics/{id}/read body {value: bool} → 읽음 토글
- PATCH /api/briefing/topics/{id}/highlight body {value: bool} → 하이라이트 토글
- 토글 시 *_at 컬럼 자동 설정/NULL
UI (frontend/src/routes/news/+page.svelte):
- 헤더 우측 <select> date dropdown — 최신 + N일치 (highlighted_count 별 표시)
- 선택 시 /api/briefing?date=… 로 해당 날짜 briefing 로드
- 카드 우측 상단 ★ (하이라이트) + 읽음 버튼
- 하이라이트 = Card class ring-2 ring-yellow-400
- 읽음 = 외부 div class opacity-60 (시각 차분화, 펴기 가능)
- 토글 즉시 PATCH 호출 + 로컬 state 갱신
each key topic.topic_rank → topic.id 변경 (이미 unique).
104 lines
4.2 KiB
Python
104 lines
4.2 KiB
Python
"""morning_briefings + briefing_topics 테이블 ORM (야간 수집 뉴스 브리핑).
|
||
|
||
axis 반대: Phase 4 = country×topic / Briefing = topic×country.
|
||
country_perspectives JSONB 안에 한 topic 의 여러 국가 관점 array.
|
||
"""
|
||
|
||
from datetime import date, datetime
|
||
|
||
from sqlalchemy import (
|
||
BigInteger,
|
||
Boolean,
|
||
Date,
|
||
DateTime,
|
||
Float,
|
||
ForeignKey,
|
||
Integer,
|
||
String,
|
||
Text,
|
||
)
|
||
from sqlalchemy.dialects.postgresql import JSONB
|
||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
|
||
from core.database import Base
|
||
|
||
|
||
class MorningBriefing(Base):
|
||
"""하루 단위 브리핑 메타데이터 (KST 자정~05:00 윈도우)"""
|
||
|
||
__tablename__ = "morning_briefings"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||
briefing_date: Mapped[date] = mapped_column(Date, nullable=False, unique=True)
|
||
window_start: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||
window_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||
decay_lambda: Mapped[float] = mapped_column(Float, nullable=False)
|
||
|
||
total_articles: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
total_countries: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
total_topics: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
|
||
generation_ms: Mapped[int | None] = mapped_column(Integer)
|
||
llm_calls: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
llm_failures: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
||
status: Mapped[str] = mapped_column(String(20), nullable=False, default="success")
|
||
|
||
headline_oneliner: Mapped[str | None] = mapped_column(Text)
|
||
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, default=datetime.now
|
||
)
|
||
|
||
topics: Mapped[list["BriefingTopic"]] = relationship(
|
||
back_populates="briefing",
|
||
cascade="all, delete-orphan",
|
||
order_by="BriefingTopic.topic_rank",
|
||
)
|
||
|
||
|
||
class BriefingTopic(Base):
|
||
"""1 briefing 안 topic_rank 순 cross-country 비교 분석 결과"""
|
||
|
||
__tablename__ = "briefing_topics"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||
briefing_id: Mapped[int] = mapped_column(
|
||
BigInteger,
|
||
ForeignKey("morning_briefings.id", ondelete="CASCADE"),
|
||
nullable=False,
|
||
)
|
||
|
||
topic_rank: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
topic_label: Mapped[str] = mapped_column(String(120), nullable=False)
|
||
headline: Mapped[str] = mapped_column(Text, nullable=False)
|
||
|
||
country_perspectives: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
||
divergences: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
||
convergences: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
||
key_quotes: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
||
|
||
historical_article_ids: Mapped[list | None] = mapped_column(JSONB)
|
||
historical_context: Mapped[str | None] = mapped_column(Text)
|
||
historical_window_days: Mapped[int | None] = mapped_column(Integer)
|
||
|
||
cluster_members: Mapped[list] = mapped_column(JSONB, nullable=False, default=list)
|
||
article_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
country_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
importance_score: Mapped[float] = mapped_column(Float, nullable=False)
|
||
raw_weight_sum: Mapped[float] = mapped_column(Float, nullable=False)
|
||
|
||
llm_model: Mapped[str | None] = mapped_column(String(100))
|
||
llm_fallback_used: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
|
||
# 2026-05-13 카드별 사용자 액션 (date picker 와 동반).
|
||
is_read: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
read_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
highlighted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||
highlighted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime(timezone=True), nullable=False, default=datetime.now
|
||
)
|
||
|
||
briefing: Mapped["MorningBriefing"] = relationship(back_populates="topics")
|