"""study_question_progress — 사용자 × 토픽 × 문제 단위 현재 상태 캐시 (Phase 1). attempts (append-only 원본 로그) 와 분리. 한 번 박힌 attempts 는 절대 update 안 함. progress 는 마지막 시도 / 사용자 검토 / 복습 큐 / 패턴 분류 derived 4 차원 메타. 세션 종료 시 finalize 가 다음 갱신: - last_outcome / last_attempted_at / last_attempt_id - pattern_state / pattern_updated_at / pattern_window_attempts - (이미 due_at 박힌 행만) review_stage / due_at ← 복습 stage 갱신 review-complete 가 다음 갱신: - last_reviewed_at - (wrong/unsure 인 경우) due_at 최초 부여 study_question_id 는 단일 topic 소속 전제 (현재 가스기사 토픽 4 단일 운영). 향후 question 재사용/N:M 가능성 대비 unique 키는 (user_id, study_topic_id, study_question_id) 3 키. """ from __future__ import annotations from datetime import datetime from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, SmallInteger, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from core.database import Base class StudyQuestionProgress(Base): __tablename__ = "study_question_progress" __table_args__ = ( UniqueConstraint( "user_id", "study_topic_id", "study_question_id", name="uq_progress_user_topic_question", ), ) 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 ) study_question_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("study_questions.id", ondelete="RESTRICT"), nullable=False ) # 마지막 시도 요약 last_outcome: Mapped[str | None] = mapped_column(String(20)) last_attempted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) last_attempt_id: Mapped[int | None] = mapped_column( BigInteger, ForeignKey("study_question_attempts.id", ondelete="SET NULL") ) # 사용자 검토 상태 last_reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) # 복습 큐 due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) review_stage: Mapped[int | None] = mapped_column(SmallInteger) # 패턴 분류 (derived) pattern_state: Mapped[str | None] = mapped_column(String(30)) pattern_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) pattern_window_attempts: Mapped[int | None] = mapped_column(SmallInteger) 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 )