/* 아리 — 메일(Mail) · 클린 화이트 글래스 여러 계정 통합 수신 · 읽기 · 작성 + AI 비서가 할 일/일정/답장/정리를 미리 준비 */ const { useState, useEffect, useRef, useMemo } = React; const MD = window.MailData; const PPL = MD.people; const ACC = MD.acc; /* ---------- 아이콘 ---------- */ const P = { spark: "M12 3l1.9 5.2L19 10l-5.1 1.8L12 17l-1.9-5.2L5 10l5.1-1.8z|M19 4v3|M5 17v3", grid: "M3 3h7v7H3z|M14 3h7v7h-7z|M14 14h7v7h-7z|M3 14h7v7H3z", cal: "M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z", route: "M6 19a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|M18 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|M9 16h6a3 3 0 0 0 3-3V11", check: "M9 11l3 3L22 4|M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11", tick: "M20 6 9 17l-5-5", mail: "M22 7 13.03 12.7a1.94 1.94 0 0 1-2.06 0L2 7|M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z", brain: "M12 5a3 3 0 1 0-5.9.8A3 3 0 0 0 4 9.5 3 3 0 0 0 6 14a3 3 0 0 0 6 1zM12 5a3 3 0 1 1 5.9.8A3 3 0 0 1 20 9.5 3 3 0 0 1 18 14a3 3 0 0 1-6 1z|M12 5v14", heart: "M19 14c1.5-1.5 3-3.4 3-5.5A4.5 4.5 0 0 0 12 5.5 4.5 4.5 0 0 0 2 8.5c0 2.1 1.5 4 3 5.5l7 7z", inbox: "M22 12h-6l-2 3h-4l-2-3H2|M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z", star: "M12 2.5l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 18.8 6.2 21l1.1-6.5L2.6 9.8l6.5-.9z", send: "M22 2 11 13|M22 2 15 22l-4-9-9-4z", file: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|M14 2v6h6", archive: "M21 8v13H3V8|M1 3h22v5H1z|M10 12h4", search: "M21 21l-4.34-4.34|M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", bell: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9|M10.3 21a1.94 1.94 0 0 0 3.4 0", sun: "M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10z|M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4", moon: "M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z", plus: "M12 5v14M5 12h14", x: "M18 6 6 18M6 6l12 12", more: "M5 12a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM11 12a1 1 0 1 0 2 0 1 1 0 0 0-2 0zM17 12a1 1 0 1 0 2 0 1 1 0 0 0-2 0z", chev: "M9 6l6 6-6 6", reply: "M9 17l-5-5 5-5|M4 12h11a5 5 0 0 1 5 5v2", replyAll: "M7 17l-5-5 5-5|M12 17l-5-5 5-5|M7 12h9a5 5 0 0 1 5 5v2", forward: "M15 17l5-5-5-5|M20 12H9a5 5 0 0 0-5 5v2", trash: "M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6", clip: "M21 11.5 12 20a5.5 5.5 0 0 1-8-8l8.5-8a3.7 3.7 0 0 1 5.3 5.3l-8.6 8a1.9 1.9 0 0 1-2.7-2.7l8-7.5", refresh: "M21 12a9 9 0 1 1-3-6.7|M21 3v5h-5", pen: "M12 20h9|M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z", clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z|M12 7v5l3 2", user: "M20 21a8 8 0 0 0-16 0|M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z", arrow: "M5 12h14|M13 6l6 6-6 6", calPlus: "M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v6|M19 16v6M16 19h6", folder: "M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z", flag: "M4 22V4M4 4s1.5-1 5-1 5 2 8 2 4-1 4-1v10s-1.5 1-4 1-5-2-8-2-5 1-5 1", wand: "M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8 19 13M17.8 6.2 19 5M3 21l9-9M12.2 6.2 11 5", at: "M12 16a4 4 0 1 0 0-8 4 4 0 0 0 0 8z|M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.9 7.9", layers: "M12 2 2 7l10 5 10-5z|M2 17l10 5 10-5M2 12l10 5 10-5", zap: "M13 2 3 14h9l-1 8 10-12h-9z", }; function I({ d, w }) { return ( {P[d].split("|").map((s, i) => )} ); } const MAIN = [ { id: "dash", label: "대시보드", icon: "grid", href: "대시보드.html" }, { id: "sbox", label: "인박스", icon: "inbox", href: "인박스.html" }, { id: "appr", label: "결재함", icon: "spark", badge: "3", href: "결재함.html" }, { id: "auto", label: "자동화", icon: "zap", href: "자동화.html" }, { id: "journey", label: "여정", icon: "route", href: "여정.html" }, { id: "cal", label: "일정", icon: "cal", href: "일정.html" }, { id: "task", label: "작업", icon: "check", badge: "4", href: "작업.html" }, { id: "mail", label: "메일", icon: "mail", href: "메일.html" }, { id: "noti", label: "알림", icon: "bell", badge: "6", href: "알림.html" }, { id: "research", label: "리서치", icon: "brain", href: "리서치.html" }, { id: "trip", label: "여행", icon: "send", href: "여행.html" }, { id: "life", label: "라이프", icon: "heart", href: "라이프.html" }, { id: "wrap", label: "하루 마감", icon: "moon", href: "하루 마감.html" }, ]; function load(k, fb) { try { const v = localStorage.getItem("ariM." + k); return v === null ? fb : JSON.parse(v); } catch (e) { return fb; } } function save(k, v) { try { localStorage.setItem("ariM." + k, JSON.stringify(v)); } catch (e) {} } const aiCount = (m) => (m.ai ? m.ai.tasks.length + m.ai.events.length + (m.ai.replies.length ? 1 : 0) : 0); /* ================= 메일 목록 행 ================= */ function MailRow({ m, on, onClick, onStar }) { const p = PPL[m.from]; const a = ACC[m.account]; return (
{p.initial}
{p.name} {m.time}
{m.subject}
{m.preview}
{m.ai && (m.ai.priority === "높음" || aiCount(m) > 0) && (
{m.ai.priority === "높음" && 중요} {m.ai.tasks.length > 0 && 할 일 {m.ai.tasks.length}} {m.ai.events.length > 0 && 일정 {m.ai.events.length}} {m.ai.replies.length > 0 && 답장 추천}
)}
{m.hasAttach && }
); } /* ================= 제안 항목 (할 일 / 일정) ================= */ function Sug({ kind, text, meta, added, onAdd }) { return (
{text}
{meta}
); } /* ================= 아리 분석 패널 ================= */ function AriPanel({ m, added, onAdd, onReply, onAddAll }) { const ai = m.ai; const keys = []; ai.tasks.forEach((_, i) => keys.push("t:" + m.id + ":" + i)); ai.events.forEach((_, i) => keys.push("e:" + m.id + ":" + i)); if (ai.file) keys.push("f:" + m.id); const allDone = keys.length > 0 && keys.every((k) => added[k]); return (
아리가 분석했어요 {ai.category} · 새 메일을 읽고 필요한 일을 정리했어요
{ai.priority}
요약 — " + ai.summary }} /> {ai.tasks.length > 0 && (
할 일로 추가{ai.tasks.length}건
{ai.tasks.map((t, i) => { const k = "t:" + m.id + ":" + i; return ( onAdd(k, "작업", t.text)} meta={<> {t.project} {t.due} {t.prio} } /> ); })}
)} {ai.events.length > 0 && (
일정에 추가{ai.events.length}건
{ai.events.map((ev, i) => { const k = "e:" + m.id + ":" + i; return ( onAdd(k, "일정", ev.title)} meta={<> {ev.date} ({ev.day}) {ev.time}{ev.dur ? " · " + ev.dur : ""} {ev.place && <>{ev.place}} } /> ); })}
)} {ai.replies.length > 0 && (
답장 추천{ai.replies.length}개 초안
{ai.replies.map((r, i) => ( ))}
)} {ai.file && (() => { const k = "f:" + m.id; return (
{ai.file.project}에 정리{ai.file.reason}
); })()} {keys.length > 1 && (
아리가 준비한 {keys.length}개 작업을 한 번에 적용할 수 있어요
)}
); } /* ================= 리딩 패널 ================= */ function Reader({ m, added, onAdd, onReply, onAddAll, onCompose, onArchive, onStar }) { if (!m) { return (

읽을 메일을 선택하세요

메일을 열면 아리가 내용을 분석해 할 일·일정·답장을 미리 준비해 드려요.

); } const p = PPL[m.from], a = ACC[m.account]; return (

{m.subject}

{p.initial}
{p.name} {p.role && · {p.role}}
{p.email}
{m.date}
받는 사람: {m.to}
{a.name} · {a.email}
{m.body.map((para, i) =>

{para}

)}
{m.attachments && m.attachments.length > 0 && (
{m.attachments.map((at, i) => (
{at.name}{at.size}
))}
)} {m.ai && }
); } /* ================= 아리 정리 (트리아지) ================= */ function Triage({ emails, onOpen, onReply }) { const fresh = emails.filter((m) => !m.read && m.ai); const tasks = [], events = [], replies = []; fresh.forEach((m) => { m.ai.tasks.forEach((t) => tasks.push({ ...t, src: PPL[m.from].name, m })); m.ai.events.forEach((e) => events.push({ ...e, src: PPL[m.from].name, m })); if (m.ai.replies.length) replies.push({ src: PPL[m.from].name, subj: m.subject, r: m.ai.replies[0], m }); }); return (
아리 정리 · 오늘 아침

새 메일 {fresh.length}통을 읽고 할 일을 정리해뒀어요

세 계정에 새로 온 메일을 분석했어요. 중요도 순으로 보면, 분기 전략 미팅온보딩 시안 피드백이 오늘 가장 급해요. 아래에서 바로 작업·일정으로 옮기거나 답장을 보낼 수 있어요.

{tasks.length}할 일 제안
{events.length}일정 감지
{replies.length}답장 초안
할 일로 정리{tasks.length}
{tasks.length === 0 &&
정리할 할 일이 없어요
} {tasks.map((t, i) => (
onOpen(t.m.id)} style={{ cursor: "pointer" }}>
{t.src}님 메일에서
{t.text}
{t.project} {t.due}
))}
일정에 추가{events.length}
{events.length === 0 &&
감지된 일정이 없어요
} {events.map((e, i) => (
onOpen(e.m.id)} style={{ cursor: "pointer" }}>
{e.src}님 메일에서
{e.title}
{e.date} ({e.day}) {e.time}
))}
답장 초안 준비됨{replies.length}
{replies.length === 0 &&
추천할 답장이 없어요
} {replies.map((rp, i) => (
onReply(rp.m, rp.r)} style={{ cursor: "pointer" }}>
{rp.src}님께 · {rp.subj}
{rp.r.tone}
“{rp.r.preview}”
))}
); } /* ================= 작성 (Compose) ================= */ const AI_DRAFTS = { default: "안녕하세요,\n\n메일 잘 받았습니다. 내용 확인하고 빠른 시일 내 회신드리겠습니다.\n\n감사합니다.\n지우 드림", }; function Compose({ init, onClose, onSend }) { const [from, setFrom] = useState(init.from || "work"); const [to, setTo] = useState(init.to || ""); const [subject, setSubject] = useState(init.subject || ""); const [body, setBody] = useState(init.body || ""); const [busy, setBusy] = useState(false); const a = ACC[from]; const cycleFrom = () => { const ids = MD.accounts.map((x) => x.id); setFrom(ids[(ids.indexOf(from) + 1) % ids.length]); }; const aiAction = (label, fn) => { setBusy(true); setTimeout(() => { setBody(fn(body)); setBusy(false); }, 650); }; useEffect(() => { const h = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]); return ( <>