feat: implement Phase 4 SvelteKit frontend + backend enhancements
Backend: - Add dashboard API (today stats, inbox count, law alerts, pipeline status) - Add /api/documents/tree endpoint for sidebar domain/sub_group tree - Migrate auth to HttpOnly cookie for refresh token (XSS defense) - Add /api/auth/logout endpoint (cookie cleanup) - Register dashboard router in main.py Frontend (SvelteKit + Tailwind CSS v4): - api.ts: fetch wrapper with refresh queue pattern, 401 single retry, forced logout on refresh failure - Auth store: login/logout/refresh with memory-based access token - UI store: toast system, sidebar state - Login page with TOTP support - Dashboard with 4 stat widgets + recent documents - Document list with hybrid search (debounce, URL query state, mode select) - Document detail with format-aware viewer (markdown/PDF/HWP/Synology/fallback) - Metadata panel (AI summary, tags, processing history) - Inbox triage UI (batch select, confirm dialog, domain override) - Settings page (password change, TOTP status) Infrastructure: - Enable frontend service in docker-compose - Caddy path routing (/api/* → fastapi, / → frontend) + gzip Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,107 @@
|
||||
<script>
|
||||
// TODO: Phase 4에서 대시보드 위젯 구현
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '$lib/api';
|
||||
import { isAuthenticated, user, logout } from '$lib/stores/auth';
|
||||
import { sidebarOpen, addToast } from '$lib/stores/ui';
|
||||
|
||||
let dashboard = null;
|
||||
let loading = true;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
dashboard = await api('/dashboard/');
|
||||
} catch (err) {
|
||||
addToast('error', '대시보드 로딩 실패');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1>hyungi Document Server</h1>
|
||||
<p>PKM 대시보드 — Phase 4에서 구현 예정</p>
|
||||
<div class="min-h-screen">
|
||||
<!-- 상단 네비게이션 -->
|
||||
<nav class="flex items-center justify-between px-6 py-3 border-b border-[var(--border)] bg-[var(--surface)]">
|
||||
<div class="flex items-center gap-4">
|
||||
<button onclick={() => sidebarOpen.update(v => !v)} class="text-[var(--text-dim)] hover:text-[var(--text)]">
|
||||
☰
|
||||
</button>
|
||||
<h1 class="text-lg font-semibold">hyungi Document Server</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/documents" class="text-sm text-[var(--text-dim)] hover:text-[var(--text)]">문서</a>
|
||||
<a href="/inbox" class="text-sm text-[var(--text-dim)] hover:text-[var(--text)]">Inbox</a>
|
||||
<span class="text-sm text-[var(--text-dim)]">{$user?.username}</span>
|
||||
<button onclick={() => { logout(); goto('/login'); }} class="text-sm text-[var(--error)] hover:underline">로그아웃</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section>
|
||||
<h2>시스템 상태</h2>
|
||||
<ul>
|
||||
<li>FastAPI: <a href="/api/health">헬스체크</a></li>
|
||||
<li>API 문서: <a href="/docs">OpenAPI</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<!-- 대시보드 -->
|
||||
<div class="max-w-6xl mx-auto p-6">
|
||||
<h2 class="text-xl font-bold mb-6">대시보드</h2>
|
||||
|
||||
{#if loading}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{#each Array(4) as _}
|
||||
<div class="bg-[var(--surface)] rounded-xl p-5 border border-[var(--border)] animate-pulse h-28"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if dashboard}
|
||||
<!-- 위젯 그리드 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<!-- 전체 문서 -->
|
||||
<div class="bg-[var(--surface)] rounded-xl p-5 border border-[var(--border)]">
|
||||
<p class="text-sm text-[var(--text-dim)]">전체 문서</p>
|
||||
<p class="text-3xl font-bold mt-1">{dashboard.total_documents}</p>
|
||||
<p class="text-xs text-[var(--text-dim)] mt-1">오늘 +{dashboard.today_added}</p>
|
||||
</div>
|
||||
|
||||
<!-- Inbox -->
|
||||
<div class="bg-[var(--surface)] rounded-xl p-5 border border-[var(--border)]">
|
||||
<p class="text-sm text-[var(--text-dim)]">Inbox 미분류</p>
|
||||
<p class="text-3xl font-bold mt-1" class:text-[var(--warning)]={dashboard.inbox_count > 0}>{dashboard.inbox_count}</p>
|
||||
{#if dashboard.inbox_count > 0}
|
||||
<a href="/inbox" class="text-xs text-[var(--accent)] hover:underline mt-1 inline-block">분류하기</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- 법령 알림 -->
|
||||
<div class="bg-[var(--surface)] rounded-xl p-5 border border-[var(--border)]">
|
||||
<p class="text-sm text-[var(--text-dim)]">법령 알림</p>
|
||||
<p class="text-3xl font-bold mt-1">{dashboard.law_alerts}</p>
|
||||
<p class="text-xs text-[var(--text-dim)] mt-1">오늘 변경</p>
|
||||
</div>
|
||||
|
||||
<!-- 파이프라인 -->
|
||||
<div class="bg-[var(--surface)] rounded-xl p-5 border border-[var(--border)]">
|
||||
<p class="text-sm text-[var(--text-dim)]">파이프라인</p>
|
||||
{#if dashboard.failed_count > 0}
|
||||
<p class="text-3xl font-bold mt-1 text-[var(--error)]">{dashboard.failed_count} 실패</p>
|
||||
{:else}
|
||||
<p class="text-3xl font-bold mt-1 text-[var(--success)]">정상</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 최근 문서 -->
|
||||
<div class="bg-[var(--surface)] rounded-xl border border-[var(--border)] p-5">
|
||||
<h3 class="text-sm font-semibold text-[var(--text-dim)] mb-3">최근 문서</h3>
|
||||
{#if dashboard.recent_documents.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each dashboard.recent_documents as doc}
|
||||
<a href="/documents/{doc.id}" class="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-[var(--bg)] transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs px-2 py-0.5 rounded bg-[var(--border)] text-[var(--text-dim)] uppercase">{doc.file_format}</span>
|
||||
<span class="text-sm truncate max-w-md">{doc.title || '제목 없음'}</span>
|
||||
</div>
|
||||
<span class="text-xs text-[var(--text-dim)]">{doc.ai_domain || ''}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-[var(--text-dim)]">문서가 없습니다</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user