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.

189 lines
6.8 KiB
TypeScript

// frontend/components/mail/TriageLogModal.tsx — '오늘 메일 상세 로그' 다이얼로그(히어로 버튼으로 열림)
// 정직성: 집중/홍보/스팸 '분류'는 Gmail·Outlook 이 한 것. 아리가 한 일은 '집중 메일 분석'뿐.
"use client";
import { useEffect } from "react";
import useSWR from "swr";
import { Icon } from "@/components/Icon";
import { mailApi, type TriageLogEntry } from "@/lib/mail/api";
import type { MailAccount } from "@/lib/types";
// ISO → 'HH:MM'(로컬).
function fmtTime(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "";
return d.toLocaleTimeString("ko-KR", { hour: "numeric", minute: "2-digit" });
}
// 분류 버킷 → 칩 색 클래스 / 표시 순서.
const BUCKET_TONE: Record<string, string> = {
: "focus",
: "promo",
: "social",
: "update",
: "spam",
: "muted",
: "muted",
: "muted",
};
const BUCKET_ORDER = ["집중", "홍보", "소셜", "업데이트", "스팸", "휴지통", "보관", "기타"];
export function TriageLogModal({
open,
onClose,
log,
accounts,
accColor,
onOpenMail,
}: {
open: boolean;
onClose: () => void;
log: TriageLogEntry[];
accounts: Record<string, MailAccount>;
accColor: (id: string) => string;
onOpenMail: (id: string) => void;
}) {
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onClose]);
// 열렸을 때만 상세 분류를 가져온다.
const { data: rows } = useSWR(
open ? "mail/triage/classified" : null,
() => {
const d = new Date();
d.setHours(0, 0, 0, 0);
return mailApi.triageClassified(d.toISOString());
},
{ revalidateOnFocus: false },
);
if (!open) return null;
const list = rows ?? [];
const counts: Record<string, number> = {};
list.forEach((r) => (counts[r.bucket] = (counts[r.bucket] ?? 0) + 1));
const buckets = BUCKET_ORDER.filter((b) => counts[b]);
return (
<div
className="tlog-overlay"
onClick={onClose}
role="dialog"
aria-modal="true"
aria-label="오늘 메일 상세 로그"
>
<div className="tlog-panel" onClick={(e) => e.stopPropagation()}>
<header className="tlog-head">
<div className="tlog-title">
<Icon name="list" />
</div>
<button className="tlog-close" onClick={onClose} aria-label="닫기">
<Icon name="x" />
</button>
</header>
<div className="tlog-body">
{/* 아리가 실제로 한 일 = 집중 메일 LLM 분석(분류 아님). */}
<section className="tlog-sec">
<h4> </h4>
{log.length === 0 ? (
<p className="tlog-muted"> .</p>
) : (
<ul className="tlog-events">
{log.map((e, i) => (
<li key={i} className={e.error ? "err" : ""}>
<time>{fmtTime(e.at)}</time>
<span>
{e.error
? `분석 실패 · ${e.error}`
: `집중 메일 ${e.analyzed}통 분석${e.cards ? ` · ${e.cards}건 챙길 거리` : ""}`}
</span>
</li>
))}
</ul>
)}
</section>
{/* 버킷 분류는 Gmail·Outlook 이 한 것 — 정직하게 출처 표기. */}
<section className="tlog-sec">
<h4> {list.length}</h4>
<p className="tlog-note">
·· <b>Gmail·Outlook</b> .
·· .
</p>
{buckets.length > 0 && (
<div className="tlog-chips">
{buckets.map((b) => (
<span key={b} className={"tlog-chip " + (BUCKET_TONE[b] ?? "muted")}>
{b} {counts[b]}
</span>
))}
</div>
)}
{!rows ? (
<p className="tlog-muted"> </p>
) : list.length === 0 ? (
<p className="tlog-muted"> .</p>
) : (
<ul className="tlog-mails">
{list.map((r) => {
const accName = accounts[r.account]?.name ?? r.account;
const noAction = r.tasks === 0 && r.events === 0 && r.replies === 0;
return (
<li
key={r.id}
className="tlog-mail"
onClick={() => {
onOpenMail(r.id);
onClose();
}}
>
<div
className="tm-ava"
style={{ background: accColor(r.account) }}
title={accName}
>
{accName.slice(0, 1).toUpperCase()}
</div>
<div className="tm-main">
<div className="tm-top">
<span className="tm-from">{r.from_name}</span>
<span className="tm-time">{r.time}</span>
</div>
<div className="tm-subj">{r.subject}</div>
</div>
<div className="tm-tags">
<span className={"tlog-chip " + (BUCKET_TONE[r.bucket] ?? "muted")}>
{r.bucket}
</span>
{r.focused &&
(r.analyzed ? (
<span className="tm-ana">
{[
r.priority,
r.tasks > 0 && `할 일 ${r.tasks}`,
r.events > 0 && `일정 ${r.events}`,
r.replies > 0 && "답장",
noAction && "정보성",
]
.filter(Boolean)
.join(" · ")}
</span>
) : (
<span className="tm-ana wait"> </span>
))}
</div>
</li>
);
})}
</ul>
)}
</section>
</div>
</div>
</div>
);
}