7f4d64c6df
- study_quiz_sessions 테이블 (한 토픽 in_progress 1개 partial unique)
- study_question_attempts 에 quiz_session_id + reviewed_at 컬럼
- 풀이 진행률 서버 단일 진실 (cursor) — 나갔다 와도 이어풀기 가능
- 통합뷰: 진행 중 카드(이어풀기) + 최근 완료 결과 카드(미확인 N건 배지)
- 신규 /quiz-sessions/[sid] 결과 페이지 (3 카테고리 + AI 해설 + 분야 설명 + 학습완료 토글)
- /review 페이지는 풀이만, 마지막 문제 풀이 후 결과 페이지로 redirect
- 마이그레이션 206~209 (single-statement, asyncpg 호환)
- API: POST/GET/PATCH /study-topics/{tid}/quiz-sessions(/{sid}),
PATCH /study-question-attempts/{aid}/review-mark
- AttemptCreate.quiz_session_id 추가 — submit_attempt 가 같은 트랜잭션에서
세션 cursor + count 증가, 마지막이면 status='done' + finished_at
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
51 lines
2.3 KiB
Python
51 lines
2.3 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)
|
|
|
|
# 출제 순서 스냅샷 — 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)
|
|
|
|
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
|
|
)
|