Merge remote-tracking branch 'origin/feat/study-memo-card-p1' into feat/email-pkm-folder
This commit is contained in:
@@ -42,6 +42,23 @@
|
||||
let tally = $state({ correct: 0, unsure: 0, wrong: 0 }); // 복습 결과 집계
|
||||
let seen = $state(0); // 그냥공부 본 카드 수
|
||||
let dueCount = $state(null); // landing 배지
|
||||
let marks = $state([]); // 카드별 결과 (데스크탑 좌측 진행트랙 점): 'correct'|'unsure'|'wrong'|'seen'|'flagged'
|
||||
|
||||
function setMark(kind) {
|
||||
const m = [...marks];
|
||||
m[idx] = kind;
|
||||
marks = m;
|
||||
}
|
||||
// 데스크탑 진행트랙 점 클래스 (크기+색). 현재 카드는 accent 세로막대, 지난 카드는 결과색.
|
||||
function dotClass(i) {
|
||||
if (i === idx && !done) return 'h-4 w-1.5 bg-accent';
|
||||
const k = marks[i];
|
||||
if (k === 'correct') return 'h-1.5 w-1.5 bg-success';
|
||||
if (k === 'unsure') return 'h-1.5 w-1.5 bg-warning';
|
||||
if (k === 'wrong') return 'h-1.5 w-1.5 bg-error';
|
||||
if (k === 'seen' || k === 'flagged') return 'h-1.5 w-1.5 bg-faint';
|
||||
return 'h-1.5 w-1.5 bg-default';
|
||||
}
|
||||
|
||||
let current = $derived(cards[idx] ?? null);
|
||||
let total = $derived(cards.length);
|
||||
@@ -63,6 +80,7 @@
|
||||
idx = 0;
|
||||
revealed = false;
|
||||
tally = { correct: 0, unsure: 0, wrong: 0 };
|
||||
marks = [];
|
||||
try {
|
||||
cards = _dueCache ?? (await fetchDue());
|
||||
_dueCache = null; // 소비
|
||||
@@ -81,6 +99,7 @@
|
||||
idx = 0;
|
||||
revealed = false;
|
||||
seen = 0;
|
||||
marks = [];
|
||||
try {
|
||||
const q = fmtFilter ? `?format=${fmtFilter}&limit=40` : '?limit=40';
|
||||
cards = (await api(`/study-cards/deck${q}`)) ?? [];
|
||||
@@ -118,6 +137,7 @@
|
||||
});
|
||||
const key = label === '암' ? 'correct' : label === '애매' ? 'unsure' : 'wrong';
|
||||
tally = { ...tally, [key]: tally[key] + 1 };
|
||||
setMark(key);
|
||||
advance();
|
||||
} catch (err) {
|
||||
addToast('error', err?.detail || '평가 저장 실패');
|
||||
@@ -132,6 +152,7 @@
|
||||
try {
|
||||
await api(`/study-cards/${current.id}/view`, { method: 'POST' });
|
||||
seen += 1;
|
||||
setMark('seen');
|
||||
advance();
|
||||
} catch (err) {
|
||||
addToast('error', err?.detail || '기록 실패');
|
||||
@@ -149,6 +170,7 @@
|
||||
try {
|
||||
await api(`/study-cards/${c.id}`, { method: 'PATCH', body: JSON.stringify({ needs_review: true }) });
|
||||
addToast('success', '검수함으로 보냈어요 — 이 카드는 학습에서 빠집니다');
|
||||
setMark('flagged');
|
||||
advance();
|
||||
} catch (err) {
|
||||
addToast('error', err?.detail || '신고 처리 실패');
|
||||
@@ -198,7 +220,7 @@
|
||||
|
||||
<svelte:head><title>암기카드 학습</title></svelte:head>
|
||||
|
||||
<div class="mx-auto flex min-h-[100dvh] max-w-md flex-col px-4 py-4 sm:py-6">
|
||||
<div class="mx-auto flex min-h-[100dvh] max-w-md flex-col px-4 py-4 sm:py-6 md:max-w-5xl md:px-6">
|
||||
<!-- 헤더 -->
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
{#if mode === 'landing'}
|
||||
@@ -231,8 +253,8 @@
|
||||
|
||||
{#if mode === 'landing'}
|
||||
<!-- 트랙 선택 -->
|
||||
<p class="mb-4 text-sm text-dim">검수 완료한 암기카드를 학습합니다. 두 가지 방법 중 선택하세요.</p>
|
||||
<div class="space-y-3">
|
||||
<p class="mb-4 text-sm text-dim md:mx-auto md:max-w-xl">검수 완료한 암기카드를 학습합니다. 두 가지 방법 중 선택하세요.</p>
|
||||
<div class="space-y-3 md:mx-auto md:max-w-xl">
|
||||
<button
|
||||
onclick={startReview}
|
||||
class="block w-full rounded-card border border-default bg-surface p-5 text-left transition-colors hover:border-accent hover:bg-accent/5"
|
||||
@@ -308,78 +330,108 @@
|
||||
</div>
|
||||
|
||||
{:else if current}
|
||||
<!-- 카드 -->
|
||||
<div class="flex flex-1 flex-col">
|
||||
<div class="flex flex-1 flex-col rounded-card border border-default bg-surface p-5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span
|
||||
class="rounded-full px-2.5 py-0.5 text-[10px] font-bold text-white {current.format === 'cloze' ? 'bg-accent' : 'bg-domain-engineering'}"
|
||||
>{current.format}</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={flagCard}
|
||||
disabled={flagBusy || busy}
|
||||
class="flex items-center gap-1 text-[11px] text-faint transition-colors hover:text-warning disabled:opacity-50"
|
||||
title="카드 내용이 이상하면 검수함으로 보냅니다"
|
||||
>
|
||||
<Flag size={12} /> 이 카드 이상해요
|
||||
</button>
|
||||
<!-- 카드 — 모바일: 단일 카드 / 데스크탑(md+): Focus Stage(좌 진행트랙·중앙 무대카드·우 근거) -->
|
||||
<div class="flex flex-1 flex-col md:grid md:grid-cols-[170px_minmax(0,1fr)_260px] md:items-center md:gap-6">
|
||||
<!-- 좌측 진행 트랙 (데스크탑 전용) -->
|
||||
<div class="hidden md:flex md:flex-col md:items-center md:justify-center md:self-stretch md:py-4">
|
||||
<div class="text-xs font-bold tabular-nums text-dim">{Math.min(idx + 1, total)} / {total}</div>
|
||||
<div class="my-4 flex max-h-[280px] flex-col items-center gap-1 overflow-y-auto">
|
||||
{#each cards as _, i (i)}
|
||||
<span class="shrink-0 rounded-full transition-all {dotClass(i)}"></span>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-[10px] font-bold uppercase tracking-wide text-faint">
|
||||
앞 — {current.format === 'qa' ? '질문' : '회상'}
|
||||
</div>
|
||||
<div class="mt-1 text-lg font-semibold leading-relaxed text-text">{frontText(current)}</div>
|
||||
|
||||
{#if revealed}
|
||||
<div class="mt-4 rounded-lg border border-accent-ring bg-bg px-4 py-3">
|
||||
<div class="text-[10px] font-bold uppercase tracking-wide text-faint">정답</div>
|
||||
<div class="mt-0.5 text-xl font-bold text-accent">{current.fact}</div>
|
||||
{#if current.evidence?.length && current.evidence[0].snippet}
|
||||
<div class="mt-2 text-[11px] leading-relaxed text-dim">근거: {current.evidence[0].snippet}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !revealed}
|
||||
<button
|
||||
onclick={() => (revealed = true)}
|
||||
class="mt-auto flex items-center justify-center gap-2 rounded-md border border-dashed border-accent-ring bg-surface-hover py-3 text-sm font-medium text-accent transition-colors hover:bg-accent/5"
|
||||
>
|
||||
<Eye size={16} /> 탭하여 정답 보기 <span class="text-faint">(Space)</span>
|
||||
</button>
|
||||
{#if mode === 'review'}
|
||||
<div class="text-center text-[11px] leading-relaxed text-dim">암 <b class="text-success">{tally.correct}</b> · 애매 <b class="text-warning">{tally.unsure}</b> · 모름 <b class="text-error">{tally.wrong}</b></div>
|
||||
{:else}
|
||||
<div class="text-center text-[11px] text-dim">본 카드 <b class="text-accent">{seen}</b></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 평가/액션 (reveal 후) -->
|
||||
{#if revealed}
|
||||
{#if mode === 'review'}
|
||||
<div class="mt-3 grid grid-cols-3 gap-2">
|
||||
<!-- 중앙 무대 카드 + 평가 -->
|
||||
<div class="flex flex-1 flex-col md:block md:w-full md:max-w-[600px] md:justify-self-center">
|
||||
<div class="flex flex-1 flex-col rounded-card border border-default bg-surface p-5 md:flex-none md:min-h-[360px] md:p-8 md:shadow-md">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span
|
||||
class="rounded-full px-2.5 py-0.5 text-[10px] font-bold text-white {current.format === 'cloze' ? 'bg-accent' : 'bg-domain-engineering'}"
|
||||
>{current.format}</span>
|
||||
<button
|
||||
onclick={() => rate('모름')}
|
||||
disabled={busy}
|
||||
class="flex flex-col items-center rounded-lg bg-error py-3.5 text-sm font-bold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>모름<span class="mt-0.5 text-[10px] font-medium opacity-85">내일</span></button>
|
||||
<button
|
||||
onclick={() => rate('애매')}
|
||||
disabled={busy}
|
||||
class="flex flex-col items-center rounded-lg bg-warning py-3.5 text-sm font-bold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>애매<span class="mt-0.5 text-[10px] font-medium opacity-85">내일</span></button>
|
||||
<button
|
||||
onclick={() => rate('암')}
|
||||
disabled={busy}
|
||||
class="flex flex-col items-center rounded-lg bg-success py-3.5 text-sm font-bold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>암<span class="mt-0.5 text-[10px] font-medium opacity-85">{correctLabel(current.review_stage)}</span></button>
|
||||
type="button"
|
||||
onclick={flagCard}
|
||||
disabled={flagBusy || busy}
|
||||
class="flex items-center gap-1 text-[11px] text-faint transition-colors hover:text-warning disabled:opacity-50"
|
||||
title="카드 내용이 이상하면 검수함으로 보냅니다"
|
||||
>
|
||||
<Flag size={12} /> 이 카드 이상해요
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 hidden text-center text-[11px] text-faint sm:block">키보드: J 모름 · K 애매 · L 암</p>
|
||||
{:else}
|
||||
<button
|
||||
onclick={markSeen}
|
||||
disabled={busy}
|
||||
class="mt-3 w-full rounded-lg bg-accent py-3.5 text-sm font-bold text-white transition-colors hover:bg-accent-hover disabled:opacity-50"
|
||||
>봤다 — 다음 <span class="text-xs font-medium opacity-85">(Enter)</span></button>
|
||||
|
||||
<div class="mt-3 text-[10px] font-bold uppercase tracking-wide text-faint">
|
||||
앞 — {current.format === 'qa' ? '질문' : '회상'}
|
||||
</div>
|
||||
<div class="mt-1 text-lg font-semibold leading-relaxed text-text md:mt-2 md:text-2xl">{frontText(current)}</div>
|
||||
|
||||
{#if revealed}
|
||||
<div class="mt-4 rounded-lg border border-accent-ring bg-bg px-4 py-3 md:mt-6">
|
||||
<div class="text-[10px] font-bold uppercase tracking-wide text-faint">정답</div>
|
||||
<div class="mt-0.5 text-xl font-bold text-accent md:text-3xl">{current.fact}</div>
|
||||
{#if current.evidence?.length && current.evidence[0].snippet}
|
||||
<div class="mt-2 text-[11px] leading-relaxed text-dim md:hidden">근거: {current.evidence[0].snippet}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !revealed}
|
||||
<button
|
||||
onclick={() => (revealed = true)}
|
||||
class="mt-auto flex items-center justify-center gap-2 rounded-md border border-dashed border-accent-ring bg-surface-hover py-3 text-sm font-medium text-accent transition-colors hover:bg-accent/5"
|
||||
>
|
||||
<Eye size={16} /> 탭하여 정답 보기 <span class="text-faint">(Space)</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 평가/액션 (reveal 후) -->
|
||||
{#if revealed}
|
||||
{#if mode === 'review'}
|
||||
<div class="mt-3 grid grid-cols-3 gap-2">
|
||||
<button
|
||||
onclick={() => rate('모름')}
|
||||
disabled={busy}
|
||||
class="flex flex-col items-center rounded-lg bg-error py-3.5 text-sm font-bold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>모름<span class="mt-0.5 text-[10px] font-medium opacity-85">내일</span></button>
|
||||
<button
|
||||
onclick={() => rate('애매')}
|
||||
disabled={busy}
|
||||
class="flex flex-col items-center rounded-lg bg-warning py-3.5 text-sm font-bold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>애매<span class="mt-0.5 text-[10px] font-medium opacity-85">내일</span></button>
|
||||
<button
|
||||
onclick={() => rate('암')}
|
||||
disabled={busy}
|
||||
class="flex flex-col items-center rounded-lg bg-success py-3.5 text-sm font-bold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>암<span class="mt-0.5 text-[10px] font-medium opacity-85">{correctLabel(current.review_stage)}</span></button>
|
||||
</div>
|
||||
<p class="mt-2 hidden text-center text-[11px] text-faint sm:block">키보드: Space 정답 · J 모름 · K 애매 · L 암</p>
|
||||
{:else}
|
||||
<button
|
||||
onclick={markSeen}
|
||||
disabled={busy}
|
||||
class="mt-3 w-full rounded-lg bg-accent py-3.5 text-sm font-bold text-white transition-colors hover:bg-accent-hover disabled:opacity-50"
|
||||
>봤다 — 다음 <span class="text-xs font-medium opacity-85">(Enter)</span></button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 우측 근거 (데스크탑 전용; reveal 시 채움, 자리 예약으로 레이아웃 점프 0) -->
|
||||
<div class="hidden md:flex md:flex-col md:self-stretch md:justify-center">
|
||||
{#if revealed && current.evidence?.length && current.evidence[0].snippet}
|
||||
<div class="rounded-lg border border-default bg-surface p-4">
|
||||
<div class="text-[10px] font-bold uppercase tracking-wide text-faint">근거</div>
|
||||
<div class="mt-1.5 text-xs leading-relaxed text-dim">{current.evidence[0].snippet}</div>
|
||||
</div>
|
||||
{:else if revealed}
|
||||
<div class="rounded-lg border border-dashed border-default p-4 text-[11px] leading-relaxed text-faint">확정 풀이에서 추출한 카드 — 별도 인용 없음</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user