8d3b648b5f
뷰어 로컬 풀이 세션을 DS 로 흘려 학습엔진(SR/pattern/오답/4-A·4-B) 재생. 기본 inert(flag off). - 마이그 373~376: study_quiz_sessions 에 finalized_at(멱등 마커)·client_session_uuid·source + UNIQUE(client_session_uuid, study_topic_id) partial. - outcome.py derive_outcome = 채점 단일 소스(라이브 submit_attempt 도 이걸로 리팩터 → 정오 어휘 한 곳, ingest 는 raw 신호 selected+unsure 만 싣고 DS 산출 = '무수정 재생' 성립). - ingest_study.py: Bearer(VIEWER_SYNC_TOKEN)+study_ingest_enabled gate. pub_id→source_id→question 해소(graceful skip)·principal=question.user_id(mixed 거부)·topic 별 DS 세션(source=viewer·uuid) 생성+attempt+finalize_session 무수정 재생+finalized_at, 1-tx 원자. uuid 존재=already_ingested 캐시반환(멱등 → at-least-once 재전송에도 SR 이중 advance 0). - config study_ingest_enabled + compose 매핑 + main 등록. 검증: py_compile·ephemeral 마이그(373~376 라이브스키마 위 클린)·single-statement. 배포 후 합성 세션 멱등/무이중SR 실측 예정. 배포=inert(STUDY_INGEST_ENABLED 미설정=503). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
3.4 KiB
Python
63 lines
3.4 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))
|
|
# study-to-viewer P2: 뷰어 ingest 멱등/출처. 라이브 세션=finalized_at·client_session_uuid NULL, source='live'.
|
|
finalized_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) # 멱등 마커(mig 373)
|
|
client_session_uuid: Mapped[str | None] = mapped_column(String(64)) # 뷰어 세션 UUID(mig 374, uq mig376)
|
|
source: Mapped[str] = mapped_column(String(20), nullable=False, default="live") # live|viewer(mig 375)
|
|
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
|
|
)
|