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:
89
frontend/src/routes/settings/+page.svelte
Normal file
89
frontend/src/routes/settings/+page.svelte
Normal file
@@ -0,0 +1,89 @@
|
||||
<script>
|
||||
import { api } from '$lib/api';
|
||||
import { addToast } from '$lib/stores/ui';
|
||||
import { user } from '$lib/stores/auth';
|
||||
|
||||
let currentPassword = '';
|
||||
let newPassword = '';
|
||||
let confirmPassword = '';
|
||||
let changing = false;
|
||||
|
||||
async function changePassword() {
|
||||
if (newPassword !== confirmPassword) {
|
||||
addToast('error', '새 비밀번호가 일치하지 않습니다');
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 8) {
|
||||
addToast('error', '비밀번호는 8자 이상이어야 합니다');
|
||||
return;
|
||||
}
|
||||
|
||||
changing = true;
|
||||
try {
|
||||
await api('/auth/change-password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
}),
|
||||
});
|
||||
addToast('success', '비밀번호가 변경되었습니다');
|
||||
currentPassword = '';
|
||||
newPassword = '';
|
||||
confirmPassword = '';
|
||||
} catch (err) {
|
||||
addToast('error', err.detail || '비밀번호 변경 실패');
|
||||
} finally {
|
||||
changing = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen">
|
||||
<nav class="flex items-center gap-2 px-6 py-3 border-b border-[var(--border)] bg-[var(--surface)] text-sm">
|
||||
<a href="/" class="text-[var(--text-dim)] hover:text-[var(--text)]">PKM</a>
|
||||
<span class="text-[var(--text-dim)]">/</span>
|
||||
<span>설정</span>
|
||||
</nav>
|
||||
|
||||
<div class="max-w-lg mx-auto p-6">
|
||||
<!-- 계정 정보 -->
|
||||
<div class="bg-[var(--surface)] rounded-xl border border-[var(--border)] p-5 mb-6">
|
||||
<h2 class="text-lg font-semibold mb-3">계정 정보</h2>
|
||||
<dl class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-[var(--text-dim)]">아이디</dt>
|
||||
<dd>{$user?.username}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-[var(--text-dim)]">2FA (TOTP)</dt>
|
||||
<dd class={$user?.totp_enabled ? 'text-[var(--success)]' : 'text-[var(--text-dim)]'}>
|
||||
{$user?.totp_enabled ? '활성' : '비활성'}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- 비밀번호 변경 -->
|
||||
<div class="bg-[var(--surface)] rounded-xl border border-[var(--border)] p-5">
|
||||
<h2 class="text-lg font-semibold mb-3">비밀번호 변경</h2>
|
||||
<form onsubmit={(e) => { e.preventDefault(); changePassword(); }} class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm text-[var(--text-dim)] mb-1">현재 비밀번호</label>
|
||||
<input type="password" bind:value={currentPassword} class="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-[var(--text)] outline-none focus:border-[var(--accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-[var(--text-dim)] mb-1">새 비밀번호</label>
|
||||
<input type="password" bind:value={newPassword} class="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-[var(--text)] outline-none focus:border-[var(--accent)]" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-[var(--text-dim)] mb-1">새 비밀번호 확인</label>
|
||||
<input type="password" bind:value={confirmPassword} class="w-full px-3 py-2 bg-[var(--bg)] border border-[var(--border)] rounded-lg text-[var(--text)] outline-none focus:border-[var(--accent)]" />
|
||||
</div>
|
||||
<button type="submit" disabled={changing} class="w-full py-2.5 bg-[var(--accent)] text-white rounded-lg disabled:opacity-50">
|
||||
{changing ? '변경 중...' : '비밀번호 변경'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user