|
|
/* 아리 — 일정(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 (
|
|
|
<svg className="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
|
strokeWidth={w || 1.9} strokeLinecap="round" strokeLinejoin="round">
|
|
|
{P[d].split("|").map((s, i) => <path key={i} d={s} />)}
|
|
|
</svg>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
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 (
|
|
|
<div className="minical">
|
|
|
<div className="mc-head">
|
|
|
<span className="mc-title">2026년 6월</span>
|
|
|
<span className="mc-nav"><button><I d="prev" /></button><button><I d="next" /></button></span>
|
|
|
</div>
|
|
|
<div className="mc-grid">
|
|
|
{C.weekdays.map((w) => <div key={w} className="mc-wd">{w}</div>)}
|
|
|
{cells.map((c, i) => (
|
|
|
<button key={i}
|
|
|
className={"mc-day" + (c.inMonth ? "" : " out") + (c.inMonth && c.n === C.today ? " today" : "") + (c.inMonth && c.n === sel ? " sel" : "")}
|
|
|
onClick={() => c.inMonth && onSel(c.n)}>
|
|
|
{c.n}
|
|
|
{c.inMonth && eventsByDay[c.n] && <span className="evdot" />}
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ---------- 이벤트 블록 (주/일) ---------- */
|
|
|
function EvBlock({ ev, onClick }) {
|
|
|
const top = topOf(ev.start), h = ((toMin(ev.end) - toMin(ev.start)) / 60) * PXH;
|
|
|
const short = h < 38;
|
|
|
return (
|
|
|
<div className={"ev tint-" + CAL[ev.cal].tone + (ev.soon ? " soon" : "") + (short ? " short" : "")}
|
|
|
style={{ top: top + "px", height: Math.max(h - 3, 20) + "px" }}
|
|
|
onClick={(e) => { e.stopPropagation(); onClick(ev, e); }}>
|
|
|
<div className="et">{ev.title}</div>
|
|
|
<div className="etm">{ev.start}–{ev.end}</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ---------- 집중 블록 (아리가 빈 시간에 배치한 작업) ---------- */
|
|
|
function FocusBlock({ b }) {
|
|
|
const top = topOf(b.start), h = ((toMin(b.end) - toMin(b.start)) / 60) * PXH;
|
|
|
const short = h < 40;
|
|
|
return (
|
|
|
<div className={"fblock " + b.type + (short ? " short" : "")}
|
|
|
style={{ top: top + "px", height: Math.max(h - 3, 22) + "px" }}>
|
|
|
<div className="fb-k"><I d="spark" />{b.tag}</div>
|
|
|
<div className="fbt">{b.title}</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ---------- 주 뷰 ---------- */
|
|
|
function WeekView({ events, focusBlocks, onOpen }) {
|
|
|
const nowTop = topOf("11:10"); // 오늘 6/8 가상 현재시각
|
|
|
return (
|
|
|
<div className="cpanel">
|
|
|
<div className="wk-head">
|
|
|
<div className="wk-corner" />
|
|
|
{C.week.map((day, i) => (
|
|
|
<div key={day} className={"wk-dh" + (day === C.today ? " today" : "") + (i === 0 || i === 6 ? " wknd" : "")}>
|
|
|
<div className="wd">{C.weekdays[i]}</div>
|
|
|
<div className="dn">{day}</div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
<div className="wk-body">
|
|
|
<div className="wk-times">
|
|
|
{HOURS.map((h) => <div key={h} className="wk-thour"><span>{h}:00</span></div>)}
|
|
|
</div>
|
|
|
{C.week.map((day, i) => {
|
|
|
const dayEvents = events.filter((e) => e.day === day);
|
|
|
const dayFocus = focusBlocks.filter((b) => b.day === day);
|
|
|
return (
|
|
|
<div key={day} className={"wk-col" + (i === 0 || i === 6 ? " wknd" : "")}>
|
|
|
{HOURS.map((h) => <div key={h} className="wk-hr" />)}
|
|
|
{day === C.today && <div className="wk-now" style={{ top: nowTop + "px" }} />}
|
|
|
{dayFocus.map((b) => <FocusBlock key={b.id} b={b} />)}
|
|
|
{dayEvents.map((ev) => <EvBlock key={ev.id} ev={ev} onClick={onOpen} />)}
|
|
|
</div>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ---------- 월 뷰 ---------- */
|
|
|
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 (
|
|
|
<div className="cpanel">
|
|
|
<div className="mo-head">
|
|
|
{C.weekdays.map((w, i) => <div key={w} className={"mo-wd" + (i === 0 || i === 6 ? " wknd" : "")}>{w}</div>)}
|
|
|
</div>
|
|
|
<div className="mo-grid">
|
|
|
{cells.map((c, i) => {
|
|
|
const evs = c.inMonth ? (byDay[c.n] || []) : [];
|
|
|
return (
|
|
|
<div key={i} className={"mo-cell" + (c.inMonth ? "" : " out") + (c.inMonth && c.n === C.today ? " today" : "")}
|
|
|
onClick={() => c.inMonth && onSelDay(c.n)}>
|
|
|
<div className="mo-dn">{c.n}</div>
|
|
|
{evs.slice(0, 3).map((ev) => (
|
|
|
<div key={ev.id} className={"mo-ev tint-" + CAL[ev.cal].tone} onClick={(e) => { e.stopPropagation(); onOpen(ev, e); }}>
|
|
|
<span className="pdot" /><span className="mt">{ev.start}</span><span className="mn">{ev.title}</span>
|
|
|
</div>
|
|
|
))}
|
|
|
{evs.length > 3 && <div className="mo-more">+{evs.length - 3}개 더</div>}
|
|
|
</div>
|
|
|
);
|
|
|
})}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ---------- 일 뷰 ---------- */
|
|
|
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 (
|
|
|
<div className="day-wrap">
|
|
|
<div className="cpanel">
|
|
|
<div className="day-grid">
|
|
|
<div className="wk-times">{HOURS.map((h) => <div key={h} className="wk-thour"><span>{h}:00</span></div>)}</div>
|
|
|
<div className="day-col">
|
|
|
{HOURS.map((h) => <div key={h} className="wk-hr" />)}
|
|
|
{day === C.today && <div className="wk-now" style={{ top: nowTop + "px" }} />}
|
|
|
{dayFocus.map((b) => <FocusBlock key={b.id} b={b} />)}
|
|
|
{dayEvents.map((ev) => <EvBlock key={ev.id} ev={ev} onClick={onOpen} />)}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div className="day-side">
|
|
|
{dayFocus.length > 0 && (
|
|
|
<div className="cpanel focus-card">
|
|
|
<div className="fc-head">
|
|
|
<div className="fc-ic"><I d="spark" /></div>
|
|
|
<div className="fc-h"><b>오늘의 집중 모드</b><span>빈 시간에 맞춰 자동 배치했어요</span></div>
|
|
|
</div>
|
|
|
<p className="fc-note">오전 미팅 사이엔 가벼운 일을, 오후 빈 블록엔 딥 워크를 넣었어요.</p>
|
|
|
{dayFocus.map((b) => (
|
|
|
<div className={"fc-item " + b.type} key={b.id}>
|
|
|
<div className="fci-time">{b.start}</div>
|
|
|
<div className="fci-body">
|
|
|
<div className="fci-top"><span className={"fci-tag " + b.type}>{b.tag}</span></div>
|
|
|
<div className="fci-title">{b.title}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
)}
|
|
|
<div className="cpanel agenda">
|
|
|
<div className="ag-head">
|
|
|
<div className="ag-title">6월 {day}일 {wd}요일</div>
|
|
|
<div className="ag-sub">일정 {dayEvents.length}개{day === C.today ? " · 오늘" : ""}</div>
|
|
|
</div>
|
|
|
{dayEvents.length === 0 && <div className="ag-empty">이 날은 일정이 없어요.</div>}
|
|
|
{dayEvents.map((ev) => (
|
|
|
<div key={ev.id} className={"ag-item tint-" + CAL[ev.cal].tone} onClick={(e) => onOpen(ev, e)}>
|
|
|
<div className="ag-time">{ev.start}<small>{ev.end}</small></div>
|
|
|
<div className="ag-body">
|
|
|
<div className="ag-t">{ev.title} {ev.soon && <span className="ag-soon">곧</span>}</div>
|
|
|
<div className="ag-m">
|
|
|
<span className="epop-cat"><span className="pdot" style={{ width: 7, height: 7, borderRadius: "50%", background: "var(--evc)", display: "inline-block" }} /> {CAL[ev.cal].name}</span>
|
|
|
{ev.loc && <><span>·</span><span><I d="pin" /> {ev.loc}</span></>}
|
|
|
</div>
|
|
|
{ev.people && (
|
|
|
<div className="ag-people">
|
|
|
{ev.people.slice(0, 3).map((p, i) => <div className="pp" key={i} style={{ background: ["var(--coral)", "var(--blue)", "var(--violet)"][i] }}>{p[0]}</div>)}
|
|
|
<span className="pl">{ev.people.join(", ")}</span>
|
|
|
</div>
|
|
|
)}
|
|
|
{ev.note && <div className="ag-note">{ev.note}</div>}
|
|
|
{ev.actions && (
|
|
|
<div className="ag-actions">
|
|
|
<div className="aga-head"><I d="spark" />내가 맡기로 한 일</div>
|
|
|
{ev.actions.map((a, i) => (
|
|
|
<div className="aga-row" key={i}><span className="aga-check"><I d="tick" w="3" /></span><span className="aga-text">{a.text}</span><span className="aga-who">{a.when}</span></div>
|
|
|
))}
|
|
|
</div>
|
|
|
)}
|
|
|
{C.meets[ev.id] && (
|
|
|
<button className="meet-open" onClick={(e) => { e.stopPropagation(); onMeet(ev); }}>
|
|
|
<I d="spark" />{meetLabel(C.meets[ev.id])}
|
|
|
</button>
|
|
|
)}
|
|
|
</div>
|
|
|
</div>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ---------- 이벤트 팝오버 ---------- */
|
|
|
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 (
|
|
|
<>
|
|
|
<div className="dp-backdrop" onClick={onClose} />
|
|
|
<div className={"epop tint-" + cat.tone} style={{ left: left + "px", top: top + "px" }}>
|
|
|
<div className="epop-bar" />
|
|
|
<div className="epop-title">{ev.title}</div>
|
|
|
<div className="epop-row"><I d="clock" /> {fmt(ev.start)} – {fmt(ev.end)}</div>
|
|
|
<div className="epop-row"><I d="cal" /> <span className="epop-cat"><span className="pdot" /> {cat.name}</span></div>
|
|
|
{ev.loc && <div className="epop-row"><I d="pin" /> {ev.loc}</div>}
|
|
|
{ev.people && <div className="epop-row"><I d="users" /> {ev.people.join(", ")}</div>}
|
|
|
{ev.note && <div className="epop-note">{ev.note}</div>}
|
|
|
{ev.actions && (
|
|
|
<div className="epop-actions">
|
|
|
<div className="epa-head"><I d="spark" />내가 맡기로 한 일<span className="n">{ev.actions.length}</span></div>
|
|
|
{ev.actions.map((a, i) => (
|
|
|
<div className="epa-row" key={i}>
|
|
|
<span className="epa-check"><I d="tick" w="3" /></span>
|
|
|
<span className="epa-text">{a.text}</span>
|
|
|
<span className="epa-who">{a.when}</span>
|
|
|
</div>
|
|
|
))}
|
|
|
<button className="epa-add"><I d="check" />내 작업에 추가</button>
|
|
|
</div>
|
|
|
)}
|
|
|
{C.meets[ev.id] && (
|
|
|
<button className="meet-open" onClick={() => onMeet(ev)}>
|
|
|
<I d="spark" />{meetLabel(C.meets[ev.id])}
|
|
|
</button>
|
|
|
)}
|
|
|
<div className="epop-acts">
|
|
|
<button className="edit"><I d="edit" /> 편집</button>
|
|
|
<button className="del" aria-label="삭제"><I d="trash" /></button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
/* ---------- 앱 ---------- */
|
|
|
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 (
|
|
|
<div className="tpage" ref={shellRef}>
|
|
|
<header className="topbar">
|
|
|
<div className="brand">
|
|
|
<div className="brand-mark"><I d="spark" /></div>
|
|
|
<div className="brand-tag"><b>아리</b><span>AI LIFE OS</span></div>
|
|
|
</div>
|
|
|
<nav className="mainnav">
|
|
|
{MAIN.map((m) => (
|
|
|
<a key={m.id} href={m.href} className={m.id === "cal" ? "active" : ""}>
|
|
|
<I d={m.icon} />{m.label}{m.badge && <span className="badge">{m.badge}</span>}
|
|
|
</a>
|
|
|
))}
|
|
|
</nav>
|
|
|
<div className="top-actions">
|
|
|
<button className="t-btn" aria-label="검색"><I d="search" /></button>
|
|
|
<button className="t-btn dot" aria-label="알림"><I d="bell" /></button>
|
|
|
<button className="t-btn" aria-label="테마" onClick={() => setTheme(theme === "light" ? "dark" : "light")}><I d={theme === "light" ? "moon" : "sun"} /></button>
|
|
|
<div className="t-ava">지</div>
|
|
|
</div>
|
|
|
</header>
|
|
|
|
|
|
<div className="twork">
|
|
|
<aside className="subnav">
|
|
|
<div className="sn-title">일정</div>
|
|
|
<button className="sn-new"><I d="plus" /> 새 일정</button>
|
|
|
<MiniCal sel={selDay} onSel={(n) => { setSelDay(n); if (view !== "day") setView("day"); }} eventsByDay={eventsByDay} />
|
|
|
<div className="sn-label">내 캘린더</div>
|
|
|
<div className="cal-list">
|
|
|
{C.cals.map((c) => (
|
|
|
<button key={c.id} className={"cal-toggle" + (calOn[c.id] ? "" : " off")} onClick={() => toggleCal(c.id)}>
|
|
|
<span className="cbx" style={{ background: `var(--${c.tone})` }}><I d="tick" w="3" /></span>
|
|
|
<span>{c.name}</span>
|
|
|
<span className="ct-n">{countByCal[c.id]}</span>
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
<div className="sn-foot">
|
|
|
<div className="av">지</div>
|
|
|
<div className="txt"><b>지우님</b><span>Pro 플랜</span></div>
|
|
|
</div>
|
|
|
</aside>
|
|
|
|
|
|
<main className="cmain">
|
|
|
<div className="chead">
|
|
|
<button className="ch-arrow" aria-label="이전"><I d="prev" /></button>
|
|
|
<button className="ch-arrow" aria-label="다음"><I d="next" /></button>
|
|
|
<h1 className="ch-title">{title}</h1>
|
|
|
<button className="ch-today" onClick={goToday}>오늘</button>
|
|
|
<span className="ch-spacer" />
|
|
|
<button className={"ch-focus" + (focusOn ? " on" : "")} onClick={() => setFocusOn((v) => !v)}>
|
|
|
<I d="spark" />집중 블록
|
|
|
</button>
|
|
|
<div className="view-seg">
|
|
|
{VIEWS.map((v) => (
|
|
|
<button key={v.id} className={view === v.id ? "on" : ""} onClick={() => setView(v.id)}>{v.label}</button>
|
|
|
))}
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div className="ai-strip">
|
|
|
<div className="ai-ic"><I d="spark" /></div>
|
|
|
<p>오늘 일정 중 <b>14:00 분기 전략 미팅</b>이 가장 중요해요. 미팅 사이 빈 시간엔 가벼운 일을, <b>오후 15:10 빈 블록</b>엔 ‘분기 리포트’ 딥 워크를 자동으로 배치해뒀어요. 16:00 ‘치과 예약’은 1:1과 가까워 15분 앞당기길 추천드려요.</p>
|
|
|
</div>
|
|
|
|
|
|
{view === "week" && <WeekView events={events} focusBlocks={focusBlocks} onOpen={openEv} />}
|
|
|
{view === "month" && <MonthView events={events} onOpen={openEv} onSelDay={(n) => { setSelDay(n); setView("day"); }} />}
|
|
|
{view === "day" && <DayView events={events} focusBlocks={focusBlocks} day={selDay} onOpen={openEv} onMeet={openMeet} />}
|
|
|
</main>
|
|
|
</div>
|
|
|
|
|
|
{pop && <EvPopover ev={pop.ev} pos={pop.pos} onClose={() => setPop(null)} onMeet={openMeet} />}
|
|
|
{meet && C.meets[meet.id] && (
|
|
|
<window.CalMeet.MeetDrawer ev={meet} meet={C.meets[meet.id]} calName={CAL[meet.cal].name} tone={CAL[meet.cal].tone} onClose={() => setMeet(null)} />
|
|
|
)}
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
|