/* 아리 — '오늘의 여정' (클린 화이트 테마) 하루를 4단계 흐름으로 보여주고, 작업↔사람↔미팅을 실시간 베지어 커넥터로 연결한다. 카드에 마우스를 올리면 연결된 흐름이 강조된다. 상단바·페이지헤더는 dash.css의 클린 화이트 언어를 그대로 사용. */ const { useState, useEffect, useRef, useLayoutEffect, useCallback, useMemo } = React; const J = window.AriJourney; /* ---------- 아이콘 ---------- */ const PATHS = { 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", 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", 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", 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", 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", search: "M21 21l-4.34-4.34|M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z", send: "M22 2 11 13|M22 2 15 22l-4-9-9-4z", zap: "M13 2 3 14h9l-1 8 10-12h-9z", 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", 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", plus: "M12 5v14|M5 12h14", share: "M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7|M16 6l-4-4-4 4|M12 2v13", star: "M12 2l2.9 6.3 6.9.8-5.1 4.7 1.4 6.8L12 18l-6 3.4 1.4-6.8L2.3 9.9l6.9-.8z", drop: "M12 2.7S5 10 5 14a7 7 0 0 0 14 0c0-4-7-11.3-7-11.3z", link: "M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1|M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1", }; function I({ d, w }) { return ( {PATHS[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 LEN = Math.PI * 64; function Gauge({ pct, color, val, unit, label, sub }) { return (
{val}
{unit}
{label}
{sub}
); } /* ---------- 컬럼 ---------- */ function Col({ stage, count, children }) { return (
{stage.title} {count || stage.n}
{children}
); } /* ==================================================================== 흐름 보드 — 동적 커넥터 ==================================================================== */ function Board({ tasks, toggle, hover, setHover, neighbors }) { const boardRef = useRef(null); const nodes = useRef({}); const [paths, setPaths] = useState([]); const [size, setSize] = useState({ w: 0, h: 0 }); const setNode = (id) => (el) => { if (el) nodes.current[id] = el; }; const compute = useCallback(() => { const board = boardRef.current; if (!board) return; const br = board.getBoundingClientRect(); const out = []; for (const lk of J.links) { const a = nodes.current[lk.from], b = nodes.current[lk.to]; if (!a || !b) continue; const ar = a.getBoundingClientRect(), bb = b.getBoundingClientRect(); const x1 = ar.right - br.left, y1 = ar.top - br.top + ar.height / 2; const x2 = bb.left - br.left, y2 = bb.top - br.top + bb.height / 2; const dx = Math.max(36, (x2 - x1) * 0.5); out.push({ key: lk.from + ">" + lk.to, from: lk.from, to: lk.to, c: lk.c, dash: lk.dash, d: `M${x1},${y1} C${x1 + dx},${y1} ${x2 - dx},${y2} ${x2},${y2}`, x1, y1, x2, y2, }); } setPaths(out); setSize({ w: br.width, h: br.height }); }, []); useLayoutEffect(() => { compute(); const raf = requestAnimationFrame(compute); const onR = () => compute(); window.addEventListener("resize", onR); const ro = new ResizeObserver(compute); if (boardRef.current) ro.observe(boardRef.current); if (document.fonts && document.fonts.ready) document.fonts.ready.then(compute); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", onR); ro.disconnect(); }; }, [compute, tasks]); const isLit = (node) => hover && (node === hover || (neighbors[hover] && neighbors[hover].has(node))); const cls = (node, base) => base + (isLit(node) ? " lit" : "") + (hover && !isLit(node) ? " mute" : ""); const pathState = (p) => (!hover ? "" : (p.from === hover || p.to === hover) ? " on" : " off"); const on = (node) => ({ onMouseEnter: () => setHover(node), onMouseLeave: () => setHover(null) }); return (
{paths.map((p) => ( ))} {/* 1 — 아침 준비 */} {J.cards.s1.map((c) => (
{c.title}
{c.meta}
{c.tag}
))} {/* 2 — 오전 집중 (할 일) */} !t.done).length}/4`}> {tasks.map((t) => { const pe = J.P[t.who]; return (
{ e.stopPropagation(); toggle(t.node); }}>
{t.title}
{t.meta}
{pe.ini}
); })} {/* 3 — 오후 협업 (미팅) */} {J.cards.s3.map((e) => { const pe = J.P[e.who]; return (
{pe.ini}
{e.title}
{e.meta}
{e.soon ? :
{e.time}
}
); })} {/* 4 — 마무리 & 내일 */}
{J.cards.s4.map((t) => (
{t.tt}
{t.ts}
))}
); } /* ==================================================================== 앱 ==================================================================== */ function App() { const [theme, setTheme] = useState("light"); const [tasks, setTasks] = useState(J.cards.s2.map((c) => ({ ...c }))); const [hover, setHover] = useState(null); const ref = useRef(null); useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]); useEffect(() => { const el = ref.current; const id = requestAnimationFrame(() => el && el.classList.add("entered")); return () => cancelAnimationFrame(id); }, []); const toggle = (node) => setTasks((t) => t.map((x) => (x.node === node ? { ...x, done: !x.done } : x))); const openCount = tasks.filter((t) => !t.done).length; const neighbors = useMemo(() => { const m = {}; for (const l of J.links) { (m[l.from] = m[l.from] || new Set()).add(l.to); (m[l.to] = m[l.to] || new Set()).add(l.from); } return m; }, []); return (
{/* ===== 상단바 ===== */}
아리AI LIFE OS
{/* ===== 페이지 헤더 ===== */}
6월 10일 화요일 맑음 · 한낮 28°

오늘의 여정

{/* ===== 흐름 패널 ===== */}
오늘의 흐름 · 4단계 · 12개 항목 · 5명 협업
{J.people.map((p) => ( ))}
+3
{J.stages.map((s) => (
{s.title}{s.time}
))}
카드에 마우스를 올리면 연결된 작업 · 사람 · 미팅 흐름이 강조돼요
{/* ===== 하단 2열 ===== */}
오늘의 작업
5건 · 진행중 2 · 검토 1 · 완료 1 · 예정 1
{J.rows.map((r, i) => { const pe = J.P[r.who]; return ( setHover(r.node)} onMouseLeave={() => setHover(null)}> ); })}
작업상태시작담당
{r.nm}
{r.stt} {r.date}
{pe.ini}
{pe.name}
오늘의 지표
집중 · 활동
오후 3시예요 — 물 한 잔, 5분 스트레칭 어때요?
); } ReactDOM.createRoot(document.getElementById("root")).render();