diff --git a/app/api/memos.py b/app/api/memos.py index 9c43496..e4e40eb 100644 --- a/app/api/memos.py +++ b/app/api/memos.py @@ -688,6 +688,55 @@ async def dismiss_event_suggestion( return _to_memo_response(doc) +@router.post("/{memo_id}/promote-to-document", status_code=201) +async def promote_memo_to_document( + memo_id: int, + user: Annotated[User, Depends(get_current_user)], + session: Annotated[AsyncSession, Depends(get_session)], +): + """메모 1건 → 문서함 정식 Document 로 승격 ("자료로 보내기", P1). + + 동작 (in-place 변환 — 별 row 생성 X, extracted_text/태그/이력 보존): + - source_channel memo/voice/hermes → 'manual' (메모 목록서 빠지고 문서함 진입) + - file_type 'note' → 'editable' (문서함 목록 필터 `file_type != 'note'` 통과) + - category='library' (자료실), content_origin='manual' + - classify/embed/chunk 재큐 → 도메인 재부여 + 요약/심층분석(26B escalate) + 임베딩/청크 갱신 + P2 'draft' 워커(후속)가 거친 메모를 구조화 마크다운(md_content)으로 정리 예정. + """ + doc = await session.get(Document, memo_id) + if ( + not doc + or doc.deleted_at is not None + or doc.source_channel not in ("memo", "voice", "hermes") + or doc.file_type != "note" + ): + raise HTTPException(status_code=404, detail="승격할 메모를 찾을 수 없습니다") + + now = datetime.now(timezone.utc) + doc.source_metadata = { + **(doc.source_metadata or {}), + "promoted_from_memo": True, + "promoted_at": now.isoformat(), + "original_source_channel": doc.source_channel, + } + doc.source_channel = "manual" + doc.file_type = "editable" + doc.category = "library" + doc.content_origin = "manual" + doc.updated_at = now + + # 문서 컨텍스트로 재처리 — 도메인 재부여 + 요약/심층분석 + 임베딩/청크 갱신. + await _enqueue_ai_stages(session, doc.id) + await session.commit() + await session.refresh(doc) + + return { + "document_id": doc.id, + "category": doc.category, + "message": "문서함으로 보냈습니다. AI 분류·요약·심층분석을 진행합니다.", + } + + # ─── Memo Intake Upgrade PR-2C: voice upload ─── diff --git a/frontend/src/routes/memos/+page.svelte b/frontend/src/routes/memos/+page.svelte index ac7348e..162da61 100644 --- a/frontend/src/routes/memos/+page.svelte +++ b/frontend/src/routes/memos/+page.svelte @@ -3,7 +3,7 @@ import { api } from '$lib/api'; import { addToast } from '$lib/stores/toast'; import { renderMemoHtml, todayIso, countHiddenTasks, DEFAULT_HIDE_AFTER_MS } from '$lib/utils/memoRenderer'; - import { Pin, PinOff, Pencil, Trash2, Eye, EyeOff, X, Check, Archive, ArchiveRestore, ListChecks, Bold, Heading, CalendarDays, Mic, Calendar, Activity, ArrowRight, FileText, BookOpen } from 'lucide-svelte'; + import { Pin, PinOff, Pencil, Trash2, Eye, EyeOff, X, Check, Archive, ArchiveRestore, ListChecks, Bold, Heading, CalendarDays, Mic, Calendar, Activity, ArrowRight, FileText, BookOpen, FolderInput } from 'lucide-svelte'; import { getAccessToken } from '$lib/api'; import Button from '$lib/components/ui/Button.svelte'; import Card from '$lib/components/ui/Card.svelte'; @@ -276,6 +276,18 @@ } } + // 자료로 보내기 — 메모를 문서함 정식 문서로 승격(이동) + AI 분류/요약/심층/도메인. + async function promoteToDocument(memoId) { + try { + const res = await api(`/memos/${memoId}/promote-to-document`, { method: 'POST' }); + addToast('success', '문서함으로 보냈습니다 · AI 분석 진행 중'); + // in-place 승격이라 더는 메모가 아님 → 목록에서 제거 + memos = memos.filter((m) => m.id !== memoId); + } catch (err) { + addToast('error', err?.detail || '자료로 보내기 실패'); + } + } + // voice 메모 audio URL — /api/documents/{id}/file?token= 패턴 재사용 function voiceAudioUrl(memoId) { const token = getAccessToken(); @@ -601,6 +613,17 @@ {/if} + + {#if editingId !== memo.id && !showArchived} +