feat(study): Phase 2-A 풀이 시작 UI — 학습 단계 + 분량 토글

vision 의 단일 풀이 진입점 — 옵션 토글로 학습 단계 (입문/학습 중/시험 직전) +
분량 (30/50/100) 선택. Phase 1-E bucket+stage 알고리즘과 매칭.

- 학습 단계 3 카드 + 분량 3 토글이 메인 옵션
- 단계 선택 시 분량 토글 노출
- 단계 미선택 시 "고급 옵션" collapsible — 기존 PR-12-B subject 단위 출제 호환
- 시작 버튼 disabled 상태 가이드 (단계 선택 또는 고급 옵션 펼침 필요)

서버 호출:
- optStage 있으면 { stage, size, abandon_existing } body
- 없으면 기존 { target_per_subject, subject, wrong_only, abandon_existing }

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-05-01 09:39:04 +09:00
parent d038f11444
commit c46fd564af
@@ -28,6 +28,12 @@
// 진입 화면 옵션 (start 모드용)
let mode = $state('start'); // 'start' | 'playing'
// Phase 2-A: 학습 단계 + 분량 (Phase 1-E bucket+stage 알고리즘 입력)
// null = 기본 (subject bucket 모드, 기존 PR-12-B 동작)
let optStage = $state(null); // null | 'intro' | 'learning' | 'pre_exam'
let optSize = $state(50); // 30 / 50 / 100
let advancedOpen = $state(false);
// 기존 옵션 (advanced — stage 미선택 시만 사용)
let optSubject = $state('');
let optWrongOnly = $state(false);
let optTarget = $state(20);
@@ -89,18 +95,27 @@
}
});
/** 'start' 모드에서 [시작] 클릭 → 신규 세션 생성 후 ?session=N 으로 이동. */
/** 'start' 모드에서 [시작] 클릭 → 신규 세션 생성 후 ?session=N 으로 이동.
* optStage 가 설정되면 Phase 1-E bucket+stage 알고리즘으로 출제 (단일 풀이 진입점).
* 미설정이면 기존 subject bucket + spacing 경로 (advanced 옵션 호환). */
async function start() {
loading = true;
try {
const body = optStage
? {
stage: optStage,
size: optSize,
abandon_existing: false,
}
: {
target_per_subject: optTarget,
subject: optSubject.trim() || null,
wrong_only: optWrongOnly,
abandon_existing: false,
};
const res = await api(`/study-topics/${topicId}/quiz-sessions`, {
method: 'POST',
body: JSON.stringify({
target_per_subject: optTarget,
subject: optSubject.trim() || null,
wrong_only: optWrongOnly,
abandon_existing: false, // start 화면에서는 in_progress 있으면 그쪽으로 이어감.
}),
body: JSON.stringify(body),
});
goto(`/study/topics/${topicId}/review?session=${res.id}`, { replaceState: true });
// loadSession 은 navigate 후 onMount 가 다시 안 트리거되므로 직접 호출.
@@ -171,38 +186,93 @@
<div class="p-5 flex flex-col gap-4">
<h1 class="text-base font-semibold text-text">문제풀이</h1>
<p class="text-xs text-dim">
기본은 과목별 {optTarget}문제씩 무작위 균등 추출. 한 과목이 부족하면 가용한 만큼만 출제됩니다.
학습 단계와 분량을 선택하세요. 단계에 맞춰 안 푼 문제, 오답, 복습 예정, 빈출 유형이 자동으로 섞여 출제됩니다.
풀이 중 정답·해설은 표시하지 않으며, 다 풀면 결과 화면에서 카테고리별로 한 번에 확인합니다.
나갔다 와도 같은 위치에서 이어풀 수 있습니다.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<TextInput label="과목 (선택, 비워두면 전체 균등)" bind:value={optSubject} placeholder="예: 연소공학" />
</div>
<div>
<label class="flex flex-col gap-1.5">
<span class="text-xs text-dim">과목당 문제 수</span>
<input
type="number"
bind:value={optTarget}
min="1"
max="100"
class="px-3 py-2 bg-surface border border-default rounded text-sm text-text outline-none focus:border-accent"
/>
</label>
<!-- 학습 단계 -->
<div class="flex flex-col gap-1.5">
<span class="text-xs text-dim">학습 단계</span>
<div class="grid grid-cols-3 gap-1.5">
{#each [
{ v: 'intro', label: '입문', desc: '안 푼 문제 위주' },
{ v: 'learning', label: '학습 중', desc: '오답·복습·신규 균형' },
{ v: 'pre_exam', label: '시험 직전', desc: '실전 + 약점 보강' },
] as opt (opt.v)}
<button
type="button"
onclick={() => (optStage = opt.v)}
class="p-2 rounded border text-left transition-colors
{optStage === opt.v ? 'border-accent bg-accent/10 text-text' : 'border-default bg-surface text-dim hover:border-accent/40'}"
>
<div class="text-xs font-semibold">{opt.label}</div>
<div class="text-[10px] mt-0.5">{opt.desc}</div>
</button>
{/each}
</div>
</div>
<label class="flex items-center gap-2 text-xs text-text cursor-pointer">
<input type="checkbox" bind:checked={optWrongOnly} />
<span>오답만 (가장 최근 attempt 가 오답인 문제만)</span>
</label>
<!-- 분량 -->
{#if optStage}
<div class="flex flex-col gap-1.5">
<span class="text-xs text-dim">분량</span>
<div class="grid grid-cols-3 gap-1.5">
{#each [30, 50, 100] as size (size)}
<button
type="button"
onclick={() => (optSize = size)}
class="p-2 rounded border text-center transition-colors
{optSize === size ? 'border-accent bg-accent/10 text-text' : 'border-default bg-surface text-dim hover:border-accent/40'}"
>
<div class="text-xs font-semibold">{size}문제</div>
</button>
{/each}
</div>
</div>
{/if}
<!-- 고급 옵션 (단계 미선택 시만 활성) -->
{#if !optStage}
<button
type="button"
onclick={() => (advancedOpen = !advancedOpen)}
class="text-xs text-dim hover:text-text text-left"
>
{advancedOpen ? '▾' : '▸'} 고급 옵션 (단계 미선택 시 — 과목 단위 균등 출제)
</button>
{#if advancedOpen}
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 pl-3 border-l-2 border-default">
<div>
<TextInput label="과목 (선택, 비워두면 전체 균등)" bind:value={optSubject} placeholder="예: 연소공학" />
</div>
<div>
<label class="flex flex-col gap-1.5">
<span class="text-xs text-dim">과목당 문제 수</span>
<input
type="number"
bind:value={optTarget}
min="1"
max="100"
class="px-3 py-2 bg-surface border border-default rounded text-sm text-text outline-none focus:border-accent"
/>
</label>
</div>
<label class="flex items-center gap-2 text-xs text-text cursor-pointer md:col-span-2">
<input type="checkbox" bind:checked={optWrongOnly} />
<span>오답만 (가장 최근 attempt 가 오답인 문제만)</span>
</label>
</div>
{/if}
{/if}
<div class="flex gap-2 justify-end">
<Button href={`/study/topics/${topicId}`} variant="ghost" icon={ArrowLeft}>주제로</Button>
<Button onclick={start} icon={Play}>시작</Button>
<Button onclick={start} icon={Play} disabled={!optStage && !advancedOpen}>시작</Button>
</div>
{#if !optStage && !advancedOpen}
<p class="text-[11px] text-dim text-right">학습 단계를 선택하거나 고급 옵션으로 시작</p>
{/if}
</div>
{/snippet}
</Card>