/* 아리 — 일정(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) => (
c.inMonth && onSel(c.n)}>
{c.n}
{c.inMonth && eventsByDay[c.n] && }
))}
);
}
/* ---------- 이벤트 블록 (주/일) ---------- */
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 (
);
}
/* ---------- 주 뷰 ---------- */
function WeekView({ events, focusBlocks, onOpen }) {
const nowTop = topOf("11:10"); // 오늘 6/8 가상 현재시각
return (
{C.week.map((day, i) => (
))}
{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) => (
))}
)}
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] && (
{ e.stopPropagation(); onMeet(ev); }}>
{meetLabel(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] && (
onMeet(ev)}>
{meetLabel(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 (
{title}
오늘
setFocusOn((v) => !v)}>
집중 블록
{VIEWS.map((v) => (
setView(v.id)}>{v.label}
))}
오늘 일정 중 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( );