e1da984e08
문제 SR과 카드 SR이 같은 간격 상수·산술을 참조하도록 순수함수 추출. 운영 동작 무변경.
- app/services/study/sr_schedule.py: REVIEW_INTERVAL_DAYS{1:3,2:7,3:14}/MASTERED=4/FIRST_DUE=1
+ advance(stage,outcome,now)→(new_stage,new_due) | None(skipped) + first_due(now).
진입 게이트(due_at IS NOT NULL/최초 due/skipped 불변)는 호출부 잔류(finalize vs review-complete 정책 차이).
- session_finalize.py: 상수·advance 분기 → sr_schedule import + sr_advance() (re-export 유지).
- study_question_progress.py: DEFAULT_FIRST_DUE_DAYS → sr_schedule import.
- 회귀 테스트 7/7: 전진 1·3·7·14·졸업·리셋·skipped불변·상수 + 전 stage×outcome 구 로직 바이트 동등.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
49 lines
2.0 KiB
Python
49 lines
2.0 KiB
Python
"""SR(간격반복) 산술 단일 source — 문제 SR + 카드 SR 공용.
|
|
|
|
session_finalize.py(문제 SR)와 study_memo_card writer(카드 SR)가 같은 상수·산술을 참조하도록
|
|
순수함수로 추출. 진입 게이트(due_at IS NOT NULL 행만 갱신 / 최초 due 부여 / skipped 불변)는
|
|
호출부에 남긴다 — finalize 와 review-complete 의 정책이 미묘히 달라 통합 시 회귀 위험.
|
|
|
|
정본 간격(실측): review_stage 0→1→2→3 = 1·3·7·14일, stage4 = 졸업(due_at=NULL),
|
|
오답/모호 리셋 = 내일(stage 0).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
# review_stage 별 '다음 due_at' interval (days). stage 1→3일, 2→7일, 3→14일.
|
|
REVIEW_INTERVAL_DAYS = {1: 3, 2: 7, 3: 14}
|
|
# 이 stage 도달 시 졸업 (due_at=NULL, 복습 큐에서 제거)
|
|
REVIEW_STAGE_MASTERED = 4
|
|
# 최초 due 부여 / 오답 리셋 = 내일
|
|
DEFAULT_FIRST_DUE_DAYS = 1
|
|
|
|
|
|
def advance(
|
|
review_stage: int | None, outcome: str, now: datetime
|
|
) -> tuple[int, datetime | None] | None:
|
|
"""이미 복습 큐(due_at IS NOT NULL)에 있는 항목의 SR 갱신 산술.
|
|
|
|
호출부가 'due_at IS NOT NULL' 가드 후 호출한다.
|
|
반환:
|
|
(new_stage, new_due_at) — correct/wrong/unsure. 졸업이면 new_due_at=None.
|
|
None — skipped/기타(변경 없음, 호출부가 무시).
|
|
"""
|
|
if outcome == "correct":
|
|
new_stage = (review_stage or 0) + 1
|
|
if new_stage >= REVIEW_STAGE_MASTERED:
|
|
return new_stage, None # 학습완료(졸업)
|
|
return new_stage, now + timedelta(days=REVIEW_INTERVAL_DAYS[new_stage])
|
|
if outcome in ("wrong", "unsure"):
|
|
return 0, now + timedelta(days=DEFAULT_FIRST_DUE_DAYS)
|
|
return None # skipped — due_at/stage 불변
|
|
|
|
|
|
def first_due(now: datetime) -> tuple[int, datetime]:
|
|
"""복습 큐 최초 진입(오답/모호 + due_at IS NULL) 시 부여값.
|
|
|
|
문제 review-complete / 카드 첫 회상 공용. 반환: (review_stage=0, due_at=내일).
|
|
"""
|
|
return 0, now + timedelta(days=DEFAULT_FIRST_DUE_DAYS)
|