feat(library): Phase 2B 문서 상세 facet 편집 + 업로드 facet 전달

FileInfoView에 회사/주제/연도/문서유형 select 4개 추가.
facet 옵션은 /api/library/facets에서 로드, 세션 캐시.
업로드 엔드포인트에 facet Form 파라미터 4개 추가.
업로드 시 현재 선택 facet 자동 전달 + 미리보기 텍스트.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-15 10:27:03 +09:00
parent 776734c897
commit ef9687b0bf
3 changed files with 115 additions and 1 deletions
+8
View File
@@ -422,6 +422,10 @@ async def upload_document(
session: Annotated[AsyncSession, Depends(get_session)],
doc_purpose: str | None = Form(None, description="business | knowledge"),
library_path: str | None = Form(None, description="자료실 경로 (자동 @library/ 태깅)"),
facet_company: str | None = Form(None),
facet_topic: str | None = Form(None),
facet_year: int | None = Form(None),
facet_doctype: str | None = Form(None),
):
"""파일 업로드 → Inbox 저장 + DB 등록 + 처리 큐 등록"""
from core.library import DEFAULT_LIBRARY_PATH, LIBRARY_PREFIX, normalize_library_path
@@ -490,6 +494,10 @@ async def upload_document(
source_channel="manual",
doc_purpose=doc_purpose,
user_tags=[library_tag] if library_tag else [],
facet_company=facet_company or None,
facet_topic=facet_topic or None,
facet_year=facet_year,
facet_doctype=facet_doctype or None,
)
session.add(doc)
await session.flush()
@@ -1,10 +1,14 @@
<script>
// Phase E.1 — 파일 메타 정보 read-only 표시.
// 파일 메타 정보 + facet 편집
import { onMount } from 'svelte';
import { api } from '$lib/api';
import { addToast } from '$lib/stores/toast';
let { doc } = $props();
// facet 옵션 (세션 내 1회 로드)
let facetOptions = $state({ company: [], topic: [], doctype: [], year: [] });
function formatDate(dateStr) {
if (!dateStr) return '-';
return new Date(dateStr).toLocaleDateString('ko-KR', {
@@ -21,6 +25,12 @@
return `${(bytes / 1048576).toFixed(1)}MB`;
}
onMount(async () => {
try {
facetOptions = await api('/library/facets');
} catch { /* 옵션 로딩 실패해도 수동 입력 가능 */ }
});
async function updatePurpose(e) {
const val = e.target.value || null;
const prev = doc.doc_purpose;
@@ -36,6 +46,21 @@
addToast('error', '용도 변경 실패');
}
}
async function updateFacet(field, value) {
const prev = doc[field];
doc[field] = value;
try {
await api(`/documents/${doc.id}`, {
method: 'PATCH',
body: JSON.stringify({ [field]: value }),
});
addToast('success', '변경됨');
} catch {
doc[field] = prev;
addToast('error', '변경 실패');
}
}
</script>
<div>
@@ -82,4 +107,69 @@
<dd class="text-text">{formatDate(doc.created_at)}</dd>
</div>
</dl>
<!-- Facet 메타데이터 -->
<h4 class="text-xs font-semibold text-dim uppercase mt-3 mb-1.5">탐색 축</h4>
<dl class="space-y-1.5 text-xs">
<div class="flex justify-between items-center">
<dt class="text-dim">회사</dt>
<dd>
<select
value={doc.facet_company || ''}
onchange={(e) => updateFacet('facet_company', e.target.value || null)}
class="bg-bg border border-default rounded px-1 py-0.5 text-xs text-text outline-none focus:border-accent"
>
<option value="">-</option>
{#each facetOptions.company || [] as v}
<option value={v}>{v}</option>
{/each}
</select>
</dd>
</div>
<div class="flex justify-between items-center">
<dt class="text-dim">주제</dt>
<dd>
<select
value={doc.facet_topic || ''}
onchange={(e) => updateFacet('facet_topic', e.target.value || null)}
class="bg-bg border border-default rounded px-1 py-0.5 text-xs text-text outline-none focus:border-accent"
>
<option value="">-</option>
{#each facetOptions.topic || [] as v}
<option value={v}>{v}</option>
{/each}
</select>
</dd>
</div>
<div class="flex justify-between items-center">
<dt class="text-dim">연도</dt>
<dd>
<select
value={doc.facet_year ?? ''}
onchange={(e) => updateFacet('facet_year', e.target.value ? Number(e.target.value) : null)}
class="bg-bg border border-default rounded px-1 py-0.5 text-xs text-text outline-none focus:border-accent"
>
<option value="">-</option>
{#each facetOptions.year || [] as v}
<option value={v}>{v}</option>
{/each}
</select>
</dd>
</div>
<div class="flex justify-between items-center">
<dt class="text-dim">문서유형</dt>
<dd>
<select
value={doc.facet_doctype || ''}
onchange={(e) => updateFacet('facet_doctype', e.target.value || null)}
class="bg-bg border border-default rounded px-1 py-0.5 text-xs text-text outline-none focus:border-accent"
>
<option value="">-</option>
{#each facetOptions.doctype || [] as v}
<option value={v}>{v}</option>
{/each}
</select>
</dd>
</div>
</dl>
</div>
+16
View File
@@ -287,6 +287,10 @@
formData.append('file', file);
formData.append('doc_purpose', 'business');
formData.append('library_path', activePath || DEFAULT_LIBRARY_PATH);
if (activeFacetCompany) formData.append('facet_company', activeFacetCompany);
if (activeFacetTopic) formData.append('facet_topic', activeFacetTopic);
if (activeFacetYear) formData.append('facet_year', activeFacetYear);
if (activeFacetDoctype) formData.append('facet_doctype', activeFacetDoctype);
try {
await api('/documents/', { method: 'POST', body: formData });
success++;
@@ -606,6 +610,18 @@
{uploadingCount > 0 ? `업로드 (${uploadingCount})` : '업로드'}
</Button>
<!-- 업로드 시 적용될 facet 미리보기 -->
{#if hasAnyFacet}
<span class="text-[10px] text-faint">
적용: {[
activeFacetCompany && `회사:${activeFacetCompany}`,
activeFacetTopic && `주제:${activeFacetTopic}`,
activeFacetYear && `연도:${activeFacetYear}`,
activeFacetDoctype && `유형:${activeFacetDoctype}`,
].filter(Boolean).join(' / ')}
</span>
{/if}
<!-- 정렬 -->
<select
value={activeSort}