// frontend/components/inbox/InboxView.tsx — 인박스 클라이언트 루트 "use client"; import { useRef, useState } from "react"; import { Icon } from "@/components/Icon"; import { captureInbox, confirmInbox, reclassifyInbox } from "@/lib/inbox/api"; import { nextType } from "@/lib/inbox/presentation"; import type { InboxItem, RouteType, Sphere, UiInboxItem } from "@/lib/types"; import ClassifyPrinciples from "./ClassifyPrinciples"; import SmartInbox from "./SmartInbox"; import TodayRouted from "./TodayRouted"; function relTime(iso: string): string { if (!iso) return "방금"; const diff = Date.now() - new Date(iso).getTime(); if (Number.isNaN(diff)) return "방금"; const m = Math.floor(diff / 60000); if (m < 1) return "방금"; if (m < 60) return `${m}분 전`; const h = Math.floor(m / 60); if (h < 24) return `${h}시간 전`; return `${Math.floor(h / 24)}일 전`; } function toUi(it: InboxItem): UiInboxItem { return { ...it, status: it.status, time: relTime(it.created_at) }; } export default function InboxView({ initialItems, initialLoadError, }: { initialItems: InboxItem[]; initialLoadError: boolean; }) { const [items, setItems] = useState(() => initialItems.map(toUi)); const [filter, setFilter] = useState<"all" | Sphere>("all"); const tmpSeq = useRef(0); const clearFresh = (xs: UiInboxItem[]) => xs.map((x) => (x.fresh ? { ...x, fresh: false } : x)); /* ---- 캡처 제출 ---- */ const submit = async (raw: string, kind: "text" | "voice" | "image") => { const text = raw.trim(); if (!text) return; const tmpId = "tmp-" + ++tmpSeq.current; setItems((xs) => [ { id: tmpId, kind, raw: text, status: "thinking", time: "방금", created_at: new Date(0).toISOString(), materialized_task_id: null, classification: null, fresh: true, } as UiInboxItem, ...clearFresh(xs), ]); try { const { item, classification } = await captureInbox({ kind, raw: text }); setItems((xs) => xs.map((x) => x.id === tmpId ? { ...item, classification, status: "classified", time: "방금", fresh: true, fallbackUsed: classification.model === "heuristic", } : x, ), ); } catch { setItems((xs) => xs.map((x) => (x.id === tmpId ? { ...x, status: "error" } : x))); } }; /* ---- 다르게 분류 (타입 순환) ---- */ const reType = async (id: string) => { const cur = items.find((x) => x.id === id); if (!cur?.classification) return; const original = cur.classification.type; const wanted = nextType(original); // 낙관적 미리보기 setItems((xs) => xs.map((x) => x.id === id && x.classification ? { ...x, classification: { ...x.classification, type: wanted } } : x, ), ); try { const { item, classification } = await reclassifyInbox(id, wanted as RouteType); setItems((xs) => xs.map((x) => x.id === id ? { ...item, classification, status: "classified", time: x.time, fresh: x.fresh, fallbackUsed: classification.model === "heuristic", } : x, ), ); } catch { // 롤백: 원래 타입 복구 setItems((xs) => xs.map((x) => x.id === id && x.classification ? { ...x, classification: { ...x.classification, type: original } } : x, ), ); } }; /* ---- 좋아요(confirm) → 실체화 ---- */ const confirm = async (id: string) => { setItems((xs) => xs.map((x) => (x.id === id ? { ...x, status: "confirmed" } : x))); try { const res = await confirmInbox(id); setItems((xs) => xs.map((x) => x.id === id ? { ...res.item, classification: x.classification, status: "confirmed", time: x.time } : x, ), ); } catch { setItems((xs) => xs.map((x) => (x.id === id ? { ...x, status: "classified" } : x))); } }; const todayRouted = items.filter( (x) => x.classification && x.status !== "thinking" && x.status !== "error", ).length; const shown = items.filter( (x) => filter === "all" || (x.classification && x.classification.sphere === filter), ); return ( <>
오늘 {todayRouted}건 정리 분류를 고민한 시간 0초

스마트 인박스 일단 적으세요

{initialLoadError && (
인박스를 불러오지 못했어요. 새로고침하거나 백엔드 연결을 확인하세요.
)}
); }