diff --git a/app/api/study_cards.py b/app/api/study_cards.py index 96bb65d..37f9aa1 100644 --- a/app/api/study_cards.py +++ b/app/api/study_cards.py @@ -16,7 +16,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel -from sqlalchemy import func, or_, select, update +from sqlalchemy import and_, func, or_, select, update from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession @@ -261,24 +261,31 @@ async def due_cards( session: Annotated[AsyncSession, Depends(get_session)], limit: Annotated[int, Query(ge=1, le=200)] = 30, ): - """오늘 복습할 카드 (due_at<=now, stage<4, 검수 통과만). due_at 오름차순.""" + """오늘 복습할 카드 (검수 통과만). 두 부류: + - 신규 승인 카드(progress 없음=첫 회상 전) — SR 큐 진입 경로(첫 회상). '암'이면 due 안 + 박고 종료('큐 폭발 방지'), 애매/모름이면 평가 즉시 due(내일)로 입고. + - 예정 due 카드(due_at<=now, stage<4). + progress 는 user+card UNIQUE 라 outer join 으로 최대 1행. 예정 due 먼저, 신규(due NULL) 뒤로.""" now = datetime.now(timezone.utc) + P = StudyMemoCardProgress rows = ( await session.execute( - select(StudyMemoCard, StudyMemoCardProgress.review_stage) - .join(StudyMemoCardProgress, StudyMemoCardProgress.card_id == StudyMemoCard.id) + select(StudyMemoCard, P.review_stage) + .outerjoin(P, and_(P.card_id == StudyMemoCard.id, P.user_id == user.id)) .where( StudyMemoCard.user_id == user.id, StudyMemoCard.deleted_at.is_(None), StudyMemoCard.needs_review.is_(False), - StudyMemoCardProgress.due_at.is_not(None), - StudyMemoCardProgress.due_at <= now, or_( - StudyMemoCardProgress.review_stage.is_(None), - StudyMemoCardProgress.review_stage < 4, + P.id.is_(None), # 신규(첫 회상 전) — progress 미생성 + and_( + P.due_at.is_not(None), + P.due_at <= now, + or_(P.review_stage.is_(None), P.review_stage < 4), + ), ), ) - .order_by(StudyMemoCardProgress.due_at.asc()) + .order_by(P.due_at.asc().nulls_last(), StudyMemoCard.id.asc()) .limit(limit) ) ).all() diff --git a/frontend/src/routes/study/cards-study/+page.svelte b/frontend/src/routes/study/cards-study/+page.svelte index 8639d21..95bd3aa 100644 --- a/frontend/src/routes/study/cards-study/+page.svelte +++ b/frontend/src/routes/study/cards-study/+page.svelte @@ -22,9 +22,11 @@ import EmptyState from '$lib/components/ui/EmptyState.svelte'; // sr_schedule.py 단일 source 미러 — '암'(correct) 다음 복습일 라벨 전용(실제 스케줄은 백엔드). + // stage===null = 신규 카드(progress 없음): '암'이면 백엔드가 due 안 박음(외움→큐 제외)이라 '안 나옴'. const REVIEW_INTERVAL_DAYS = { 1: 3, 2: 7, 3: 14 }; function correctLabel(stage) { - const ns = (stage ?? 0) + 1; + if (stage === null || stage === undefined) return '안 나옴'; + const ns = stage + 1; if (ns >= 4) return '졸업'; return `+${REVIEW_INTERVAL_DAYS[ns]}일`; }