You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

385 lines
19 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* 아리 — '오늘의 여정' 대시보드 (RON 디자인 언어) */
const { useState, useEffect, useRef } = React;
/* ---------- 아이콘 ---------- */
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",
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",
wallet: "M19 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0 0 4h15a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5|M16 12h.01",
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",
zap: "M13 2 3 14h9l-1 8 10-12h-9z",
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",
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",
video: "M23 7l-7 5 7 5zM1 5h15a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H1a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z",
pen: "M12 20h9|M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z",
file: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|M14 2v6h6",
drop: "M12 2.7S5 10 5 14a7 7 0 0 0 14 0c0-4-7-11.3-7-11.3z",
foot: "M4 16c0-2 .5-4 2-4s2 1 2 3 .5 4-1 4-3-1-3-3z|M8 7c0-2 .5-3 1.5-3S11 5 11 7s0 3-1.5 3S8 9 8 7z|M14 16c0-2-.5-4-2-4s-2 1-2 3-.5 4 1 4 3-1 3-3z|M16 7c0-2-.5-3-1.5-3S13 5 13 7s0 3 1.5 3S16 9 16 7z",
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",
back: "M19 12H5|M12 19l-7-7 7-7",
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",
arrow: "M5 12h14|M13 6l6 6-6 6",
clock: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z|M12 7v5l3 2",
};
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 C = { blue: "#83a2db", blueD: "#6286c9", red: "#ce6969", navy: "#10141a", teal: "#5fa6a0", mauve: "#9c8bcb" };
/* ---------- 상단 메뉴 ---------- */
const NAV = [
{ id: "dash", label: "대시보드", icon: "grid", href: "아리 대시보드 (RON).html" },
{ id: "journey", label: "여정", icon: "route", href: "아리 대시보드 (RON v2).html" },
{ id: "cal", label: "일정", icon: "cal", href: "#" },
{ id: "task", label: "작업", icon: "check", badge: "4", href: "#" },
{ id: "mail", label: "메일", icon: "mail", href: "#" },
{ id: "life", label: "라이프", icon: "heart", href: "#" },
];
const RAIL = [
{ icon: "back", label: "뒤로" }, { sep: true },
{ icon: "grid", label: "오늘 개요", active: true },
{ icon: "cal", label: "일정" },
{ icon: "check", label: "할 일" },
{ icon: "target", label: "목표" },
{ icon: "heart", label: "건강" },
{ icon: "wallet", label: "지출" },
{ sep: true },
{ icon: "plus", label: "추가" },
];
/* ---------- 흐름 보드 데이터 ---------- */
const COL_MORNING = [
{ ic: "moon", title: "수면 7시간 12분", meta: "평소만큼 푹 잔 밤", tag: "컨디션 좋음", tool: "tick", toolC: "b" },
{ ic: "sun", title: "아침 브리핑 확인", meta: "맑음 24° · 출근 23분", tag: "4분 빠름", tool: "cal" },
];
const COL_FOCUS = [
{ id: "t1", title: "분기 리포트 초안 마무리", meta: "경영 · 우선", done: false },
{ id: "t2", title: "온보딩 와이어프레임 피드백", meta: "디자인 · 우선", done: false },
{ id: "t3", title: "신규 입사자 환영 메일 회신", meta: "팀", done: true },
{ id: "t4", title: "구독 결제 카드 갱신", meta: "개인", done: false },
];
const COL_MEET = [
{ time: "09:30", title: "팀 데일리 스탠드업", meta: "프로덕트 · 15분", av: C.blue, ini: "팀" },
{ time: "11:00", title: "디자인 리뷰", meta: "온보딩 · 45분", av: C.red, ini: "현" },
{ time: "14:00", title: "분기 전략 미팅", meta: "경영진 · 60분", av: C.navy, ini: "대", soon: true },
{ time: "16:30", title: "1:1 — 민서님", meta: "팀 · 30분", av: C.mauve, ini: "민" },
];
const COL_WRAP = [
{ hi: true, ic: "target", tt: "지금 집중", ts: "분기 리포트" },
{ ic: "heart", tt: "운동 30분", ts: "주 3/4회" },
{ ic: "brain", tt: "독서 2장", ts: "‘몰입’" },
{ ic: "wallet", tt: "가계부 정산", ts: "8% 절약" },
{ ic: "cal", tt: "내일 준비", ts: "일정 4건" },
{ ic: "moon", tt: "수면 모드", ts: "23:00" },
];
const COL_LABELS = [
{ t: "아침 루틴", s: "06:40 09:00", c: C.blue },
{ t: "오전 집중", s: "09:00 13:00", c: C.red },
{ t: "오후 미팅", s: "13:00 17:00", c: C.blue },
{ t: "마무리 & 내일", s: "17:00 23:00", c: C.navy },
];
/* 겹친 아바타 */
const STACK = [
{ ini: "대", c: C.navy, cnt: "2", cc: "b" },
{ ini: "현", c: C.red, cnt: "3", cc: "b" },
{ ini: "민", c: C.blue, cnt: "2", cc: "r" },
{ ini: "재", c: C.mauve, cnt: "1", cc: "r" },
{ ini: "수", c: C.teal },
];
/* 커넥터 라인 (gap center, y1, y2, color, dash) */
const CONN = [
{ c: 245, y1: 150, y2: 124, col: C.blue }, { c: 245, y1: 252, y2: 286, col: C.red, dash: true }, { c: 245, y1: 360, y2: 338, col: C.blue },
{ c: 499, y1: 160, y2: 138, col: C.blue }, { c: 499, y1: 270, y2: 300, col: C.red, dash: true }, { c: 499, y1: 378, y2: 356, col: C.blue, dash: true },
{ c: 753, y1: 150, y2: 172, col: C.red, dash: true }, { c: 753, y1: 262, y2: 238, col: C.blue }, { c: 753, y1: 360, y2: 384, col: C.red },
];
/* ---------- 데이터 테이블 ---------- */
const ROWS = [
{ star: true, nm: "분기 리포트 초안", st: "prog", stt: "진행중", date: "09:00", un: "지우", uc: C.navy },
{ nm: "온보딩 와이어프레임 피드백", st: "sched", stt: "예정", date: "11:00", un: "현우", uc: C.red },
{ star: true, nm: "신규 입사자 환영 메일", st: "done", stt: "완료", date: "08:30", un: "민서", uc: C.blue },
{ nm: "구독 결제 카드 갱신", st: "sched", stt: "예정", date: "18:00", un: "지우", uc: C.navy },
{ nm: "독서모임 책 2장 읽기", st: "sched", stt: "예정", date: "21:00", un: "지우", uc: C.navy },
];
/* ---------- 반원 게이지 ---------- */
const LEN = Math.PI * 64;
function Gauge({ pct, color, val, unit, label, sub }) {
return (
<div className="gauge">
<svg viewBox="0 0 160 104">
<path d="M16 88 A64 64 0 0 1 144 88" fill="none" stroke="var(--card-2)" strokeWidth="13" strokeLinecap="round" />
<path d="M16 88 A64 64 0 0 1 144 88" fill="none" stroke={color} strokeWidth="13" strokeLinecap="round"
strokeDasharray={`${(LEN * pct) / 100} ${LEN}`} />
</svg>
<div style={{ marginTop: "-50px", display: "flex", flexDirection: "column", alignItems: "center" }}>
<div className="gval">{val}</div>
<div className="gunit">{unit}</div>
</div>
<div className="glabel">{label}</div>
<div className="gsub">{sub}</div>
</div>
);
}
function App() {
const [theme, setTheme] = useState("light");
const [tasks, setTasks] = useState(COL_FOCUS);
const ref = useRef(null);
useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]);
useEffect(() => {
const el = ref.current;
const id = requestAnimationFrame(() => el && el.classList.add("enter"));
return () => cancelAnimationFrame(id);
}, []);
const toggle = (id) => setTasks((t) => t.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
const openCount = tasks.filter((t) => !t.done).length;
return (
<div className="app" ref={ref}>
{/* ===== 상단바 ===== */}
<header className="top">
<div className="brand">
<div className="brand-mark"><I d="spark" /></div>
<span className="brand-name">아리</span>
</div>
<nav className="nav">
{NAV.map((m) => (
<a key={m.id} href={m.href} className={m.id === "journey" ? "active" : ""}>
<I d={m.icon} />{m.label}
{m.badge && <span className="badge">{m.badge}</span>}
</a>
))}
</nav>
<div className="top-actions">
<button className="circ" aria-label="검색"><I d="search" /></button>
<button className="circ dot" aria-label="메일"><I d="mail" /></button>
<button className="circ dot" aria-label="알림"><I d="bell" /></button>
<button className="circ" aria-label="테마" onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
<I d={theme === "light" ? "moon" : "sun"} />
</button>
<div className="avatar"></div>
</div>
</header>
{/* ===== 무대 ===== */}
<div className="stage">
{/* 좌측 레일 */}
<aside className="rail">
{RAIL.map((r, i) =>
r.sep ? <span className="sep" key={i} /> : (
<button key={i} className={r.active ? "active" : ""} aria-label={r.label}>
<I d={r.icon} /><span className="tip">{r.label}</span>
</button>
)
)}
</aside>
{/* 메인 */}
<main className="main">
{/* 페이지 헤더 */}
<div className="pagehead">
<div className="ph-titles">
<h1 className="ph-title">오늘의 여정</h1>
<p className="ph-sub">좋은 아침이에요, <b>지우님</b> · 6 7 · <b>4</b>, <b>{openCount}</b></p>
</div>
<div className="ph-right">
<div className="ph-weather"><I d="sun" /><b>24°</b> · 28°</div>
</div>
</div>
{/* 흐름 보드 패널 */}
<section className="panel journey">
<div className="journey-top">
<div className="journey-label">오늘의 흐름 <span>· 4단계 · 12 항목</span></div>
<div className="stack">
{STACK.map((s, i) => (
<div className="av" key={i} style={{ background: s.c }}>
{s.ini}
{s.cnt && <span className={"cnt " + s.cc}>{s.cnt}</span>}
</div>
))}
<div className="more">+3</div>
</div>
<div className="journey-actions">
<button className="act"><I d="plus" /></button>
<button className="act"><I d="share" /></button>
<button className="act fill"><I d="cal" /></button>
</div>
</div>
<div className="board">
{/* 커넥터 */}
<svg className="connectors" viewBox="0 0 1000 460" preserveAspectRatio="none">
{CONN.map((k, i) => (
<g key={i}>
<path d={`M${k.c - 34} ${k.y1} C ${k.c - 6} ${k.y1}, ${k.c + 6} ${k.y2}, ${k.c + 34} ${k.y2}`}
fill="none" stroke={k.col} strokeWidth="1.8" strokeLinecap="round"
vectorEffect="non-scaling-stroke" opacity="0.6"
strokeDasharray={k.dash ? "1 6" : "none"} />
<circle cx={k.c - 34} cy={k.y1} r="3" fill={k.col} opacity="0.7" vectorEffect="non-scaling-stroke" />
<circle cx={k.c + 34} cy={k.y2} r="3" fill={k.col} opacity="0.7" vectorEffect="non-scaling-stroke" />
</g>
))}
</svg>
{/* 컬럼 1 — 아침 루틴 */}
<div className="col">
<div className="col-head"><span className="dot" style={{ background: C.blue }} /><span className="t">아침 루틴</span><span className="n">2</span></div>
<div className="col-body">
{COL_MORNING.map((c, i) => (
<div className="fcard" key={i}>
<div className="fcard-row">
<div className="fc-ic"><I d={c.ic} /></div>
<div className="fc-main"><div className="fc-title">{c.title}</div><div className="fc-meta">{c.meta}</div></div>
<div className={"mini " + (c.toolC || "")}><I d={c.tool} w="2.4" /></div>
</div>
<div className="fcard-foot"><span className="tag">{c.tag}</span></div>
</div>
))}
</div>
</div>
{/* 컬럼 2 — 오전 집중 (할 일) */}
<div className="col">
<div className="col-head"><span className="dot" style={{ background: C.red }} /><span className="t">오전 집중</span><span className="n">{openCount}/4</span></div>
<div className="col-body">
{tasks.map((t) => (
<div className={"fcard task" + (t.done ? " done" : "")} key={t.id} onClick={() => toggle(t.id)}>
<div className="fcard-row">
<div className="fcheck"><I d="tick" w="3" /></div>
<div className="fc-main"><div className="fc-title">{t.title}</div><div className="fc-meta">{t.meta}</div></div>
<div className="mini"><I d="cal" /></div>
</div>
</div>
))}
</div>
</div>
{/* 컬럼 3 — 오후 미팅 */}
<div className="col">
<div className="col-head"><span className="dot" style={{ background: C.blue }} /><span className="t">오후 미팅</span><span className="n">4</span></div>
<div className="col-body">
{COL_MEET.map((e, i) => (
<div className="fcard" key={i}>
<div className="fcard-row">
<div className="fc-av" style={{ background: e.av }}>{e.ini}</div>
<div className="fc-main"><div className="fc-title">{e.title}</div><div className="fc-meta">{e.meta}</div></div>
{e.soon ? <span className="mini r" style={{ width: "auto", padding: "0 9px", fontSize: "11px", fontWeight: 700 }}></span> : <div className="fc-time">{e.time}</div>}
</div>
</div>
))}
</div>
</div>
{/* 컬럼 4 — 마무리 & 내일 */}
<div className="col">
<div className="col-head"><span className="dot" style={{ background: C.navy }} /><span className="t">마무리 & 내일</span><span className="n">6</span></div>
<div className="col-body">
<div className="tiles">
{COL_WRAP.map((t, i) => (
<div className={"tile" + (t.hi ? " hi" : "")} key={i}>
<div className="tic"><I d={t.ic} /></div>
<div className="tt">{t.tt}</div>
<div className="ts">{t.ts}</div>
</div>
))}
</div>
</div>
</div>
{/* 말풍선 */}
<div className="bubble" style={{ top: "150px", left: "62%" }}>
<span className="bsm">다음 집중</span>
<b>분기 리포트</b>
</div>
</div>
{/* 컬럼 라벨 */}
<div className="col-labels">
{COL_LABELS.map((l, i) => (
<div className="cl" key={i} style={{ color: l.c === C.navy ? "var(--ink)" : l.c }}>{l.t}<span>{l.s}</span></div>
))}
</div>
</section>
{/* 하단 2열 */}
<section className="bottom">
{/* 데이터 테이블 */}
<div className="panel">
<div className="panel-head">
<div><div className="pt">오늘의 작업</div><div className="ps">5 · 1 · 3 · 1</div></div>
<div className="pa">
<button className="act"><I d="plus" /></button>
<button className="act"><I d="share" /></button>
<button className="act"><I d="cal" /></button>
</div>
</div>
<table className="table">
<thead>
<tr><th>작업</th><th></th><th></th><th></th></tr>
</thead>
<tbody>
{ROWS.map((r, i) => (
<tr key={i}>
<td>
<div className="t-subj">
<svg className={"ic star" + (r.star ? " on" : "")} viewBox="0 0 24 24" fill={r.star ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.7"><path d={P.star} /></svg>
<span className="nm">{r.nm}</span>
</div>
</td>
<td><span className={"chip " + r.st}>{r.stt}</span></td>
<td><span className="t-date">{r.date}</span></td>
<td>
<div className="t-user"><div className="ua" style={{ background: r.uc }}>{r.un[0]}</div><span className="un">{r.un}</span></div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 게이지 */}
<div className="panel">
<div className="panel-head">
<div><div className="pt">오늘의 지표</div><div className="ps"> · </div></div>
<div className="pa"><button className="act"><I d="more" /></button></div>
</div>
<div className="gauges">
<Gauge pct={87} color={C.blue} val="5.2" unit="시간" label="딥 워크" sub="목표 6시간" />
<Gauge pct={72} color={C.red} val="72" unit="%" label="활동 링" sub="7.2천 걸음" />
</div>
<div className="gauge-foot"><I d="drop" />오후 3시예요 , 5 스트레칭 어때요?</div>
</div>
</section>
</main>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);