You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

183 lines
5.4 KiB
TypeScript

// 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<UiInboxItem[]>(() => 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,
}
: 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,
}
: 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 (
<>
<div className="pagehead">
<div>
<div className="ph-eyebrow">
<span> {todayRouted} </span>
<span className="sep" />
<span> 0</span>
</div>
<h1 className="ph-title">
<em> </em>
</h1>
</div>
</div>
<div className="work">
<div className="board">
{initialLoadError && (
<div className="inbox-loaderr card sp2" style={{ gridColumn: "span 4" }}>
<Icon name="x" /> . .
</div>
)}
<SmartInbox
items={shown}
todayRouted={todayRouted}
filter={filter}
onFilter={setFilter}
onSubmit={submit}
onConfirm={confirm}
onReType={reType}
/>
<ClassifyPrinciples />
<TodayRouted items={items} todayRouted={todayRouted} />
</div>
</div>
</>
);
}