From fe8235d7269a722870ae076374d153ccee17811e Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 15 Jun 2026 14:32:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(memos):=20=EC=9E=90=EB=A3=8C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=B4=EB=82=B4=EA=B8=B0=20=E2=80=94=20=EB=A9=94=EB=AA=A8?= =?UTF-8?q?=EB=A5=BC=20=EB=AC=B8=EC=84=9C=ED=95=A8=20=EC=A0=95=EC=8B=9D=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=EB=A1=9C=20=EC=8A=B9=EA=B2=A9=20(P1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 새 POST /memos/{id}/promote-to-document: in-place 승격(별 row X) — source_channel→manual, file_type note→editable, category=library, content_origin=manual + classify/embed/chunk 재큐(도메인 재부여·요약·심층분석). 메모 카드에 always-visible '자료로 보내기' 버튼(지식 메모=ai_event_kind note 포함). P2(거친 메모→구조화 마크다운 draft 워커)는 후속. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/memos.py | 49 ++++++++++++++++++++++++++ frontend/src/routes/memos/+page.svelte | 25 ++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) 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} +
+ +
+ {/if} + {#if editingId !== memo.id} {#if memo.user_tags?.length || memo.ai_tags?.length}