refactor: preview 병렬 트리거 + 파일 이동 제거 + domain 색상 바
- queue_consumer: extract 완료 시 classify + preview 동시 등록 - classify_worker: _move_to_knowledge() 제거, 파일 원본 위치 유지 - DocumentCard: 좌측 domain별 색상 바 (4px) 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
"""AI 분류 워커 — Qwen3.5로 도메인/태그/요약 생성 + Inbox→Knowledge 이동"""
|
"""AI 분류 워커 — Qwen3.5로 도메인/태그/요약 생성 + Inbox→Knowledge 이동"""
|
||||||
|
|
||||||
import shutil
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -70,9 +69,7 @@ async def process(document_id: int, session: AsyncSession) -> None:
|
|||||||
doc.ai_model_version = "qwen3.5-35b-a3b"
|
doc.ai_model_version = "qwen3.5-35b-a3b"
|
||||||
doc.ai_processed_at = datetime.now(timezone.utc)
|
doc.ai_processed_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
# ─── Inbox → Knowledge 폴더 이동 ───
|
# 파일은 원본 위치 유지 (물리 이동 없음, DB 메타데이터만 관리)
|
||||||
if doc.file_path.startswith("PKM/Inbox/") and domain:
|
|
||||||
_move_to_knowledge(doc, domain)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[분류] document_id={document_id}: "
|
f"[분류] document_id={document_id}: "
|
||||||
@@ -83,33 +80,4 @@ async def process(document_id: int, session: AsyncSession) -> None:
|
|||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
def _move_to_knowledge(doc: Document, domain: str):
|
# _move_to_knowledge 제거됨 — 파일은 원본 위치 유지, 분류는 DB 메타데이터만
|
||||||
"""분류 완료 후 Inbox에서 Knowledge 폴더로 파일 이동"""
|
|
||||||
nas_root = Path(settings.nas_mount_path)
|
|
||||||
src = nas_root / doc.file_path
|
|
||||||
|
|
||||||
if not src.exists():
|
|
||||||
logger.warning(f"[이동] 원본 파일 없음: {src}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 대상 경로: PKM/{domain}/{파일명}
|
|
||||||
sub_group = doc.ai_sub_group
|
|
||||||
if sub_group:
|
|
||||||
new_rel = f"PKM/{domain}/{sub_group}/{src.name}"
|
|
||||||
else:
|
|
||||||
new_rel = f"PKM/{domain}/{src.name}"
|
|
||||||
|
|
||||||
dst = nas_root / new_rel
|
|
||||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# 중복 파일명 처리
|
|
||||||
counter = 1
|
|
||||||
stem, suffix = dst.stem, dst.suffix
|
|
||||||
while dst.exists():
|
|
||||||
dst = dst.parent / f"{stem}_{counter}{suffix}"
|
|
||||||
new_rel = str(dst.relative_to(nas_root))
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
shutil.move(str(src), str(dst))
|
|
||||||
doc.file_path = new_rel
|
|
||||||
logger.info(f"[이동] {doc.file_path} → {new_rel}")
|
|
||||||
|
|||||||
@@ -34,27 +34,28 @@ async def reset_stale_items():
|
|||||||
|
|
||||||
async def enqueue_next_stage(document_id: int, current_stage: str):
|
async def enqueue_next_stage(document_id: int, current_stage: str):
|
||||||
"""현재 stage 완료 후 다음 stage를 pending으로 등록"""
|
"""현재 stage 완료 후 다음 stage를 pending으로 등록"""
|
||||||
next_stages = {"extract": "classify", "classify": "embed", "embed": "preview"}
|
next_stages = {"extract": ["classify", "preview"], "classify": ["embed"]}
|
||||||
next_stage = next_stages.get(current_stage)
|
stages = next_stages.get(current_stage, [])
|
||||||
if not next_stage:
|
if not stages:
|
||||||
return
|
return
|
||||||
|
|
||||||
async with async_session() as session:
|
async with async_session() as session:
|
||||||
existing = await session.execute(
|
for next_stage in stages:
|
||||||
select(ProcessingQueue).where(
|
existing = await session.execute(
|
||||||
ProcessingQueue.document_id == document_id,
|
select(ProcessingQueue).where(
|
||||||
ProcessingQueue.stage == next_stage,
|
ProcessingQueue.document_id == document_id,
|
||||||
ProcessingQueue.status.in_(["pending", "processing"]),
|
ProcessingQueue.stage == next_stage,
|
||||||
|
ProcessingQueue.status.in_(["pending", "processing"]),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
if existing.scalar_one_or_none():
|
||||||
if existing.scalar_one_or_none():
|
continue
|
||||||
return
|
|
||||||
|
|
||||||
session.add(ProcessingQueue(
|
session.add(ProcessingQueue(
|
||||||
document_id=document_id,
|
document_id=document_id,
|
||||||
stage=next_stage,
|
stage=next_stage,
|
||||||
status="pending",
|
status="pending",
|
||||||
))
|
))
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,18 @@
|
|||||||
return `${(bytes / 1048576).toFixed(1)}MB`;
|
return `${(bytes / 1048576).toFixed(1)}MB`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DOMAIN_COLORS = {
|
||||||
|
'Knowledge/Philosophy': 'var(--domain-philosophy)',
|
||||||
|
'Knowledge/Language': 'var(--domain-language)',
|
||||||
|
'Knowledge/Engineering': 'var(--domain-engineering)',
|
||||||
|
'Knowledge/Industrial_Safety': 'var(--domain-safety)',
|
||||||
|
'Knowledge/Programming': 'var(--domain-programming)',
|
||||||
|
'Knowledge/General': 'var(--domain-general)',
|
||||||
|
'Reference': 'var(--domain-reference)',
|
||||||
|
};
|
||||||
|
|
||||||
|
let domainColor = $derived(DOMAIN_COLORS[doc.ai_domain] || 'var(--border)');
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
// 모바일에서는 항상 detail 페이지로 이동
|
// 모바일에서는 항상 detail 페이지로 이동
|
||||||
if (window.innerWidth < 1024) {
|
if (window.innerWidth < 1024) {
|
||||||
@@ -39,9 +51,14 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={handleClick}
|
onclick={handleClick}
|
||||||
class="flex items-start gap-3 p-3 bg-[var(--surface)] border rounded-lg hover:border-[var(--accent)] transition-colors group w-full text-left
|
class="flex items-stretch bg-[var(--surface)] border rounded-lg hover:border-[var(--accent)] transition-colors group w-full text-left overflow-hidden
|
||||||
{selected ? 'border-[var(--accent)] bg-[var(--accent)]/5' : 'border-[var(--border)]'}"
|
{selected ? 'border-[var(--accent)] bg-[var(--accent)]/5' : 'border-[var(--border)]'}"
|
||||||
>
|
>
|
||||||
|
<!-- domain 색상 바 -->
|
||||||
|
<div class="w-1 shrink-0 rounded-l-lg" style="background: {domainColor}"></div>
|
||||||
|
|
||||||
|
<!-- 콘텐츠 -->
|
||||||
|
<div class="flex items-start gap-3 p-3 flex-1 min-w-0">
|
||||||
<!-- 포맷 아이콘 -->
|
<!-- 포맷 아이콘 -->
|
||||||
<div class="shrink-0 mt-0.5 text-[var(--text-dim)] group-hover:text-[var(--accent)]">
|
<div class="shrink-0 mt-0.5 text-[var(--text-dim)] group-hover:text-[var(--accent)]">
|
||||||
<FormatIcon format={doc.file_format} size={18} />
|
<FormatIcon format={doc.file_format} size={18} />
|
||||||
@@ -86,4 +103,5 @@
|
|||||||
<span class="text-[var(--text-dim)]">{formatSize(doc.file_size)}</span>
|
<span class="text-[var(--text-dim)]">{formatSize(doc.file_size)}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user