"""study_quiz_sessions ORM (PR-10) — 문제풀이 세션 기록 + 이어풀기. 한 토픽의 한 회차 풀이 = 한 행. question_ids 는 출제 순서 스냅샷. status: in_progress / done / abandoned (강한 enum 미사용 — VARCHAR 권장값). 한 토픽당 in_progress 1개 강제는 partial unique idx (마이그레이션 207). """ from datetime import datetime from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, Integer, String from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from core.database import Base class StudyQuizSession(Base): __tablename__ = "study_quiz_sessions" id: Mapped[int] = mapped_column(BigInteger, primary_key=True) user_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) study_topic_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("study_topics.id", ondelete="CASCADE"), nullable=False ) target_per_subject: Mapped[int] = mapped_column(Integer, nullable=False, default=20) subject_filter: Mapped[str | None] = mapped_column(String(120)) wrong_only: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) # PR-12-B: 출제 모드. 권장값 = random (1차) / frequent_focus / wrong_variants (예약). quiz_mode: Mapped[str] = mapped_column(String(30), nullable=False, default="random") # 출제 순서 스냅샷 — list[int] (question id). 출제 후 변경 안 됨. question_ids: Mapped[list] = mapped_column(JSONB, nullable=False) # {subject: count} 분포. 결과 카드 통계 표시용. subject_distribution: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) status: Mapped[str] = mapped_column(String(20), nullable=False, default="in_progress") cursor: Mapped[int] = mapped_column(Integer, nullable=False, default=0) correct_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) wrong_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) unsure_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) # Phase 2-B: finalize 결과 요약 스냅샷. 세션 종료 시점에 박혀 결과 화면 헤더에 노출. newly_correct_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) relapsed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) recovered_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) chronic_remaining_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) # study-to-viewer P2: 뷰어 ingest 멱등/출처. 라이브 세션=finalized_at·client_session_uuid NULL, source='live'. finalized_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) # 멱등 마커(mig 373) client_session_uuid: Mapped[str | None] = mapped_column(String(64)) # 뷰어 세션 UUID(mig 374, uq mig376) source: Mapped[str] = mapped_column(String(20), nullable=False, default="live") # live|viewer(mig 375) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=datetime.now, nullable=False ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=datetime.now, onupdate=datetime.now, nullable=False )