feat(ui): 학습 진단(이드 코치) 허브 진입점 + /study/diagnosis 전용 라우트

diagnosis는 cross-topic(사용자 단위) 코칭 표면인데 기존엔 /study/topics 상단에만
노출돼 발견성이 낮았다. 허브(/study)에 '학습 진단' 카드 추가 + 전용 라우트
/study/diagnosis 신설(향후 weekly_recap·review_set_draft 코치 표면의 정식 홈).

패널은 StudyDiagnosisPanel 공유 컴포넌트로 추출 — topics·diagnosis 양쪽이 단일
청크 참조(복붙 drift 0). 백엔드 무변경(기존 POST /diagnosis/generate 재사용).

검증: vite build OK, lint:tokens 내 파일 위반 0, 새 라우트·허브 링크·공유 청크
번들 반영 확인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyungi
2026-06-08 07:22:08 +09:00
parent c8600f8046
commit 279124d953
4 changed files with 125 additions and 67 deletions
@@ -0,0 +1,78 @@
<script>
/**
* 학습 진단 패널 (study_diagnosis surface) — 이드 코치 표면.
*
* 워커(study_weakness)가 산출한 최신 약점 스냅샷을 코치 언어로 번역. 데이터 없으면 status='none'.
* LLM 호출이라 버튼 트리거(자동 호출 X). /study/diagnosis 와 /study/topics 양쪽에서 재사용.
*/
import { api } from '$lib/api';
import { addToast } from '$lib/stores/toast';
import { Activity } from 'lucide-svelte';
import Button from '$lib/components/ui/Button.svelte';
import Card from '$lib/components/ui/Card.svelte';
import Skeleton from '$lib/components/ui/Skeleton.svelte';
import { renderMathMarkdown } from '$lib/utils/mathMarkdown';
let { class: className = '' } = $props();
let diag = $state(null); // StudyDiagnosisResponse | null
let diagLoading = $state(false);
async function generateDiagnosis() {
if (diagLoading) return;
diagLoading = true;
try {
diag = await api('/study-topics/diagnosis/generate', { method: 'POST' });
} catch {
addToast('error', '진단 생성 실패');
} finally {
diagLoading = false;
}
}
function fmtDiagTime(s) {
if (!s) return '';
const d = new Date(s);
if (isNaN(d.getTime())) return '';
return d.toLocaleString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
</script>
<Card class={className}>
{#snippet children()}
<div class="p-3">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 min-w-0">
<Activity size={16} class="text-accent shrink-0" />
<span class="text-sm font-semibold text-text">학습 진단</span>
<span class="text-[11px] text-faint truncate hidden sm:inline">누적 풀이 약점·학습 태도 코치</span>
</div>
<Button onclick={generateDiagnosis} size="sm" variant={diag ? 'ghost' : 'primary'} loading={diagLoading}>
{diag ? '새로고침' : '진단 생성'}
</Button>
</div>
{#if diagLoading}
<div class="mt-3 space-y-2">
<Skeleton w="w-full" h="h-4" /><Skeleton w="w-5/6" h="h-4" /><Skeleton w="w-2/3" h="h-4" />
</div>
{:else if diag && diag.status === 'ready'}
<div class="markdown-body math-area mt-3 text-sm leading-relaxed text-text">{@html renderMathMarkdown(diag.content)}</div>
{#if diag.review_set_draft_id}
<div class="mt-2.5 inline-block text-xs text-accent-hover bg-accent/10 rounded-md px-2.5 py-1.5">
권장 복습세트 초안 #{diag.review_set_draft_id} — 복습함에서 1클릭 확인 후 편성
</div>
{/if}
<div class="mt-2 text-[11px] text-faint">
{#if diag.snapshot_at}스냅샷 {fmtDiagTime(diag.snapshot_at)}{/if}{#if diag.generated_at} · 생성 {fmtDiagTime(diag.generated_at)}{/if}{#if diag.model} · {diag.model}{/if}
</div>
{:else if diag && diag.status === 'none'}
<p class="mt-3 text-xs text-dim leading-relaxed">
아직 진단할 약점 데이터가 없습니다. 학습 주제를 <b class="text-text">공부중</b>으로 표시하면 매일 새벽 누적 풀이에서 약점·태도 스냅샷이 만들어지고, 여기서 진단 코치를 받을 수 있습니다.
</p>
{:else}
<p class="mt-3 text-xs text-dim leading-relaxed">
누적 학습 이력을 근거로 약점 토픽과 학습 태도를 진단합니다. <span class="text-text font-medium">진단 생성</span>을 눌러보세요.
</p>
{/if}
</div>
{/snippet}
</Card>
+12 -1
View File
@@ -3,7 +3,7 @@
// 주제로 보기(퀴즈·복습·통계) / 자료 학습 / 필사 세션 / 암기카드 검수.
import { onMount } from 'svelte';
import { api } from '$lib/api';
import { BookOpen, PenLine, GraduationCap, FolderKanban, Layers, Repeat, Flag, Inbox } from 'lucide-svelte';
import { BookOpen, PenLine, GraduationCap, FolderKanban, Layers, Repeat, Flag, Inbox, Activity } from 'lucide-svelte';
let cardReviewCount = $state(0);
let questionFlagCount = $state(0);
@@ -38,6 +38,17 @@
<p class="text-xs text-dim">"가스기사" 같은 학습 주제 아래에 필기 세션과 자료를 함께 묶어 본다. 한 주제 안에서 필기·자료를 한눈에.</p>
</a>
<a
href="/study/diagnosis"
class="block mb-3 p-5 rounded-lg border border-default bg-surface hover:border-accent hover:bg-accent/5 transition-colors"
>
<div class="flex items-center gap-2 mb-2">
<Activity size={18} class="text-accent" />
<h2 class="text-base font-semibold text-text">학습 진단</h2>
</div>
<p class="text-xs text-dim">누적 풀이 이력에서 약점 토픽과 학습 태도를 코치(이드)가 진단합니다. 매일 새벽 약점 스냅샷을 만들고, 권장 복습세트 초안까지 제안.</p>
</a>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<a
href="/study/sources"
@@ -0,0 +1,31 @@
<script>
/**
* /study/diagnosis — 학습 진단(이드 코치) 전용 페이지.
*
* 누적 풀이 약점·학습 태도를 코치 언어로 진단하는 cross-topic 표면. 허브(/study)에서 진입.
* 패널 본체는 공유 컴포넌트 StudyDiagnosisPanel (/study/topics 상단에도 동일 노출).
*/
import { ArrowLeft, Activity } from 'lucide-svelte';
import StudyDiagnosisPanel from '$lib/components/StudyDiagnosisPanel.svelte';
</script>
<svelte:head><title>학습 진단 — 공부</title></svelte:head>
<div class="p-4 md:p-6 max-w-5xl mx-auto">
<div class="flex items-center gap-2 text-xs md:text-sm mb-3">
<a href="/study" class="text-dim hover:text-text flex items-center gap-1">
<ArrowLeft size={14} /> 공부
</a>
<span class="text-faint">/</span>
<span class="text-text font-medium flex items-center gap-1.5">
<Activity size={14} class="text-accent" /> 학습 진단
</span>
</div>
<header class="mb-4">
<h1 class="text-lg font-semibold text-text">학습 진단</h1>
<p class="text-xs text-dim mt-1">누적 풀이 이력을 근거로 약점 토픽과 학습 태도를 코치가 진단합니다. 약점·수치는 매일 새벽 약점 스냅샷에서만 인용되며, 스냅샷에 없는 토픽은 만들지 않습니다.</p>
</header>
<StudyDiagnosisPanel />
</div>
+4 -66
View File
@@ -13,7 +13,7 @@
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import { addToast } from '$lib/stores/toast';
import { Plus, ArrowLeft, FolderKanban, Trash2, Pencil, Activity } from 'lucide-svelte';
import { Plus, ArrowLeft, FolderKanban, Trash2, Pencil } from 'lucide-svelte';
import Button from '$lib/components/ui/Button.svelte';
import Card from '$lib/components/ui/Card.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
@@ -21,7 +21,7 @@
import TextInput from '$lib/components/ui/TextInput.svelte';
import Select from '$lib/components/ui/Select.svelte';
import Textarea from '$lib/components/ui/Textarea.svelte';
import { renderMathMarkdown } from '$lib/utils/mathMarkdown';
import StudyDiagnosisPanel from '$lib/components/StudyDiagnosisPanel.svelte';
const STUDY_TYPE_OPTIONS = [
{ value: '', label: '미지정' },
@@ -36,29 +36,6 @@
let total = $state(0);
let loading = $state(true);
// ─── 이드 학습 진단 (study_diagnosis surface) ───
// 워커(study_weakness)가 산출한 최신 약점 스냅샷을 코치 언어로 번역. 데이터 없으면 status='none'.
// LLM 호출이라 버튼 트리거(자동 호출 X).
let diag = $state(null); // StudyDiagnosisResponse | null
let diagLoading = $state(false);
async function generateDiagnosis() {
if (diagLoading) return;
diagLoading = true;
try {
diag = await api('/study-topics/diagnosis/generate', { method: 'POST' });
} catch {
addToast('error', '진단 생성 실패');
} finally {
diagLoading = false;
}
}
function fmtDiagTime(s) {
if (!s) return '';
const d = new Date(s);
if (isNaN(d.getTime())) return '';
return d.toLocaleString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
}
// 생성 폼
let formOpen = $state(false);
let f_name = $state('');
@@ -229,47 +206,8 @@
<p class="text-xs text-dim mt-1">한 주제 아래에 필기 세션과 자료를 묶어 보고 진도 관리. 향후 단어장·오디오·문제세트도 같은 묶음으로 연결됩니다.</p>
</header>
<!-- 이드 학습 진단 -->
<Card class="mb-4">
{#snippet children()}
<div class="p-3">
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 min-w-0">
<Activity size={16} class="text-accent shrink-0" />
<span class="text-sm font-semibold text-text">학습 진단</span>
<span class="text-[11px] text-faint truncate hidden sm:inline">누적 풀이 약점·학습 태도 코치</span>
</div>
<Button onclick={generateDiagnosis} size="sm" variant={diag ? 'ghost' : 'primary'} loading={diagLoading}>
{diag ? '새로고침' : '진단 생성'}
</Button>
</div>
{#if diagLoading}
<div class="mt-3 space-y-2">
<Skeleton w="w-full" h="h-4" /><Skeleton w="w-5/6" h="h-4" /><Skeleton w="w-2/3" h="h-4" />
</div>
{:else if diag && diag.status === 'ready'}
<div class="markdown-body math-area mt-3 text-sm leading-relaxed text-text">{@html renderMathMarkdown(diag.content)}</div>
{#if diag.review_set_draft_id}
<div class="mt-2.5 inline-block text-xs text-accent-hover bg-accent/10 rounded-md px-2.5 py-1.5">
권장 복습세트 초안 #{diag.review_set_draft_id} — 복습함에서 1클릭 확인 후 편성
</div>
{/if}
<div class="mt-2 text-[11px] text-faint">
{#if diag.snapshot_at}스냅샷 {fmtDiagTime(diag.snapshot_at)}{/if}{#if diag.generated_at} · 생성 {fmtDiagTime(diag.generated_at)}{/if}{#if diag.model} · {diag.model}{/if}
</div>
{:else if diag && diag.status === 'none'}
<p class="mt-3 text-xs text-dim leading-relaxed">
아직 진단할 약점 데이터가 없습니다. 학습 주제를 <b class="text-text">공부중</b>으로 표시하면 매일 새벽 누적 풀이에서 약점·태도 스냅샷이 만들어지고, 여기서 진단 코치를 받을 수 있습니다.
</p>
{:else}
<p class="mt-3 text-xs text-dim leading-relaxed">
누적 학습 이력을 근거로 약점 토픽과 학습 태도를 진단합니다. <span class="text-text font-medium">진단 생성</span>을 눌러보세요.
</p>
{/if}
</div>
{/snippet}
</Card>
<!-- 이드 학습 진단 (공유 컴포넌트 — /study/diagnosis 와 동일 패널) -->
<StudyDiagnosisPanel class="mb-4" />
<!-- 새 주제 -->
<Card class="mb-4">