feat(study): 카드 SR 모바일 학습 UI — 복습/그냥공부 2트랙 (B3)
검수 완료 카드를 모바일에서 학습하는 UI. 복습(SR)=앞면 회상→reveal→3단 자기평가(모름/애매/암) / 그냥공부(cram)=덜 본 순 휙휙+봤다(SR 무관). - 새 페이지 /study/cards-study(+page.svelte): landing 트랙선택·진행바·결과(세션 tally)·빈/로딩 상태·cram format 필터·키보드(Space reveal·복습 J/K/L·cram Enter). 아이폰15PM 우선, 세이지 토큰. - '암'(correct) 버튼 stage별 동적 라벨(+3/7/14일·졸업), 모름/애매=내일. correctLabel은 sr_schedule REVIEW_INTERVAL_DAYS 미러(라벨 전용, 산술 정본은 백엔드). - API: /study-cards/due CardItem에 review_stage 추가(복습 큐에서만 채움, 동적 라벨용). _build_card_items(session,cards,stages) 확장, /due는 select(card, progress.review_stage)로 변경. - 진입: 허브 '암기카드 학습' 카드+예정목록 갱신 / 검수 UI 헤더 '학습' 버튼. 검증: py_compile OK · 4차원 적대검토(runes·API계약·SR규칙·UX) 통과(확정 조치 0, 지적 2건 거짓양성). 로컬 vite 빌드 불가(node_modules 부재)→배포가 컴파일 게이트. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+17
-6
@@ -47,6 +47,9 @@ class CardItem(BaseModel):
|
||||
needs_review: bool
|
||||
flagged_by: str | None = None
|
||||
evidence: list[CardEvidence] = []
|
||||
# 복습(SR) 큐에서만 채움 — 정답('암') 시 다음 복습일 미리보기 라벨 계산용
|
||||
# (stage별 동적: +3/7/14일·졸업). deck/검수 응답에선 None.
|
||||
review_stage: int | None = None
|
||||
|
||||
|
||||
class CardQuestionGroup(BaseModel):
|
||||
@@ -86,11 +89,17 @@ _RATE_MAP = {
|
||||
|
||||
|
||||
async def _build_card_items(
|
||||
session: AsyncSession, cards: list[StudyMemoCard]
|
||||
session: AsyncSession,
|
||||
cards: list[StudyMemoCard],
|
||||
stages: dict[int, int | None] | None = None,
|
||||
) -> list[CardItem]:
|
||||
"""카드 목록 → CardItem(evidence 동반). due/deck 학습 flow 공용."""
|
||||
"""카드 목록 → CardItem(evidence 동반). due/deck 학습 flow 공용.
|
||||
|
||||
stages: card_id → review_stage (복습 큐에서만 전달, 동적 라벨 미리보기용).
|
||||
"""
|
||||
if not cards:
|
||||
return []
|
||||
stages = stages or {}
|
||||
ids = [c.id for c in cards]
|
||||
ev_rows = (
|
||||
await session.execute(
|
||||
@@ -106,7 +115,7 @@ async def _build_card_items(
|
||||
CardItem(
|
||||
id=c.id, source_kind=c.source_kind, format=c.format, cue=c.cue, fact=c.fact,
|
||||
cloze_text=c.cloze_text, needs_review=c.needs_review, flagged_by=c.flagged_by,
|
||||
evidence=ev_by.get(c.id, []),
|
||||
evidence=ev_by.get(c.id, []), review_stage=stages.get(c.id),
|
||||
)
|
||||
for c in cards
|
||||
]
|
||||
@@ -256,7 +265,7 @@ async def due_cards(
|
||||
now = datetime.now(timezone.utc)
|
||||
rows = (
|
||||
await session.execute(
|
||||
select(StudyMemoCard)
|
||||
select(StudyMemoCard, StudyMemoCardProgress.review_stage)
|
||||
.join(StudyMemoCardProgress, StudyMemoCardProgress.card_id == StudyMemoCard.id)
|
||||
.where(
|
||||
StudyMemoCard.user_id == user.id,
|
||||
@@ -272,8 +281,10 @@ async def due_cards(
|
||||
.order_by(StudyMemoCardProgress.due_at.asc())
|
||||
.limit(limit)
|
||||
)
|
||||
).scalars().all()
|
||||
return await _build_card_items(session, list(rows))
|
||||
).all()
|
||||
cards = [r[0] for r in rows]
|
||||
stages = {r[0].id: r[1] for r in rows}
|
||||
return await _build_card_items(session, cards, stages)
|
||||
|
||||
|
||||
@router.post("/{card_id}/rate", response_model=RateResult)
|
||||
|
||||
Reference in New Issue
Block a user