From 776734c89716b432b51a820af576e707bbcd17db Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Wed, 15 Apr 2026 10:25:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(library):=20Phase=202B=20facet=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=ED=8C=A8=EB=84=90=20+=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 자료실 좌측에 회사/주제/연도/문서유형 facet pill 패널 추가. single-select 토글, count 표시, 교차 필터 (자기 축 제외). URL searchParams 기반 상태 관리 (뒤로가기/새로고침 유지). loadDocs에 facet 파라미터 전달, loadFacetCounts 분리 (page/sort 제외). count 0은 dim+disabled, 초기화 버튼 포함. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/routes/library/+page.svelte | 112 ++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/library/+page.svelte b/frontend/src/routes/library/+page.svelte index 7e1a8f4..b9890d0 100644 --- a/frontend/src/routes/library/+page.svelte +++ b/frontend/src/routes/library/+page.svelte @@ -52,6 +52,19 @@ const ODF_FORMATS = ['ods', 'odt', 'odp', 'odoc', 'osheet']; const DEFAULT_LIBRARY_PATH = '미분류'; const MAX_DEPTH = 5; + const FACET_LABELS = { company: '회사', topic: '주제', year: '연도', doctype: '문서유형' }; + const FACET_KEYS = ['facet_company', 'facet_topic', 'facet_year', 'facet_doctype']; + + // ─── Facet 상태 ─── + + let facetCounts = $state({ company: [], topic: [], year: [], doctype: [] }); + let facetLoading = $state(false); + + let activeFacetCompany = $derived($page.url.searchParams.get('facet_company')); + let activeFacetTopic = $derived($page.url.searchParams.get('facet_topic')); + let activeFacetYear = $derived($page.url.searchParams.get('facet_year')); + let activeFacetDoctype = $derived($page.url.searchParams.get('facet_doctype')); + let hasAnyFacet = $derived(activeFacetCompany || activeFacetTopic || activeFacetYear || activeFacetDoctype); // ─── 카테고리 CRUD 상태 ─── @@ -90,6 +103,10 @@ if (activePath) params.set('path', activePath); if (activeSort) params.set('sort', activeSort); if (activeQ) params.set('q', activeQ); + if (activeFacetCompany) params.set('facet_company', activeFacetCompany); + if (activeFacetTopic) params.set('facet_topic', activeFacetTopic); + if (activeFacetYear) params.set('facet_year', activeFacetYear); + if (activeFacetDoctype) params.set('facet_doctype', activeFacetDoctype); params.set('page', String(activePage)); params.set('page_size', '20'); const result = await api(`/documents/library?${params}`); @@ -102,17 +119,42 @@ } } + async function loadFacetCounts() { + facetLoading = true; + try { + const params = new URLSearchParams(); + if (activePath) params.set('library_path', activePath); + if (activeQ) params.set('q', activeQ); + if (activeFacetCompany) params.set('facet_company', activeFacetCompany); + if (activeFacetTopic) params.set('facet_topic', activeFacetTopic); + if (activeFacetYear) params.set('facet_year', activeFacetYear); + if (activeFacetDoctype) params.set('facet_doctype', activeFacetDoctype); + facetCounts = await api(`/library/facet-counts?${params}`); + } catch { + /* facet 실패해도 문서 목록은 정상 */ + } finally { + facetLoading = false; + } + } + onMount(() => { loadTree(); }); - // URL 파라미터 변경 시 문서 목록 재로드 + // 문서 목록: 모든 URL 파라미터 변경 시 재로드 $effect(() => { // eslint-disable-next-line no-unused-expressions - activePath, activeSort, activeQ, activePage; + activePath, activeSort, activeQ, activePage, activeFacetCompany, activeFacetTopic, activeFacetYear, activeFacetDoctype; loadDocs(); }); + // facet counts: path/q/facet 변경 시만 재집계 (page/sort 제외) + $effect(() => { + // eslint-disable-next-line no-unused-expressions + activePath, activeQ, activeFacetCompany, activeFacetTopic, activeFacetYear, activeFacetDoctype; + loadFacetCounts(); + }); + // 선택된 경로의 부모 자동 펼치기 $effect(() => { if (activePath) { @@ -159,6 +201,25 @@ goto(`/library?${params}`, { noScroll: true }); } + function toggleFacet(key, value) { + const params = new URLSearchParams($page.url.searchParams); + const current = params.get(key); + if (current === String(value)) { + params.delete(key); + } else { + params.set(key, String(value)); + } + params.delete('page'); + goto(`/library?${params}`, { noScroll: true }); + } + + function clearAllFacets() { + const params = new URLSearchParams($page.url.searchParams); + for (const k of FACET_KEYS) params.delete(k); + params.delete('page'); + goto(`/library?${params}`, { noScroll: true }); + } + function toggleExpand(path) { expanded[path] = !expanded[path]; } @@ -480,6 +541,53 @@ {/if} + + + {#if facetCounts.company.length > 0 || facetCounts.topic.length > 0 || facetCounts.year.length > 0 || facetCounts.doctype.length > 0} +
+
+

탐색 축

+ {#if hasAnyFacet} + + {/if} +
+ + {#each Object.entries(FACET_LABELS) as [key, label]} + {@const items = facetCounts[key] || []} + {@const activeValue = key === 'company' ? activeFacetCompany : key === 'topic' ? activeFacetTopic : key === 'year' ? activeFacetYear : activeFacetDoctype} + {@const paramKey = `facet_${key}`} + {#if items.length > 0} +
+

{label}

+
+ {#each items as item} + {@const isActive = activeValue === String(item.value)} + {@const isDisabled = item.count === 0 && !isActive} + + {/each} +
+
+ {/if} + {/each} +
+ {/if}