d3bf963a66
Phase 1 finalize 가 계산하던 SessionSummary 가 응답에 포함되지 않고 discard 되던 것을 quiz_session row 4 컬럼으로 영속화. 결과 화면 헤더에 회복/퇴행/ 새로 맞힘/반복 오답 누적 변화 카운트 + "바로 할 일" 콜아웃 (지금 시점 progress 기반 동적 카운트 — pending_review/chronic/regressed). 동적 카운트는 결과 GET 호출 시점에만 계산 (목록 endpoint 비용 회피). 확인완료 통합 — 결과 카드의 [학습완료] 버튼이 attempts.reviewed_at 만 박던 것을 progress.last_reviewed_at + (wrong/unsure 면 due_at 최초 부여) 도 같이 박도록. reviewed=false 토글은 attempts 만 되돌림 (다른 attempt 가 검토 표시 했을 수 있어 progress 의 last_reviewed_at 은 보존). - migrations/230 — quiz_sessions 4 컬럼 ADD (단일 ALTER TABLE) - StudyQuizSession 모델 + finalize_session 가 row 영속화 - QuizSessionSummary 응답에 4 스냅샷 + 3 동적 필드 (default 0) - _build_session_summary include_progress_counts=True 시 SQL 3회 - review-mark 가 reveiwed=true 시 progress 동기화 - 결과 화면: 헤더 변화 카운트 줄 + 바로 할 일 콜아웃 (값 있을 때만) Plan: ~/.claude/plans/crispy-petting-dijkstra.md (Phase 2-B) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
59 lines
2.9 KiB
Python
59 lines
2.9 KiB
Python
"""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))
|
|
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
|
|
)
|