diff --git a/app/api/study_cards.py b/app/api/study_cards.py index 38ef817..96bb65d 100644 --- a/app/api/study_cards.py +++ b/app/api/study_cards.py @@ -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) diff --git a/frontend/src/routes/study/+page.svelte b/frontend/src/routes/study/+page.svelte index fa43a65..463137c 100644 --- a/frontend/src/routes/study/+page.svelte +++ b/frontend/src/routes/study/+page.svelte @@ -3,7 +3,7 @@ // 주제로 보기(퀴즈·복습·통계) / 자료 학습 / 필사 세션 / 암기카드 검수. import { onMount } from 'svelte'; import { api } from '$lib/api'; - import { BookOpen, PenLine, GraduationCap, FolderKanban, Layers } from 'lucide-svelte'; + import { BookOpen, PenLine, GraduationCap, FolderKanban, Layers, Repeat } from 'lucide-svelte'; let cardReviewCount = $state(0); onMount(async () => { @@ -69,13 +69,23 @@

푼 문제에서 AI가 추출한 암기카드(cloze 빈칸 / qa)를 확인하고 승인·수정·폐기. 승인된 카드만 학습에 쓰입니다.

+ + +
+ +

암기카드 학습

+
+

검수한 암기카드를 모바일에서 학습. 복습(간격반복 1·3·7·14일)으로 자기평가하거나, 그냥 공부로 덜 본 카드를 휙휙 훑어봅니다.

+
예정
diff --git a/frontend/src/routes/study/cards-review/+page.svelte b/frontend/src/routes/study/cards-review/+page.svelte index ed828ba..99565cb 100644 --- a/frontend/src/routes/study/cards-review/+page.svelte +++ b/frontend/src/routes/study/cards-review/+page.svelte @@ -12,7 +12,7 @@ import { api } from '$lib/api'; import { addToast } from '$lib/stores/toast'; import { - ArrowLeft, Check, Pencil, Trash2, X, CheckCheck, FileText, + ArrowLeft, Check, Pencil, Trash2, X, CheckCheck, FileText, Repeat, } from 'lucide-svelte'; import Button from '$lib/components/ui/Button.svelte'; import Skeleton from '$lib/components/ui/Skeleton.svelte'; @@ -134,10 +134,11 @@ {#if total > 0} 대기 {total} {/if} -
+
+
diff --git a/frontend/src/routes/study/cards-study/+page.svelte b/frontend/src/routes/study/cards-study/+page.svelte new file mode 100644 index 0000000..8639d21 --- /dev/null +++ b/frontend/src/routes/study/cards-study/+page.svelte @@ -0,0 +1,355 @@ + + +암기카드 학습 + +
+ +
+ {#if mode === 'landing'} + +

암기카드 학습

+ {:else} + + {#if !done && total > 0} +
+
+
+ {Math.min(idx + 1, total)} / {total} + {:else} +

{mode === 'review' ? '복습' : '그냥 공부'}

+ {/if} + {/if} +
+ + {#if mode === 'cram' && !loading && !done} + +
+ {#each [['', '전체'], ['cloze', 'cloze'], ['qa', 'qa']] as [val, label] (val)} + + {/each} +
+ {/if} + + {#if mode === 'landing'} + +

검수 완료한 암기카드를 학습합니다. 두 가지 방법 중 선택하세요.

+
+ + + +
+ + {:else if loading} +
+ + {:else if done} + +
+ {#if mode === 'review'} +
오늘 카드 복습 완료
+
+
{tally.correct}
+
{tally.unsure}
애매
+
{tally.wrong}
모름
+
+

애매·모름 카드는 내일 복습 큐에 다시 올라옵니다. 암 카드는 간격만큼 쉬어요.

+ {:else} +
훑어보기 완료
+
{seen}
+

'봤다'로 기록한 카드는 다음에 덜 본 순서에서 뒤로 갑니다.

+ {/if} +
+ + +
+
+ + {:else if total === 0} + +
+ {#if mode === 'review'} + + {:else} + + {/if} +
+ + {:else if current} + +
+
+ {current.format} + +
+ 앞 — {current.format === 'qa' ? '질문' : '회상'} +
+
{frontText(current)}
+ + {#if revealed} +
+
정답
+
{current.fact}
+ {#if current.evidence?.length && current.evidence[0].snippet} +
근거: {current.evidence[0].snippet}
+ {/if} +
+ {/if} + + {#if !revealed} + + {/if} +
+ + + {#if revealed} + {#if mode === 'review'} +
+ + + +
+ + {:else} + + {/if} + {/if} +
+ {/if} +