# backend/app/routers/inbox.py import uuid from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select from ..db import get_session from ..llm.provider import LLMProvider, get_provider from ..models import InboxClassification, InboxItem, InboxStatus, Prio, Project, Task, TaskStatus from ..routers.tasks import to_node from ..schemas import ( CaptureRequest, CaptureResponse, ClassificationOut, ConfirmResponse, InboxItemOut, ReclassifyRequest, ) from ..services.classification import classify, persist_classification router = APIRouter() # prefix 없음. main.py 에서 prefix="/api" 등록. def latest_cls(s: Session, item_id: str) -> InboxClassification | None: # id 가 TEXT PK(UUID)라 정렬 키로 부적합 → created_at 기준 최신. rows = s.exec( select(InboxClassification) .where(InboxClassification.inbox_item_id == item_id) .order_by(InboxClassification.created_at.desc()) ).all() return rows[0] if rows else None def item_out(s: Session, item: InboxItem) -> InboxItemOut: c = latest_cls(s, item.id) cls = ClassificationOut.model_validate(c, from_attributes=True) if c else None return InboxItemOut( id=item.id, kind=item.kind, raw=item.raw, status=item.status, created_at=item.created_at, materialized_task_id=item.materialized_task_id, classification=cls, ) @router.get("/inbox", response_model=list[InboxItemOut]) def list_inbox(s: Session = Depends(get_session)): items = s.exec(select(InboxItem).order_by(InboxItem.created_at.desc())).all() return [item_out(s, it) for it in items] @router.post("/inbox/capture", response_model=CaptureResponse) def capture( body: CaptureRequest, s: Session = Depends(get_session), provider: LLMProvider = Depends(get_provider), ): iid = "s-" + uuid.uuid4().hex[:8] item = InboxItem(id=iid, kind=body.kind, raw=body.raw, status=InboxStatus.new) s.add(item) s.commit() s.refresh(item) c = classify(s, body.raw, provider) # 동기 분류 (LLM or 폴백) row = persist_classification(s, iid, c) item.status = InboxStatus.classified s.add(item) s.commit() return CaptureResponse( item=item_out(s, item), classification=ClassificationOut.model_validate(row, from_attributes=True), ) @router.post("/inbox/{iid}/reclassify", response_model=ClassificationOut) def reclassify( iid: str, body: ReclassifyRequest, s: Session = Depends(get_session), provider: LLMProvider = Depends(get_provider), ): item = s.get(InboxItem, iid) if not item: raise HTTPException(404, "inbox item not found") c = classify(s, item.raw, provider, force_type=body.type.value if body.type else None) row = persist_classification(s, iid, c) # 새 row → latest 가 최신 item.status = InboxStatus.classified s.add(item) s.commit() return ClassificationOut.model_validate(row, from_attributes=True) @router.post("/inbox/{iid}/confirm", response_model=ConfirmResponse) def confirm(iid: str, s: Session = Depends(get_session)): item = s.get(InboxItem, iid) if not item: raise HTTPException(404, "inbox item not found") # 멱등: 이미 confirmed 면 중복 실체화하지 않고 현재 상태만 반환 if item.status == InboxStatus.confirmed: return ConfirmResponse(item=item_out(s, item), task=None) c = latest_cls(s, iid) if not c: raise HTTPException(400, "no classification to confirm") created: Task | None = None ctype = c.type.value if hasattr(c.type, "value") else c.type csphere = c.sphere.value if hasattr(c.sphere, "value") else c.sphere if ctype == "task": # 프로젝트 결정: classification.project_id 있으면 그대로, 없으면 sphere 기본 pid = c.project_id if not pid or not s.get(Project, pid): pid = "me" if csphere == "life" else _fallback_work_project(s) # confirm task 필드 정본(R10) notes = "스마트 인박스에서 실체화된 작업이에요." if c.extra: notes += " " + c.extra # 예: 비행기 티켓 → "가격 추적 알림 켜둠" tid = "t-" + uuid.uuid4().hex[:8] created = Task( id=tid, title=item.raw, project_id=pid, status=TaskStatus.todo, assignee_id="jiwoo", prio=Prio.normal, due=None, notes=notes, ) s.add(created) s.commit() s.refresh(created) item.materialized_task_id = tid item.status = InboxStatus.confirmed s.add(item) s.commit() node = to_node(s, created, s.exec(select(Task)).all()) if created else None return ConfirmResponse(item=item_out(s, item), task=node) @router.post("/inbox/{iid}/dismiss", response_model=InboxItemOut) def dismiss(iid: str, s: Session = Depends(get_session)): item = s.get(InboxItem, iid) if not item: raise HTTPException(404, "inbox item not found") item.status = InboxStatus.dismissed s.add(item) s.commit() return item_out(s, item) def _fallback_work_project(s: Session) -> str: p = s.exec(select(Project).where(Project.folder_id == "work")).first() return p.id if p else "me"