/* 아리 — 일정(Calendar) · 주/월/일 뷰 */ const { useState, useEffect, useRef } = React; const C = window.CalData; const CAL = Object.fromEntries(C.cals.map((c) => [c.id, c])); 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", 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", 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", 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", 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", 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", prev: "M15 18l-6-6 6-6", next: "M9 18l6-6-6-6", clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 7v5l3 2", pin: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z|M12 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6z", users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2|M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z|M23 21v-2a4 4 0 0 0-3-3.87|M16 3.13a4 4 0 0 1 0 7.75", edit: "M12 20h9|M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z", trash: "M3 6h18|M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2", wand: "M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8 19 13M17.8 6.2 19 5M3 21l9-9M12.2 6.2 11 5", zap: "M13 2 3 14h9l-1 8 10-12h-9z", send: "M22 2 11 13|M22 2 15 22l-4-9-9-4z", target: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z|M12 12h.01", }; 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" }, ]; const VIEWS = [{ id: "week", label: "주" }, { id: "month", label: "월" }, { id: "day", label: "일" }]; const HOURS = []; for (let h = 7; h <= 22; h++) HOURS.push(h); const HSTART = 7, PXH = 52; const toMin = (t) => { const [h, m] = t.split(":").map(Number); return h * 60 + m; }; const fmt = (t) => { const [h, m] = t.split(":").map(Number); const ap = h < 12 ? "오전" : "오후"; const hh = h % 12 === 0 ? 12 : h % 12; return `${ap} ${hh}:${String(m).padStart(2, "0")}`; }; const topOf = (t) => ((toMin(t) - HSTART * 60) / 60) * PXH; const load = (k, fb) => { try { const v = localStorage.getItem("ariC." + k); return v === null ? fb : JSON.parse(v); } catch (e) { return fb; } }; const save = (k, v) => { try { localStorage.setItem("ariC." + k, JSON.stringify(v)); } catch (e) {} }; /* 회의 도우미 진입 버튼 레이블 */ const meetLabel = (m) => m.oneOnOne ? "1:1 어시스턴트 브리핑" : m.phase === "upcoming" ? "사전 브리핑 보기" : m.phase === "live" ? "실시간 기록 보기" : "회의 노트 · 액션 보기"; /* ---------- 미니 달력 ---------- */ function MiniCal({ sel, onSel, eventsByDay }) { const first = new Date(2026, 5, 1), off = first.getDay(); const cells = []; for (let i = 0; i < 42; i++) { const d = new Date(2026, 5, 1 - off + i); cells.push({ d, inMonth: d.getMonth() === 5, n: d.getDate() }); } return (
2026년 6월
{C.weekdays.map((w) =>
{w}
)} {cells.map((c, i) => ( ))}
); } /* ---------- 이벤트 블록 (주/일) ---------- */ function EvBlock({ ev, onClick }) { const top = topOf(ev.start), h = ((toMin(ev.end) - toMin(ev.start)) / 60) * PXH; const short = h < 38; return (
{ e.stopPropagation(); onClick(ev, e); }}>
{ev.title}
{ev.start}–{ev.end}
); } /* ---------- 집중 블록 (아리가 빈 시간에 배치한 작업) ---------- */ function FocusBlock({ b }) { const top = topOf(b.start), h = ((toMin(b.end) - toMin(b.start)) / 60) * PXH; const short = h < 40; return (
{b.tag}
{b.title}
); } /* ---------- 주 뷰 ---------- */ function WeekView({ events, focusBlocks, onOpen }) { const nowTop = topOf("11:10"); // 오늘 6/8 가상 현재시각 return (
{C.week.map((day, i) => (
{C.weekdays[i]}
{day}
))}
{HOURS.map((h) =>
{h}:00
)}
{C.week.map((day, i) => { const dayEvents = events.filter((e) => e.day === day); const dayFocus = focusBlocks.filter((b) => b.day === day); return (
{HOURS.map((h) =>
)} {day === C.today &&
} {dayFocus.map((b) => )} {dayEvents.map((ev) => )}
); })}
); } /* ---------- 월 뷰 ---------- */ function MonthView({ events, onOpen, onSelDay }) { const first = new Date(2026, 5, 1), off = first.getDay(); const byDay = {}; events.forEach((e) => { (byDay[e.day] = byDay[e.day] || []).push(e); }); Object.values(byDay).forEach((a) => a.sort((x, y) => toMin(x.start) - toMin(y.start))); const cells = []; for (let i = 0; i < 42; i++) { const d = new Date(2026, 5, 1 - off + i); cells.push({ d, inMonth: d.getMonth() === 5, n: d.getDate() }); } return (
{C.weekdays.map((w, i) =>
{w}
)}
{cells.map((c, i) => { const evs = c.inMonth ? (byDay[c.n] || []) : []; return (
c.inMonth && onSelDay(c.n)}>
{c.n}
{evs.slice(0, 3).map((ev) => (
{ e.stopPropagation(); onOpen(ev, e); }}> {ev.start}{ev.title}
))} {evs.length > 3 &&
+{evs.length - 3}개 더
}
); })}
); } /* ---------- 일 뷰 ---------- */ function DayView({ events, focusBlocks, day, onOpen, onMeet }) { const dayEvents = events.filter((e) => e.day === day).sort((a, b) => toMin(a.start) - toMin(b.start)); const dayFocus = focusBlocks.filter((b) => b.day === day).sort((a, b) => toMin(a.start) - toMin(b.start)); const idx = C.week.indexOf(day); const wd = idx >= 0 ? C.weekdays[idx] : C.weekdays[new Date(2026, 5, day).getDay()]; const nowTop = topOf("11:10"); return (
{HOURS.map((h) =>
{h}:00
)}
{HOURS.map((h) =>
)} {day === C.today &&
} {dayFocus.map((b) => )} {dayEvents.map((ev) => )}
{dayFocus.length > 0 && (
오늘의 집중 모드빈 시간에 맞춰 자동 배치했어요

오전 미팅 사이엔 가벼운 일을, 오후 빈 블록엔 딥 워크를 넣었어요.

{dayFocus.map((b) => (
{b.start}
{b.tag}
{b.title}
))}
)}
6월 {day}일 {wd}요일
일정 {dayEvents.length}개{day === C.today ? " · 오늘" : ""}
{dayEvents.length === 0 &&
이 날은 일정이 없어요.
} {dayEvents.map((ev) => (
onOpen(ev, e)}>
{ev.start}{ev.end}
{ev.title} {ev.soon && }
{CAL[ev.cal].name} {ev.loc && <>· {ev.loc}}
{ev.people && (
{ev.people.slice(0, 3).map((p, i) =>
{p[0]}
)} {ev.people.join(", ")}
)} {ev.note &&
{ev.note}
} {ev.actions && (
내가 맡기로 한 일
{ev.actions.map((a, i) => (
{a.text}{a.when}
))}
)} {C.meets[ev.id] && ( )}
))}
); } /* ---------- 이벤트 팝오버 ---------- */ function EvPopover({ ev, pos, onClose, onMeet }) { const cat = CAL[ev.cal]; const style = { tint: { "--evc": `var(--${cat.tone})` } }; let left = pos.x, top = pos.y; if (left + 300 > window.innerWidth - 12) left = window.innerWidth - 312; if (top + 280 > window.innerHeight - 12) top = window.innerHeight - 292; return ( <>
{ev.title}
{fmt(ev.start)} – {fmt(ev.end)}
{cat.name}
{ev.loc &&
{ev.loc}
} {ev.people &&
{ev.people.join(", ")}
} {ev.note &&
{ev.note}
} {ev.actions && (
내가 맡기로 한 일{ev.actions.length}
{ev.actions.map((a, i) => (
{a.text} {a.when}
))}
)} {C.meets[ev.id] && ( )}
); } /* ---------- 앱 ---------- */ function App() { const [theme, setTheme] = useState(() => load("theme", "light")); const [view, setView] = useState(() => load("view", "week")); const [selDay, setSelDay] = useState(() => load("selDay", C.today)); const [calOn, setCalOn] = useState(() => load("calOn", Object.fromEntries(C.cals.map((c) => [c.id, true])))); const [focusOn, setFocusOn] = useState(() => load("focusOn", true)); const [pop, setPop] = useState(null); const [meet, setMeet] = useState(null); const shellRef = useRef(null); useEffect(() => { document.documentElement.setAttribute("data-theme", theme); save("theme", theme); }, [theme]); useEffect(() => save("view", view), [view]); useEffect(() => save("selDay", selDay), [selDay]); useEffect(() => save("calOn", calOn), [calOn]); useEffect(() => save("focusOn", focusOn), [focusOn]); useEffect(() => { const el = shellRef.current; const id = requestAnimationFrame(() => el && el.classList.add("entered")); return () => cancelAnimationFrame(id); }, []); const events = C.events.filter((e) => calOn[e.cal]); const focusBlocks = focusOn ? C.focusBlocks : []; const eventsByDay = {}; events.forEach((e) => { eventsByDay[e.day] = (eventsByDay[e.day] || 0) + 1; }); const countByCal = {}; C.cals.forEach((c) => { countByCal[c.id] = C.events.filter((e) => e.cal === c.id).length; }); const openEv = (ev, e) => { setPop({ ev, pos: { x: e.clientX + 8, y: e.clientY + 8 } }); }; const openMeet = (ev) => { setPop(null); setMeet(ev); }; const toggleCal = (id) => setCalOn((s) => ({ ...s, [id]: !s[id] })); const goToday = () => { setSelDay(C.today); }; const title = view === "month" ? "2026년 6월" : view === "day" ? `6월 ${selDay}일` : "6월 7일 – 13일"; return (
아리AI LIFE OS

{title}

{VIEWS.map((v) => ( ))}

오늘 일정 중 14:00 분기 전략 미팅이 가장 중요해요. 미팅 사이 빈 시간엔 가벼운 일을, 오후 15:10 빈 블록엔 ‘분기 리포트’ 딥 워크를 자동으로 배치해뒀어요. 16:00 ‘치과 예약’은 1:1과 가까워 15분 앞당기길 추천드려요.

{view === "week" && } {view === "month" && { setSelDay(n); setView("day"); }} />} {view === "day" && }
{pop && setPop(null)} onMeet={openMeet} />} {meet && C.meets[meet.id] && ( setMeet(null)} /> )}
); } ReactDOM.createRoot(document.getElementById("root")).render();