feat(memos): include source_channel=email in memo inbox list

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
This commit is contained in:
hyungi
2026-05-12 06:56:44 +00:00
parent f4eef9e6e0
commit 52dd7129a3
+11 -2
View File
@@ -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,
)