0d274cc5fe
검토 완료 카드를 학습하는 백엔드. 복습(SR)=즉시 자동 입고 / 그냥공부(cram)=봤다 기록, SR 무관.
- migrations 299(idx_card_progress_due partial) + 300(study_memo_cards view_count/last_viewed_at).
- StudyMemoCardProgress 모델(294 미러, UNIQUE user+card) + rate_card(get-or-create → sr_schedule.advance/first_due, 즉시 자동 입고: 애매/모름 평가 즉시 due, 암은 due 안 박음).
- StudyMemoCard view_count/last_viewed_at + record_card_view 헬퍼(cram, SR 무관).
- API: GET /study-cards/due(복습 큐, 검수통과만) · POST /{id}/rate(자기평가 read-time 매핑) · GET /deck(cram, 덜 본 순) · POST /{id}/view(봤다 기록).
검증: 부팅+8라우트 등록 · 287~300 ephemeral 적용(인덱스·컬럼 확인) · sr_schedule 회귀 7/7(B1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
3.7 KiB
Python
89 lines
3.7 KiB
Python
"""study_memo_card_progress ORM — 카드 SR(간격반복) 상태 (문제 progress '분리 미러').
|
|
|
|
migration 294. 226 골격 축소: SR 4컬럼(last_outcome/last_reviewed_at/due_at/review_stage)만,
|
|
pattern 분류 컬럼은 미보유(카드 복습함은 due/미확인/완료 3탭). UNIQUE(user_id, card_id).
|
|
간격 산술은 sr_schedule.py 단일 source.
|
|
|
|
입고 정책(결정 2026-06-07): '평가 즉시 자동 입고' — 애매/모름 카드는 평가 즉시 due 부여
|
|
(문제 SR의 [학습완료] 수동 게이트와 달리 자동). 암(correct) 카드는 due 안 박음(큐 폭발 방지).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import BigInteger, DateTime, ForeignKey, SmallInteger, String, UniqueConstraint, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from core.database import Base
|
|
from models.study_memo_card import StudyMemoCard
|
|
from services.study import sr_schedule
|
|
|
|
|
|
class StudyMemoCardProgress(Base):
|
|
__tablename__ = "study_memo_card_progress"
|
|
__table_args__ = (UniqueConstraint("user_id", "card_id", name="uq_card_progress_user_card"),)
|
|
|
|
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
|
|
)
|
|
card_id: Mapped[int] = mapped_column(
|
|
BigInteger, ForeignKey("study_memo_cards.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
|
|
last_outcome: Mapped[str | None] = mapped_column(String(20))
|
|
last_reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
due_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
review_stage: Mapped[int | None] = mapped_column(SmallInteger)
|
|
|
|
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
|
|
)
|
|
|
|
|
|
async def rate_card(
|
|
session: AsyncSession, *, card: StudyMemoCard, outcome: str, now: datetime
|
|
) -> StudyMemoCardProgress:
|
|
"""카드 자기평가 1건 처리 (SR 즉시 자동 입고). outcome ∈ correct/wrong/unsure.
|
|
|
|
- progress 없으면 생성. last_outcome/last_reviewed_at 갱신.
|
|
- 이미 due(복습 큐)면 sr_schedule.advance(전진/리셋/졸업).
|
|
- due 없으면 애매/모름만 first_due 부여(즉시 입고), 암은 due 안 박음.
|
|
caller 가 commit.
|
|
"""
|
|
progress = (
|
|
await session.execute(
|
|
select(StudyMemoCardProgress).where(
|
|
StudyMemoCardProgress.user_id == card.user_id,
|
|
StudyMemoCardProgress.card_id == card.id,
|
|
)
|
|
)
|
|
).scalar_one_or_none()
|
|
if progress is None:
|
|
progress = StudyMemoCardProgress(
|
|
user_id=card.user_id, study_topic_id=card.study_topic_id, card_id=card.id
|
|
)
|
|
session.add(progress)
|
|
|
|
progress.last_outcome = outcome
|
|
progress.last_reviewed_at = now
|
|
|
|
if progress.due_at is not None:
|
|
result = sr_schedule.advance(progress.review_stage, outcome, now)
|
|
if result is not None: # skipped 는 None → 불변
|
|
progress.review_stage, progress.due_at = result
|
|
elif outcome in ("wrong", "unsure"):
|
|
# 즉시 자동 입고: 애매·모름은 평가 즉시 복습 큐로 (stage0 + 내일)
|
|
progress.review_stage, progress.due_at = sr_schedule.first_due(now)
|
|
# outcome == 'correct' 이고 due 없음 → due 안 박음(큐 폭발 방지)
|
|
|
|
return progress
|