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일)으로 자기평가하거나, 그냥 공부로 덜 본 카드를 휙휙 훑어봅니다.
+
예정
- - 검수한 암기카드로 복습 (카드 SRS)
- - 모바일 암기카드 복습 + 공부 알람
+ - 애플워치 빠른복습 + 공부 알람(push)
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'}
+
오늘 카드 복습 완료
+
+
애매·모름 카드는 내일 복습 큐에 다시 올라옵니다. 암 카드는 간격만큼 쉬어요.
+ {:else}
+
훑어보기 완료
+
+
'봤다'로 기록한 카드는 다음에 덜 본 순서에서 뒤로 갑니다.
+ {/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'}
+
+
+
+
+
+
키보드: J 모름 · K 애매 · L 암
+ {:else}
+
+ {/if}
+ {/if}
+
+ {/if}
+