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:
@@ -422,6 +422,10 @@ async def upload_document(
|
|||||||
session: Annotated[AsyncSession, Depends(get_session)],
|
session: Annotated[AsyncSession, Depends(get_session)],
|
||||||
doc_purpose: str | None = Form(None, description="business | knowledge"),
|
doc_purpose: str | None = Form(None, description="business | knowledge"),
|
||||||
library_path: str | None = Form(None, description="자료실 경로 (자동 @library/ 태깅)"),
|
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 등록 + 처리 큐 등록"""
|
"""파일 업로드 → Inbox 저장 + DB 등록 + 처리 큐 등록"""
|
||||||
from core.library import DEFAULT_LIBRARY_PATH, LIBRARY_PREFIX, normalize_library_path
|
from core.library import DEFAULT_LIBRARY_PATH, LIBRARY_PREFIX, normalize_library_path
|
||||||
@@ -490,6 +494,10 @@ async def upload_document(
|
|||||||
source_channel="manual",
|
source_channel="manual",
|
||||||
doc_purpose=doc_purpose,
|
doc_purpose=doc_purpose,
|
||||||
user_tags=[library_tag] if library_tag else [],
|
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)
|
session.add(doc)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
// Phase E.1 — 파일 메타 정보 read-only 표시.
|
// 파일 메타 정보 + facet 편집
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import { addToast } from '$lib/stores/toast';
|
import { addToast } from '$lib/stores/toast';
|
||||||
|
|
||||||
let { doc } = $props();
|
let { doc } = $props();
|
||||||
|
|
||||||
|
// facet 옵션 (세션 내 1회 로드)
|
||||||
|
let facetOptions = $state({ company: [], topic: [], doctype: [], year: [] });
|
||||||
|
|
||||||
function formatDate(dateStr) {
|
function formatDate(dateStr) {
|
||||||
if (!dateStr) return '-';
|
if (!dateStr) return '-';
|
||||||
return new Date(dateStr).toLocaleDateString('ko-KR', {
|
return new Date(dateStr).toLocaleDateString('ko-KR', {
|
||||||
@@ -21,6 +25,12 @@
|
|||||||
return `${(bytes / 1048576).toFixed(1)}MB`;
|
return `${(bytes / 1048576).toFixed(1)}MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
facetOptions = await api('/library/facets');
|
||||||
|
} catch { /* 옵션 로딩 실패해도 수동 입력 가능 */ }
|
||||||
|
});
|
||||||
|
|
||||||
async function updatePurpose(e) {
|
async function updatePurpose(e) {
|
||||||
const val = e.target.value || null;
|
const val = e.target.value || null;
|
||||||
const prev = doc.doc_purpose;
|
const prev = doc.doc_purpose;
|
||||||
@@ -36,6 +46,21 @@
|
|||||||
addToast('error', '용도 변경 실패');
|
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>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -82,4 +107,69 @@
|
|||||||
<dd class="text-text">{formatDate(doc.created_at)}</dd>
|
<dd class="text-text">{formatDate(doc.created_at)}</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</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>
|
</div>
|
||||||
|
|||||||
@@ -287,6 +287,10 @@
|
|||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('doc_purpose', 'business');
|
formData.append('doc_purpose', 'business');
|
||||||
formData.append('library_path', activePath || DEFAULT_LIBRARY_PATH);
|
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 {
|
try {
|
||||||
await api('/documents/', { method: 'POST', body: formData });
|
await api('/documents/', { method: 'POST', body: formData });
|
||||||
success++;
|
success++;
|
||||||
@@ -606,6 +610,18 @@
|
|||||||
{uploadingCount > 0 ? `업로드 중 (${uploadingCount})` : '업로드'}
|
{uploadingCount > 0 ? `업로드 중 (${uploadingCount})` : '업로드'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<!-- 업로드 시 적용될 facet 미리보기 -->
|
||||||
|
{#if hasAnyFacet}
|
||||||
|
<span class="text-[10px] text-faint">
|
||||||
|
적용: {[
|
||||||
|
activeFacetCompany && `회사:${activeFacetCompany}`,
|
||||||
|
activeFacetTopic && `주제:${activeFacetTopic}`,
|
||||||
|
activeFacetYear && `연도:${activeFacetYear}`,
|
||||||
|
activeFacetDoctype && `유형:${activeFacetDoctype}`,
|
||||||
|
].filter(Boolean).join(' / ')}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- 정렬 -->
|
<!-- 정렬 -->
|
||||||
<select
|
<select
|
||||||
value={activeSort}
|
value={activeSort}
|
||||||
|
|||||||
Reference in New Issue
Block a user