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.

191 lines
5.9 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// frontend/components/notify/NotifyClient.tsx — 알림 페이지 오케스트레이터 (SubRail 3뷰)
"use client";
import { useState } from "react";
import { Icon } from "@/components/Icon";
import { SubRail, type RailItem } from "@/components/SubRail";
import { useNotify } from "@/lib/hooks/useNotify";
import { notifyApi } from "@/lib/notify/api";
import type { Notification, NotifyBucket } from "@/lib/types";
import { TriageStream } from "./TriageStream";
import { GuardPanel } from "./GuardPanel";
import { SenderRules } from "./SenderRules";
const HEAD: Record<string, { title: string; em: string }> = {
triage: { title: "알림", em: "중요한 것만 울려요" },
guard: { title: "집중 보호", em: "조용히, 그래도 놓치지 않게" },
senders: { title: "발신자 규칙", em: "누구는 바로, 누구는 모아서" },
};
export function NotifyClient() {
const [view, setView] = useState<string>("triage");
const [tab, setTab] = useState<NotifyBucket>("now");
const { triage, guard, digests, senders, error, mutate, mGuard } = useNotify();
if (error && !triage) {
return (
<div className="notifypage" data-screen-label="알림">
<div className="dash-error" role="alert">
<p> .</p>
<button className="mini-cta" onClick={() => mutate()}>
<Icon name="swap" />
</button>
</div>
</div>
);
}
if (!triage) {
return (
<div className="notifypage" data-screen-label="알림">
<div className="pagehead">
<div>
<h1 className="ph-title"></h1>
</div>
</div>
</div>
);
}
const counts = triage.counts;
const stats = triage.stats;
const h = HEAD[view];
const NT_RAIL: RailItem[] = [
{ id: "triage", icon: "bell", label: "트리아지", dot: counts.now > 0 },
{ sep: true },
{ id: "guard", icon: "shield", label: "집중 보호 · 묶음" },
{ id: "senders", icon: "users", label: "발신자 규칙" },
];
const onLater = async (it: Notification) => {
// 낙관적: now 에서 제거 + later 맨 앞에 추가(why=내가 미룸/muted)
mutate(
(cur) =>
cur
? {
...cur,
now: cur.now.filter((x) => x.id !== it.id),
later: [
{ ...it, bucket: "later", why: "내가 미룸", tone: "muted", act: "" },
...cur.later,
],
counts: { ...cur.counts, now: cur.counts.now - 1, later: cur.counts.later + 1 },
}
: cur,
false,
);
try {
await notifyApi.reclassify(it.id, "later");
} finally {
mutate();
}
};
const onUndo = async (it: Notification) => {
// 낙관적: held 에서 제거 + now 맨 앞에 추가(why=되돌림/amber)
mutate(
(cur) =>
cur
? {
...cur,
held: cur.held.filter((x) => x.id !== it.id),
now: [{ ...it, bucket: "now", why: "되돌림", tone: "amber" }, ...cur.now],
counts: { ...cur.counts, held: cur.counts.held - 1, now: cur.counts.now + 1 },
}
: cur,
false,
);
try {
await notifyApi.undo(it.id);
} finally {
mutate();
}
};
const onToggleGuard = async () => {
if (!guard) return;
const next = !guard.on;
mGuard({ ...guard, on: next }, false); // 낙관적
try {
await notifyApi.setGuard(next);
} finally {
mGuard();
}
};
return (
<div className="notifypage" data-screen-label="알림">
<div className="pagehead">
<div>
<div className="ph-eyebrow">
<span> {stats.total} </span>
<span className="sep" />
<span> {stats.seen}</span>
<span className="sep" />
<span>{stats.filtered} </span>
</div>
<h1 className="ph-title">
{h.title} <em>{h.em}</em>
</h1>
</div>
</div>
<div className="work">
<SubRail items={NT_RAIL} active={view} onPick={setView} />
{view === "triage" && (
<div className="board">
<TriageStream
triage={triage}
tab={tab}
setTab={setTab}
onLater={onLater}
onUndo={onUndo}
/>
<section className="card sp2">
<div className="ch">
<div className="ico">
<Icon name="spark" />
</div>
<div className="htext">
<h3> </h3>
<div className="sub"> </div>
</div>
</div>
<div className="nt-sumgrid">
<div className="nt-sumcell">
<b>{stats.total}</b>
<span></span>
</div>
<div className="nt-sumcell hi">
<b>{counts.now}</b>
<span> </span>
</div>
<div className="nt-sumcell">
<b>{counts.held}</b>
<span> </span>
</div>
<div className="nt-sumcell">
<b>{stats.filtered}</b>
<span></span>
</div>
</div>
<div className="sd-note">
<Icon name="spark" />
, .
</div>
</section>
</div>
)}
{view === "guard" && guard && digests && (
<GuardPanel guard={guard} digests={digests} onToggle={onToggleGuard} />
)}
{view === "senders" && senders && <SenderRules senders={senders} />}
</div>
</div>
);
}