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.
186 lines
6.5 KiB
TypeScript
186 lines
6.5 KiB
TypeScript
// frontend/lib/mail/api.ts — mailApi (accounts/folders/list/detail/extract/reply/send)
|
|
import type {
|
|
AccountFolder,
|
|
Conversation,
|
|
EmailDetail,
|
|
EmailRow,
|
|
ExtractAllResponse,
|
|
ExtractKind,
|
|
ExtractResponse,
|
|
MailAccount,
|
|
ReplyDraft,
|
|
SendRequest,
|
|
SendResponse,
|
|
} from "@/lib/types";
|
|
|
|
const J = { "Content-Type": "application/json" };
|
|
|
|
// 아리 자동 정리 실행 로그 1줄(사이클 단위).
|
|
export type TriageLogEntry = { at: string; analyzed: number; cards: number; error: string | null };
|
|
// 오늘 새 메일 1통의 '아리 분류·분석' 상세 — 상세 로그 뷰용.
|
|
export type TriageClassifiedRow = {
|
|
id: string;
|
|
account: string;
|
|
from_name: string;
|
|
subject: string;
|
|
time: string;
|
|
bucket: string; // 집중/홍보/소셜/업데이트/스팸/휴지통/보관/기타
|
|
focused: boolean;
|
|
analyzed: boolean;
|
|
priority: string | null;
|
|
tasks: number;
|
|
events: number;
|
|
replies: number;
|
|
is_card: boolean;
|
|
};
|
|
|
|
async function ok<T>(r: Response): Promise<T> {
|
|
if (!r.ok) {
|
|
let msg = `${r.status} ${r.statusText}`;
|
|
try {
|
|
const j = await r.json();
|
|
if (j?.detail) msg = j.detail;
|
|
} catch {
|
|
/* 본문 없음 */
|
|
}
|
|
throw new Error(msg);
|
|
}
|
|
return r.json() as Promise<T>;
|
|
}
|
|
|
|
export const mailApi = {
|
|
accounts: () =>
|
|
fetch("/api/mail/accounts", { cache: "no-store" }).then((r) => ok<MailAccount[]>(r)),
|
|
|
|
folders: () =>
|
|
fetch("/api/mail/folders", { cache: "no-store" }).then((r) => ok<AccountFolder[]>(r)),
|
|
|
|
list: (account: string | null, folder: string, q?: string) => {
|
|
const qs = new URLSearchParams();
|
|
if (account) qs.set("account", account);
|
|
qs.set("folder", folder);
|
|
if (q && q.trim()) qs.set("q", q.trim());
|
|
return fetch(`/api/mail?${qs}`, { cache: "no-store" }).then((r) => ok<EmailRow[]>(r));
|
|
},
|
|
|
|
sent: (account: string | null) => {
|
|
const qs = new URLSearchParams();
|
|
if (account) qs.set("account", account);
|
|
const suffix = qs.toString() ? `?${qs}` : "";
|
|
return fetch(`/api/mail/sent${suffix}`, { cache: "no-store" }).then((r) => ok<EmailRow[]>(r));
|
|
},
|
|
|
|
drafts: (account: string | null) => {
|
|
const qs = new URLSearchParams();
|
|
if (account) qs.set("account", account);
|
|
const suffix = qs.toString() ? `?${qs}` : "";
|
|
return fetch(`/api/mail/drafts${suffix}`, { cache: "no-store" }).then((r) => ok<EmailRow[]>(r));
|
|
},
|
|
|
|
detail: (id: string) =>
|
|
fetch(`/api/mail/${id}`, { cache: "no-store" }).then((r) => ok<EmailDetail>(r)),
|
|
|
|
extract: (id: string, kind: ExtractKind, index = 0) =>
|
|
fetch(`/api/mail/${id}/extract`, {
|
|
method: "POST",
|
|
headers: J,
|
|
body: JSON.stringify({ kind, index }),
|
|
}).then((r) => ok<ExtractResponse>(r)),
|
|
|
|
extractAll: (id: string) =>
|
|
fetch(`/api/mail/${id}/extract-all`, { method: "POST", headers: J, body: "{}" }).then((r) =>
|
|
ok<ExtractAllResponse>(r),
|
|
),
|
|
|
|
// 아리 정리 기준 시각 + 요약: count(카드/배지) · new_total(오늘 온 새 메일) · screened(자동 걸러낸 수).
|
|
// 새 메일 분석은 백그라운드 auto-triage 가 전담 — 수동 분석/기준 재설정 엔드포인트는 없앴다.
|
|
// dayStart=클라 로컬 자정(ISO) → new_total/screened 를 '오늘 도착분'으로 한정(서버 UTC TZ 보정).
|
|
triageBaseline: (dayStart?: string) =>
|
|
fetch(`/api/mail/triage/baseline${dayStart ? `?day_start=${encodeURIComponent(dayStart)}` : ""}`, {
|
|
cache: "no-store",
|
|
}).then((r) => ok<{ since: string; count: number; new_total: number; screened: number }>(r)),
|
|
|
|
// 오늘 아리 자동 정리 실행 로그(관측성) — 실제 분석/에러를 시각과 함께. dayStart=클라 로컬 자정.
|
|
triageLog: (dayStart?: string) =>
|
|
fetch(`/api/mail/triage/log${dayStart ? `?day_start=${encodeURIComponent(dayStart)}` : ""}`, {
|
|
cache: "no-store",
|
|
}).then((r) => ok<TriageLogEntry[]>(r)),
|
|
|
|
// 오늘 새 메일별 분류·분석 상세(상세 로그 뷰) — 어떤 메일을 어디로 분류했는지 하나씩.
|
|
triageClassified: (dayStart?: string) =>
|
|
fetch(
|
|
`/api/mail/triage/classified${dayStart ? `?day_start=${encodeURIComponent(dayStart)}` : ""}`,
|
|
{ cache: "no-store" },
|
|
).then((r) => ok<TriageClassifiedRow[]>(r)),
|
|
|
|
// 아리 정리에서 이 메일 카드를 기각 — 다이제스트에서 빼고 다시 표시하지 않음(받은편지함엔 잔류).
|
|
triageDismiss: (id: string) =>
|
|
fetch(`/api/mail/${id}/triage/dismiss`, { method: "POST", headers: J, body: "{}" }).then((r) =>
|
|
ok<{ ok: boolean; count: number }>(r),
|
|
),
|
|
|
|
replyDraft: (id: string, reply_index: number | null) =>
|
|
fetch(`/api/mail/${id}/reply-draft`, {
|
|
method: "POST",
|
|
headers: J,
|
|
body: JSON.stringify({ reply_index }),
|
|
}).then((r) => ok<ReplyDraft>(r)),
|
|
|
|
send: (body: SendRequest) =>
|
|
fetch("/api/mail/send", { method: "POST", headers: J, body: JSON.stringify(body) }).then((r) =>
|
|
ok<SendResponse>(r),
|
|
),
|
|
|
|
sync: () =>
|
|
fetch("/api/mail/sync", { method: "POST", headers: J, body: "{}" }).then((r) =>
|
|
ok<{ accounts: number; upserted: number }>(r),
|
|
),
|
|
|
|
star: (id: string, starred: boolean) =>
|
|
fetch(`/api/mail/${id}/star`, {
|
|
method: "PATCH",
|
|
headers: J,
|
|
body: JSON.stringify({ starred }),
|
|
}).then((r) => ok<EmailRow>(r)),
|
|
|
|
setRead: (id: string, read: boolean) =>
|
|
fetch(`/api/mail/${id}/read`, {
|
|
method: "PATCH",
|
|
headers: J,
|
|
body: JSON.stringify({ read }),
|
|
}).then((r) => ok<EmailRow>(r)),
|
|
|
|
archive: (id: string) =>
|
|
fetch(`/api/mail/${id}/archive`, { method: "POST", headers: J, body: "{}" }).then((r) =>
|
|
ok<EmailRow>(r),
|
|
),
|
|
|
|
unarchive: (id: string) =>
|
|
fetch(`/api/mail/${id}/unarchive`, { method: "POST", headers: J, body: "{}" }).then((r) =>
|
|
ok<EmailRow>(r),
|
|
),
|
|
|
|
remove: (id: string) =>
|
|
fetch(`/api/mail/${id}`, { method: "DELETE" }).then((r) => ok<{ ok: boolean; id: string }>(r)),
|
|
|
|
// 첨부 다운로드용 URL(브라우저가 직접 내려받음)
|
|
attachmentUrl: (id: string, attId: string) => `/api/mail/${id}/attachments/${attId}`,
|
|
|
|
thread: (id: string) =>
|
|
fetch(`/api/mail/${id}/thread`, { cache: "no-store" }).then((r) => ok<Conversation>(r)),
|
|
|
|
labels: (id: string, add: string[], remove: string[]) =>
|
|
fetch(`/api/mail/${id}/labels`, {
|
|
method: "PATCH",
|
|
headers: J,
|
|
body: JSON.stringify({ add, remove }),
|
|
}).then((r) => ok<EmailRow>(r)),
|
|
|
|
bulk: (ids: string[], action: string) =>
|
|
fetch("/api/mail/bulk", {
|
|
method: "POST",
|
|
headers: J,
|
|
body: JSON.stringify({ ids, action }),
|
|
}).then((r) => ok<{ ok: boolean; count: number; action: string }>(r)),
|
|
};
|