"""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")