feat(ui): Phase D.6~D.7 — AnalysisPanel + 문서 상세 통합
D.6: AnalysisPanel 컴포넌트 — 기본 접힌 상태 + '이 문서 분석' 버튼
- POST /documents/{id}/analyze 호출
- docId 변경 시 state 완전 리셋 ($effect)
- 층별 렌더 (근거/해설/사례/요약, 없는 층 생략)
- 에러 통일 문구 + 재시도/재분석 버튼
D.7: 문서 상세 페이지 우측 editors stack에 Card 래핑으로 삽입
- AIClassificationEditor 다음, FileInfoView 이전
- DocumentViewer / PreviewPanel 변경 없음
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
<!--
|
||||
AnalysisPanel.svelte — 문서 상세 페이지 "이드 분석" 패널 (Phase D.6).
|
||||
|
||||
기본 접힌 상태 + "이 문서 분석" 버튼.
|
||||
클릭 시 POST /api/documents/{docId}/analyze 호출 후 층별 결과 표시.
|
||||
docId 변경 시 state 완전 리셋 ($effect).
|
||||
|
||||
상세 페이지의 editors stack에서 <Card>로 래핑되어 사용됨 (D.7).
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api';
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import Skeleton from '$lib/components/ui/Skeleton.svelte';
|
||||
import { Sparkles, RotateCcw } from 'lucide-svelte';
|
||||
import type { AnalyzeResponse } from '$lib/types/analyze';
|
||||
|
||||
interface Props {
|
||||
docId: number;
|
||||
}
|
||||
|
||||
let { docId }: Props = $props();
|
||||
|
||||
let data = $state<AnalyzeResponse | null>(null);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// docId 변경 시 state 완전 리셋
|
||||
// (프론트 캐시: 같은 docId 재방문 시 재호출 방지. 서버 캐시와 독립)
|
||||
$effect(() => {
|
||||
const _id = docId;
|
||||
data = null;
|
||||
loading = false;
|
||||
error = null;
|
||||
});
|
||||
|
||||
async function runAnalysis() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
data = await api<AnalyzeResponse>(`/documents/${docId}/analyze`, {
|
||||
method: 'POST',
|
||||
});
|
||||
} catch {
|
||||
error = '분석 결과를 정리하지 못했습니다. 다시 시도해 주세요.';
|
||||
data = null;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-2 mb-1.5">
|
||||
<h4 class="text-xs font-semibold text-dim uppercase flex items-center gap-1">
|
||||
<Sparkles size={12} class="text-accent" />
|
||||
이드 분석
|
||||
</h4>
|
||||
{#if data?.cached}
|
||||
<Badge tone="neutral" size="sm">캐시</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !data && !loading && !error}
|
||||
<!-- 기본 접힌 상태: 버튼만 -->
|
||||
<Button variant="secondary" size="sm" icon={Sparkles} onclick={runAnalysis}>
|
||||
이 문서 분석
|
||||
</Button>
|
||||
<p class="mt-1.5 text-[10px] text-dim">
|
||||
약 10초 소요. 문서 전문을 Gemma 4로 구조화합니다.
|
||||
</p>
|
||||
{:else if loading}
|
||||
<div class="space-y-2">
|
||||
<Skeleton w="w-20" h="h-3" />
|
||||
<Skeleton w="w-full" h="h-12" />
|
||||
<Skeleton w="w-20" h="h-3" />
|
||||
<Skeleton w="w-full" h="h-12" />
|
||||
</div>
|
||||
<p class="mt-2 text-[10px] text-dim flex items-center gap-1.5">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full border-2 border-dim border-t-accent animate-spin"></span>
|
||||
분석 중…
|
||||
</p>
|
||||
{:else if error}
|
||||
<p class="text-xs text-error mb-2">{error}</p>
|
||||
<Button variant="ghost" size="sm" icon={RotateCcw} onclick={runAnalysis}>
|
||||
다시 시도
|
||||
</Button>
|
||||
{:else if data}
|
||||
<div class="space-y-3">
|
||||
{#each data.layers as layer}
|
||||
<div>
|
||||
<p class="text-[10px] font-medium text-accent uppercase tracking-wider mb-1">
|
||||
{layer.title}
|
||||
</p>
|
||||
<p class="text-xs text-text leading-relaxed whitespace-pre-wrap">
|
||||
{layer.content}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if data.truncated}
|
||||
<p class="mt-2.5 text-[10px] text-dim">
|
||||
원문이 15,000자를 초과하여 앞부분만 분석했습니다.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="mt-2.5 flex items-center justify-between">
|
||||
<span class="text-[10px] text-dim">
|
||||
{Math.round(data.elapsed_ms)}ms · {data.layers.length}개 층
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={runAnalysis}
|
||||
class="text-[10px] text-dim hover:text-text flex items-center gap-0.5"
|
||||
title="다시 분석"
|
||||
>
|
||||
<RotateCcw size={10} /> 재분석
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -22,6 +22,7 @@
|
||||
import ProcessingStatusView from '$lib/components/editors/ProcessingStatusView.svelte';
|
||||
import LibraryPathEditor from '$lib/components/editors/LibraryPathEditor.svelte';
|
||||
import DocumentDangerZone from '$lib/components/editors/DocumentDangerZone.svelte';
|
||||
import AnalysisPanel from '$lib/components/AnalysisPanel.svelte';
|
||||
|
||||
marked.use({ mangle: false, headerIds: false });
|
||||
function renderMd(text) {
|
||||
@@ -244,6 +245,9 @@
|
||||
<Card>
|
||||
<AIClassificationEditor {doc} />
|
||||
</Card>
|
||||
<Card>
|
||||
<AnalysisPanel docId={doc.id} />
|
||||
</Card>
|
||||
<Card>
|
||||
<FileInfoView {doc} />
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user