13404cd366
학습 의미: 한 quiz 세션 안에서 같은 유형 문제가 과도하게 몰리지 않게 분산. 같은 유형을 없애는 게 아니라 펼치는 것 — dedup/제거 프레임 금지. - 마이그레이션 210: study_quiz_sessions.quiz_mode VARCHAR(30) DEFAULT 'random' - ORM: StudyQuizSession.quiz_mode 필드 - service.related_types: apply_type_spacing helper 추가 - SPACING_THRESHOLD=0.88 (회차 무관 — PR-12-A 회차 필터 재사용 X) - PER_TYPE_CAP=2 (local neighbor cap, transitive cluster 보장 X) - SPACING_BUFFER_RATIO=2.0 - 3단계 fallback: ready spacing → pending 보충 → hold cap 위반 fallback - debug 로그 type_spacing_applied subject=... ready=N selected=M ... - _select_questions_for_topic: subject bucket 단위 spacing (과목 균등 보호) - QuizMode Enum (random) — 향후 frequent_focus/wrong_variants 예약 - start_quiz_session 에 quiz_mode 받기 + apply_spacing 전달 - 프론트 startNewQuiz body 에 quiz_mode='random' 명시 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
53 lines
2.5 KiB
Python
53 lines
2.5 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)
|
|
|
|
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
|
|
)
|