diff --git a/frontend/src/routes/documents/+page.svelte b/frontend/src/routes/documents/+page.svelte
index 7dcbbf0..982ebd6 100644
--- a/frontend/src/routes/documents/+page.svelte
+++ b/frontend/src/routes/documents/+page.svelte
@@ -3,7 +3,7 @@
import { goto } from '$app/navigation';
import { api } from '$lib/api';
import { addToast } from '$lib/stores/toast';
- import { Info, List, LayoutGrid, ChevronLeft } from 'lucide-svelte';
+ import { Info, List, LayoutGrid, ChevronLeft, X, Plus } from 'lucide-svelte';
import DocumentCard from '$lib/components/DocumentCard.svelte';
import DocumentTable from '$lib/components/DocumentTable.svelte';
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
@@ -13,6 +13,10 @@
import { ui } from '$lib/stores/uiState.svelte';
import { useIsXl } from '$lib/composables/useMedia.svelte';
+ // D.2: 필터 칩에서 사용할 format 화이트리스트.
+ // 백엔드 `GET /documents/?format=...` 파라미터가 이미 받음.
+ const FORMATS = ['pdf', 'hwp', 'hwpx', 'md', 'docx', 'xlsx', 'png', 'jpg'];
+
// 뷰 모드 (localStorage 기억)
let viewMode = $state(typeof localStorage !== 'undefined' ? (localStorage.getItem('viewMode') || 'card') : 'card');
function toggleViewMode() {
@@ -75,6 +79,11 @@
let filterSubGroup = $derived($page.url.searchParams.get('sub_group') || '');
let filterTag = $derived($page.url.searchParams.get('tag') || '');
let filterSource = $derived($page.url.searchParams.get('source') || '');
+ let filterFormat = $derived($page.url.searchParams.get('format') || '');
+
+ // D.2: 필터 칩 popover 상태
+ let tagPopoverOpen = $state(false);
+ let formatPopoverOpen = $state(false);
$effect(() => {
const _p = currentPage;
@@ -82,6 +91,7 @@
const _s = filterSubGroup;
const _t = filterTag;
const _src = filterSource;
+ const _f = filterFormat;
const urlQ = $page.url.searchParams.get('q') || '';
const urlMode = $page.url.searchParams.get('mode') || 'hybrid';
@@ -108,6 +118,7 @@
if (filterSubGroup) params.set('sub_group', filterSubGroup);
if (filterTag) params.set('tag', filterTag);
if (filterSource) params.set('source', filterSource);
+ if (filterFormat) params.set('format', filterFormat);
const data = await api(`/documents/?${params}`);
documents = data.items;
@@ -176,6 +187,26 @@
searchQuery = '';
}
+ // D.2: 필터 add/remove 헬퍼 — URL 라운드트립으로 필터 상태 관리.
+ function addFilter(key, value) {
+ const params = new URLSearchParams($page.url.searchParams);
+ params.set(key, value);
+ params.delete('page');
+ const qs = params.toString();
+ goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true });
+ tagPopoverOpen = false;
+ formatPopoverOpen = false;
+ }
+
+ function removeFilter(key) {
+ const params = new URLSearchParams($page.url.searchParams);
+ params.delete(key);
+ // domain 제거 시 sub_group도 함께 제거 (의존성)
+ if (key === 'domain') params.delete('sub_group');
+ const qs = params.toString();
+ goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true });
+ }
+
function selectDoc(doc) {
selectedDoc = selectedDoc?.id === doc.id ? null : doc;
}
@@ -188,7 +219,37 @@
let totalPages = $derived(Math.ceil(total / 20));
let items = $derived(searchResults || documents);
- let hasActiveFilters = $derived(!!filterDomain || !!filterSubGroup || !!filterTag || !!filterSource || !!searchQuery);
+ let hasActiveFilters = $derived(
+ !!filterDomain || !!filterSubGroup || !!filterTag || !!filterSource || !!filterFormat || !!searchQuery
+ );
+
+ // D.2: 현재 결과 집계 — 상위 20개 태그 (클라이언트 집계, 백엔드 변경 없음).
+ let topTags = $derived.by(() => {
+ const counts = new Map();
+ for (const d of items) {
+ for (const t of d.ai_tags || []) {
+ counts.set(t, (counts.get(t) || 0) + 1);
+ }
+ }
+ return [...counts.entries()]
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 20);
+ });
+
+ // 바깥 클릭으로 popover 닫기
+ $effect(() => {
+ if (!tagPopoverOpen && !formatPopoverOpen) return;
+ function onDocClick(e) {
+ const target = e.target;
+ if (!(target instanceof Element)) return;
+ if (!target.closest('[data-popover-root]')) {
+ tagPopoverOpen = false;
+ formatPopoverOpen = false;
+ }
+ }
+ document.addEventListener('click', onDocClick);
+ return () => document.removeEventListener('click', onDocClick);
+ });
현재 결과에 태그 없음
+ {:else} + {#each topTags as [tag, count]} + + {/each} + {/if} +