Files
hyungi_document_server/app/models/study_quiz_session.py
T
Hyungi Ahn 13404cd366 feat(study): 같은 유형 과밀 방지 — 출제 단계 spacing (PR-12-B)
학습 의미: 한 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>
2026-04-29 08:45:15 +09:00

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
)