From 52dd7129a3c03acdc6e369b9f2439b86ec0a2bcd Mon Sep 17 00:00:00 2001 From: hyungi Date: Tue, 12 May 2026 06:56:44 +0000 Subject: [PATCH] feat(memos): include source_channel=email in memo inbox list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit list 쿼리 확장: - 기존 source_channel IN (memo, voice) → OR (source_channel = email AND source_external_id IS NOT NULL) - mailplus_archive 의 INBOX root archive row (source_external_id=NULL) 는 자동 제외 - inbox_ingest 가 만든 email memo 만 /memos UI 에 노출 MemoResponse 확장: - source_external_id: Message-ID 또는 imap UID fallback - email_subject: email_metadata.subject (UI 부제/툴팁) _to_memo_response 가 email_metadata JSONB 에서 subject 추출. ingest 가 만든 row 가 UI 에 보이는 게 PR-2B 의 분류 배지/4 버튼/promote flow 자산 재사용의 전제. plan: ~/.claude/plans/document-enchanted-candy.md --- app/api/memos.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/api/memos.py b/app/api/memos.py index a1b5316..f24fcd9 100644 --- a/app/api/memos.py +++ b/app/api/memos.py @@ -15,7 +15,7 @@ from typing import Annotated, Any from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile from pydantic import BaseModel, Field -from sqlalchemy import delete, func, select +from sqlalchemy import delete, func, select, or_, and_ from sqlalchemy.ext.asyncio import AsyncSession from core.auth import get_current_user @@ -178,6 +178,9 @@ class MemoResponse(BaseModel): source_channel: str | None = None # voice/memo 등 진입점 식별 (UI 배지) file_type: str | None = None # audio (voice 메모) vs note (text 메모) file_path: str | None = None # voice 메모의 NAS audio 경로 (audio player 용) + # PR-4 Email Ingest — 이메일 source 메모 식별 + UI 표시용 + source_external_id: str | None = None # email 의 Message-ID 또는 imap UID fallback + email_subject: str | None = None # email_metadata.subject — 메모 카드 부제 / 툴팁 created_at: datetime updated_at: datetime @@ -212,6 +215,8 @@ def _to_memo_response(doc: Document) -> MemoResponse: source_channel=doc.source_channel, file_type=doc.file_type, file_path=doc.file_path, + source_external_id=doc.source_external_id, + email_subject=(doc.email_metadata or {}).get('subject') if doc.email_metadata else None, created_at=doc.created_at, updated_at=doc.updated_at, ) @@ -274,8 +279,12 @@ async def list_memos( voice 메모는 file_type='immutable' + category='audio' + source_channel='voice' 패턴. source_channel 만으로 분리 (file_type 필터는 immutable 다른 binary 까지 끌어옴 — 회피). """ + # PR-4: inbox_ingest 가 만든 email memo 도 포함 (source_external_id != NULL 로 mailplus_archive 의 archive row 제외) base = select(Document).where( - Document.source_channel.in_(("memo", "voice")), + or_( + Document.source_channel.in_(("memo", "voice")), + and_(Document.source_channel == "email", Document.source_external_id.isnot(None)), + ), Document.deleted_at == None, # noqa: E711 Document.archived == archived, )