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.

261 lines
11 KiB
JavaScript

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.

/* 아리 — 알림 트리아지 페이지
"중요한 것만 울려요" — 지금/나중에/아리가 처리 3단 분류 + 집중 보호 + 발신자 규칙 */
const { useState } = React;
const I = window.AriShell.Icon;
const Topbar = window.AriShell.Topbar;
const SubRail = window.AriShell.SubRail;
const N = window.AriNotify;
/* 페이지 전용 아이콘 (각 JSX는 독립 스코프 — 필요한 path만 로컬 정의) */
const NT_P = {
msg: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",
card: "M2 5h20v14H2z|M2 10h20",
};
function NI({ d }) {
return (
<svg className="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor"
strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
{NT_P[d].split("|").map((s, i) => <path key={i} d={s} />)}
</svg>
);
}
const NT_SRC = {
mail: { local: false, d: "mail", tone: "var(--blue)" },
cal: { local: false, d: "cal", tone: "var(--violet)" },
chat: { local: true, d: "msg", tone: "var(--violet)" },
fin: { local: true, d: "card", tone: "var(--green)" },
app: { local: false, d: "grid", tone: "var(--ink-2)" },
};
function SrcIcon({ src }) {
const s = NT_SRC[src] || NT_SRC.app;
return (
<span className="nt-ic" style={{ color: s.tone }}>
{s.local ? <NI d={s.d} /> : <I d={s.d} />}
</span>
);
}
function WhyChip({ why, tone }) {
const t = tone === "muted" ? "var(--muted)" : "var(--" + tone + ")";
return (
<span className="nt-why" style={{ color: t, background: "color-mix(in oklab, " + t + " 13%, transparent)" }}>
{why}
</span>
);
}
const NT_TABS = [
{ id: "now", label: "지금" },
{ id: "later", label: "나중에" },
{ id: "held", label: "아리가 처리" },
];
const NT_CAP = {
now: "직접 봐야 하는 알림만 남겼어요. 나머지는 아리가 들고 있을게요.",
later: "급하지 않은 알림은 묶어서 다이제스트 시간에 전달돼요.",
held: "아리가 이미 처리한 알림이에요. 전부 되돌릴 수 있어요.",
};
function App() {
const [view, setView] = useState("triage");
const [tab, setTab] = useState("now");
const [buckets, setBuckets] = useState({ now: N.buckets.now, later: N.buckets.later, held: N.buckets.held });
const [guardOn, setGuardOn] = useState(N.guard.on);
const counts = {
now: buckets.now.length,
later: buckets.later.length + N.buckets.laterMore,
held: buckets.held.length + N.buckets.heldMore,
};
const toLater = (it) =>
setBuckets({
...buckets,
now: buckets.now.filter((x) => x.id !== it.id),
later: [{ ...it, why: "내가 미룸", tone: "muted" }, ...buckets.later],
});
const undo = (it) =>
setBuckets({
...buckets,
held: buckets.held.filter((x) => x.id !== it.id),
now: [{ ...it, why: "되돌림", tone: "amber" }, ...buckets.now],
});
const list = buckets[tab];
const more = tab === "later" ? N.buckets.laterMore : tab === "held" ? N.buckets.heldMore : 0;
const NT_RAIL = [
{ id: "triage", icon: "bell", label: "트리아지", dot: counts.now > 0 },
{ sep: true },
{ id: "guard", icon: "shield", label: "집중 보호 · 묶음" },
{ id: "senders", icon: "users", label: "발신자 규칙" },
];
const HEAD = {
triage: { title: "알림", em: "중요한 것만 울려요" },
guard: { title: "집중 보호", em: "조용히, 그래도 놓치지 않게" },
senders: { title: "발신자 규칙", em: "누구는 바로, 누구는 모아서" },
};
const h = HEAD[view];
return (
<div className="dash" data-screen-label="알림">
<Topbar current="noti" />
<div className="pagehead">
<div>
<div className="ph-eyebrow">
<span>오늘 {N.stats.total} 도착</span>
<span className="sep"></span>
<span>직접 {N.stats.seen}</span>
<span className="sep"></span>
<span>{N.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">
{/* 트리아지 스트림 */}
<section className="card sp2 r2">
<div className="ch">
<div className="ico"><I d="bell" /></div>
<div className="htext"><h3>트리아지</h3><div className="sub"> </div></div>
</div>
<div className="nt-tabs">
{NT_TABS.map((t) => (
<button key={t.id} className={"nt-tab" + (tab === t.id ? " on" : "")} onClick={() => setTab(t.id)}>
{t.label}<span className="n">{counts[t.id]}</span>
</button>
))}
</div>
<div className="nt-cap">{NT_CAP[tab]}</div>
<div className="nt-list">
{list.map((it) => (
<div className="nt-row" key={it.id}>
<SrcIcon src={it.src} />
<div className="nt-body">
<div className="nt-app">
<span>{it.app}</span>
<WhyChip why={it.why} tone={it.tone} />
</div>
<div className="nt-title">{it.title}</div>
<div className="nt-sum">{it.sum}</div>
{tab === "now" && (
<div className="nt-acts">
{it.act && <button className="nt-act">{it.act}</button>}
<button className="nt-act ghost" onClick={() => toLater(it)}>나중에</button>
</div>
)}
{tab === "held" && (
<div className="nt-acts">
<button className="nt-act ghost" onClick={() => undo(it)}>되돌리기</button>
</div>
)}
</div>
<span className="nt-time">{it.time}</span>
</div>
))}
{list.length === 0 && (
<div className="nt-empty"><I d="tick" />비어 있어요. 오늘은 조용한 날이네요.</div>
)}
</div>
{more > 0 && <div className="nt-more">비슷한 알림 +{more} 아리가 묶어뒀어요</div>}
</section>
{/* 오늘 요약 */}
<section className="card sp2">
<div className="ch">
<div className="ico"><I d="spark" /></div>
<div className="htext"><h3>오늘의 알림 요약</h3><div className="sub"> </div></div>
</div>
<div className="nt-sumgrid">
<div className="nt-sumcell"><b>{N.stats.total}</b><span></span></div>
<div className="nt-sumcell hi"><b>{counts.now}</b><span> </span></div>
<div className="nt-sumcell"><b>{N.buckets.held.length + N.buckets.heldMore}</b><span> </span></div>
<div className="nt-sumcell"><b>{N.stats.filtered}</b><span></span></div>
</div>
<div className="sd-note"><I d="spark" />지금 알림으로 울리고, 나머지는 묶음 배달 시간에 모아서 보여드려요.</div>
</section>
</div>
)}
{view === "guard" && (
<div className="board">
<section className="card sp2">
<div className="ch">
<div className="ico"><I d="shield" /></div>
<div className="htext"><h3>집중 보호 · 묶음 배달</h3><div className="sub">, </div></div>
</div>
<div className={"gd-hold" + (guardOn ? "" : " off")}>
<div className="gd-ic"><I d="target" /></div>
<div className="gd-body">
{guardOn ? (
<React.Fragment>
<div className="gd-t"> 워크 보호 {N.guard.until}까지</div>
<div className="gd-s">알림 {N.guard.held} 보류 · 끝나면 번에 전달해요</div>
</React.Fragment>
) : (
<React.Fragment>
<div className="gd-t">보호 꺼짐</div>
<div className="gd-s">모든 알림이 바로 도착해요 · 집중 모드를 켜면 다시 막아드려요</div>
</React.Fragment>
)}
</div>
<button className={"sw" + (guardOn ? " on" : "")} onClick={() => setGuardOn(!guardOn)} aria-label="집중 보호 켜기/끄기"></button>
</div>
<div className="gd-quiet">
<I d="moon" /><span>조용한 시간</span><b>{N.guard.quiet}</b>
<span className="hint">긴급·VIP만 통과</span>
</div>
<div className="gd-sec">묶음 배달 시간</div>
<div className="dg2-list">
{N.digests.map((d) => (
<div className={"dg2-row" + (d.done ? " done" : "")} key={d.time}>
<span className="dg2-time">{d.time}</span>
<div className="dg2-body">
<div className="dg2-name">{d.name}{d.done ? " — 전달됨" : ""}</div>
<div className="dg2-desc">{d.desc}</div>
</div>
<span className="dg2-n">{d.count}</span>
</div>
))}
</div>
</section>
</div>
)}
{view === "senders" && (
<div className="board">
<section className="card sp2">
<div className="ch">
<div className="ico"><I d="users" /></div>
<div className="htext"><h3>발신자 규칙</h3><div className="sub"> , </div></div>
<span className="count">{N.senders.length}</span>
</div>
<div className="sd-list">
{N.senders.map((s) => (
<div className="sd-row" key={s.who}>
<span className="sd-dot" style={{ background: "var(--" + s.tone + ")" }}></span>
<div className="sd-who">{s.who}<span>{s.from}</span></div>
<span className="sd-rule">{s.rule}</span>
</div>
))}
</div>
<div className="sd-note">
<I d="spark" />나중에 미룬 발신자가 쌓이면 아리가 규칙을 제안해요. 자동화 페이지에서 언제든 고칠 있어요.
</div>
</section>
</div>
)}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);