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.

500 lines
25 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.

/* 아리 — 대시보드 v2 (Rond 클린 화이트 + 아리 결재함)
v2: '제안만 하는 AI' → '미리 해두고 승인/되돌리기만 받는 AI'
- ApprovalCard (approve.jsx) · 자율성 수준은 Tweaks로 조절 */
const { useState, useEffect, useRef } = React;
const D = window.AriData;
/* ---------- 아이콘 ---------- */
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",
settings: "M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z",
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",
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",
back: "M19 12H5|M12 19l-7-7 7-7",
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",
swap: "M16 3h5v5|M21 3l-7 7|M8 21H3v-5|M3 21l7-7",
up: "M12 19V5|M5 12l7-7 7 7",
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",
drop: "M12 2.7S5 10 5 14a7 7 0 0 0 14 0c0-4-7-11.3-7-11.3z",
zzz: "M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z|M16 4h4l-4 4h4",
arrow: "M5 12h14|M13 6l6 6-6 6",
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",
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",
mic: "M12 2a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z|M19 10v1a7 7 0 0 1-14 0v-1|M12 18v4M8 22h8",
image: "M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z|M8.5 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z|M21 15l-5-5L5 21",
pen: "M12 20h9|M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z",
layers: "M12 2 2 7l10 5 10-5z|M2 17l10 5 10-5M2 12l10 5 10-5",
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",
clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z|M12 7v5l3 2",
branch: "M6 3v12|M18 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z|M15 12H9a3 3 0 0 0-3 3",
play: "M6 3l15 9-15 9z",
flag: "M4 22V4M4 4s1.5-1 5-1 5 2 8 2 4-1 4-1v10s-1.5 1-4 1-5-2-8-2-5 1-5 1",
rain: "M16 13v6M8 13v6M12 15v6|M20 16.6A5 5 0 0 0 18 7h-1.3A8 8 0 1 0 4 14.9",
plus: "M12 5v14M5 12h14",
scaffold: "M3 9h18M9 21V9|M4 4h16a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z",
};
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 TONE = { blue: "var(--blue)", violet: "var(--violet)", coral: "var(--coral)", green: "var(--green)" };
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 CURRENT = "dash";
const RAIL = [
{ icon: "grid", label: "오늘 개요" },
{ icon: "cal", label: "일정" },
{ icon: "check", label: "할 일" },
{ icon: "mail", label: "메일" },
{ icon: "target", label: "목표" },
{ icon: "heart", label: "건강" },
{ icon: "wallet", label: "지출" },
{ icon: "zap", label: "자동화" },
{ icon: "brain", label: "리서치" },
];
const CHIPS = [
{ icon: "cal", text: "내일 오후 비워줘" },
{ icon: "mail", text: "중요 메일만 요약" },
{ icon: "video", text: "14시 미팅 준비" },
];
const PRIO = { 높음: "high", 보통: "mid", 낮음: "low" };
const AVCOL = ["var(--coral)", "var(--blue)", "var(--violet)"];
/* 요약 카드용 데이터 — 실행은 전용 페이지(결재함·스마트 인박스)에서 */
const AP = window.AriApprovals;
const SB = window.AriSmartInbox;
const apPending = AP.items.filter((i) => i.risk === "high");
function App() {
const [theme, setTheme] = useState("light");
const [tasks, setTasks] = useState(D.tasks);
const shellRef = useRef(null);
useEffect(() => { document.documentElement.setAttribute("data-theme", theme); }, [theme]);
useEffect(() => {
const el = shellRef.current;
const id = requestAnimationFrame(() => el && el.classList.add("entered"));
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="dash" 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={CURRENT === m.id ? "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="mail" /></button>
<button className="t-btn" aria-label="테마" onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
<I d={theme === "light" ? "moon" : "sun"} />
</button>
<div className="t-ava">{D.user.initial}</div>
</div>
</header>
{/* ===== 페이지 헤더 ===== */}
<div className="pagehead">
<div>
<div className="ph-eyebrow">
<span>{D.today}</span>
<span className="sep" />
<span className="wx"><I d="sun" />{D.weather.cond}</span>
</div>
<h1 className="ph-title">좋은 아침이에요, {D.user.name} <em></em></h1>
</div>
<div className="ph-search">
<I d="search" />
<input placeholder="아리에게 무엇이든…" />
</div>
</div>
{/* ===== 작업 영역 ===== */}
<div className="work">
{/* 보드 — 베n토 그리드 */}
<div className="board">
{/* 히어로 — 브리핑 + 명령 */}
<section className="card hero sp2">
<span className="hero-spark"><I d="spark" />아리 브리핑</span>
<p className="brief" dangerouslySetInnerHTML={{ __html: D.briefingNote }} />
<div className="chips">
{CHIPS.map((c, i) => (
<button className="chip" key={i}><I d={c.icon} />{c.text}</button>
))}
</div>
<div className="cmd">
<I d="spark" />
<input placeholder="오늘 하루, 무엇이든 맡겨보세요…" />
<button className="send" aria-label="보내기"><I d="send" /></button>
</div>
</section>
{/* 결재함 요약 — 실행은 전용 페이지에서 */}
<section className="card">
<div className="ch">
<div className="ico lime"><I d="spark" /></div>
<div className="htext"><h3>아리 결재함</h3><div className="sub"> {AP.savedToday} </div></div>
<span className="count warm">대기 {apPending.length}</span>
</div>
<div className="mini-list">
{apPending.map((it) => (
<div className="mini-row" key={it.id}>
<span className="mini-ic" style={{ "--tone": `var(--${it.tone})` }}><I d={it.icon} /></span>
<span className="mini-text">{it.title}</span>
<span className="mini-sub">{it.time}</span>
</div>
))}
</div>
<a className="mini-cta" href="결재함.html"><I d="tick" w="3" />결재함에서 승인하기</a>
</section>
{/* 인박스 요약 — 캡처·분류는 전용 페이지에서 */}
<section className="card">
<div className="ch">
<div className="ico"><I d="inbox" /></div>
<div className="htext"><h3>스마트 인박스</h3><div className="sub"> </div></div>
<span className="count">오늘 {SB.todayRouted}</span>
</div>
<div className="mini-list">
{SB.items.slice(0, 3).map((c) => (
<div className="mini-row" key={c.id}>
<span className="mini-ic" style={{ "--tone": `var(--${c.route.tone})` }}><I d={c.kind === "voice" ? "mic" : c.kind === "image" ? "image" : "pen"} /></span>
<span className="mini-text">{c.raw}</span>
<span className="mini-sub">{c.route.typeLabel}</span>
</div>
))}
</div>
<a className="mini-cta" href="인박스.html"><I d="plus" />새로 적기 · 인박스 열기</a>
</section>
{/* 오늘의 집중 모드 — 캘린더 빈 블록에 맞춰 큐레이션 */}
<section className="card foc r2">
<div className="ch">
<div className="ico"><I d="target" /></div>
<div className="htext"><h3>오늘의 집중</h3><div className="sub">Focus of the Day</div></div>
<span className="count">4블록</span>
</div>
<p className="foc-note" dangerouslySetInnerHTML={{ __html: D.focus.note }} />
<div className="foc-track">
{D.focus.blocks.map((b, i) => (
<div className={"foc-block " + b.type} key={i}>
<div className="fb-time">{b.time}</div>
<div className="fb-card">
<div className="fb-top">
<span className={"fb-tag " + b.type}>{b.tag}</span>
<span className="fb-len">{b.len}</span>
</div>
<div className="fb-title">{b.title}</div>
<div className="fb-gap"><I d="clock" />{b.gap}</div>
</div>
</div>
))}
</div>
</section>
{/* 오늘 일정 */}
<section className="card r2">
<div className="ch">
<div className="ico"><I d="cal" /></div>
<div className="htext"><h3>오늘 일정</h3><div className="sub">4 · 2</div></div>
<button className="tool"><I d="more" /></button>
</div>
<div className="sched">
{D.schedule.map((e, i) => (
<div className="ev" key={i}>
<div className="ev-time">{e.time}</div>
<div className="ev-body" style={{ "--accent": TONE[e.tone] }}>
<div className="ev-title">{e.title}</div>
<div className="ev-meta"><span>{e.tag}</span><span className="dur">{e.dur}</span></div>
</div>
{e.soon && <span className="soon"></span>}
</div>
))}
<div className="sched-note"><I d="bell" />치과 예약은 16:00 옮겨뒀어요 결재함에서 되돌릴 있어요.</div>
</div>
</section>
{/* 할 일 */}
<section className="card r2">
<div className="ch">
<div className="ico"><I d="check" /></div>
<div className="htext"><h3> </h3></div>
<span className="count">{openCount} 남음</span>
</div>
<div className="tasks">
{tasks.map((t, idx) => {
const focus = !t.done && idx === 0;
return (
<div key={t.id} className={"task" + (t.done ? " done" : "") + (focus ? " focus" : "")} onClick={() => toggle(t.id)}>
<div className="box"><I d="tick" w="3" /></div>
<div className="task-body">
<div className="task-title">{t.title}</div>
<div className="task-meta">{t.project}</div>
</div>
{focus
? <span className="focus-tag">지금</span>
: !t.done && <span className={"prio " + PRIO[t.prio]}>{t.prio}</span>}
</div>
);
})}
</div>
</section>
{/* 미팅 준비 */}
<section className="card">
<div className="ch">
<div className="ico"><I d="video" /></div>
<div className="htext"><h3>미팅 준비</h3></div>
</div>
<div className="mtg-when">{D.meeting.title} · {D.meeting.when}</div>
<div className="mtg-people">
{D.meeting.people.slice(0, 3).map((p, i) => (
<div className="av" key={i} style={{ background: AVCOL[i] }}>{p[0]}</div>
))}
<span className="more">{D.meeting.people.join(", ")}</span>
</div>
<ul className="mtg-brief">
{D.meeting.brief.map((b, i) => (
<li key={i}><span className="dot" />{b}</li>
))}
</ul>
<div className="mtg-actions">
<div className="ma-head"><I d="spark" />내가 맡기로 · {D.meeting.recap}</div>
{D.meeting.actions.map((a, i) => (
<div className="ma-row" key={i}>
<span className="ma-check"><I d="tick" w="3" /></span>
<span className="ma-text">{a.text}</span>
<span className="ma-meta">{a.when}</span>
</div>
))}
</div>
<button className="mtg-cta"><I d="spark" />브리핑 열기</button>
</section>
{/* 위임 현황 — 매니지먼트 & 위임 */}
<section className="card deleg">
<div className="ch">
<div className="ico"><I d="users" /></div>
<div className="htext"><h3>위임 현황</h3><div className="sub"> </div></div>
</div>
<div className="dg-suggest">
<div className="dg-s-ic"><I d="spark" /></div>
<p>오늘 일정이 {D.delegation.suggest.task} 직접 하기 빠듯해요. {D.delegation.suggest.proj} 함께 하는 <b>{D.delegation.suggest.who}</b> .</p>
</div>
<div className="dg-list">
{D.delegation.waiting.map((w, i) => (
<div className={"dg-row" + (w.review ? " review" : "")} key={i}>
<div className="dg-av" style={{ background: w.c }}>{w.initial}</div>
<div className="dg-body">
<div className="dg-task">{w.task}</div>
<div className="dg-sub">{w.who} · {w.note}</div>
</div>
<span className={"dg-state" + (w.review ? " review" : "")}>{w.state}</span>
</div>
))}
</div>
</section>
{/* 이어가기 & 우선순위 — 개인 프로젝트 스마트 어시스턴트 */}
<section className="card resume">
<div className="ch">
<div className="ico"><I d="play" /></div>
<div className="htext"><h3>이어서 하기</h3><div className="sub"> </div></div>
</div>
<div className="rs-card">
<div className="rs-top">
<span className="rs-proj">{D.resume.project}</span>
<span className="rs-ago">{D.resume.ago}</span>
</div>
<p className="rs-ctx">{D.resume.context}</p>
<div className="rs-next"><span className="rs-next-lb">다음</span>{D.resume.next}</div>
<button className="rs-cta"><I d="play" />이어서 작업하기</button>
</div>
<div className="env-nudge">
<div className="en-ic"><I d="rain" /></div>
<div className="en-body">
<div className="en-task">{D.envNudge.task}</div>
<div className="en-reason">{D.envNudge.reason}</div>
</div>
<span className="en-flag"><I d="flag" />최우선</span>
</div>
</section>
{/* 이번 주 집중도 */}
<section className="card">
<div className="ch">
<div className="ico"><I d="zap" /></div>
<div className="htext"><h3>이번 집중도</h3><div className="sub"> </div></div>
<button className="tool"><I d="more" /></button>
</div>
<div className="focus-stat">
<b>27.4</b><span className="u"></span>
<span className="delta"><I d="up" />+12%</span>
</div>
<div className="chart">
<svg viewBox="0 0 300 130" preserveAspectRatio="none">
<defs>
<linearGradient id="fg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="var(--ink)" stopOpacity="0.16" />
<stop offset="1" stopColor="var(--ink)" stopOpacity="0" />
</linearGradient>
</defs>
<path d="M6 96 L48 80 L90 88 L132 54 L174 64 L216 36 L258 48 L294 26 L294 130 L6 130 Z" fill="url(#fg)" />
<polyline points="6,96 48,80 90,88 132,54 174,64 216,36 258,48 294,26"
fill="none" stroke="var(--ink)" strokeWidth="2.2" strokeLinejoin="round" strokeLinecap="round" />
<circle cx="216" cy="36" r="4" fill="var(--ink)" stroke="var(--card)" strokeWidth="2.5" />
</svg>
<div className="chart-x">
{["월","화","수","목","금","토","일"].map((d) => <span key={d}>{d}</span>)}
</div>
</div>
</section>
{/* 목표 */}
<section className="card">
<div className="ch">
<div className="ico"><I d="target" /></div>
<div className="htext"><h3>목표</h3></div>
</div>
<div className="goals">
{D.goals.map((g, i) => (
<div key={i}>
<div className="goal-top">
<span className="goal-title">{g.title}</span>
<span className="goal-pct">{g.pct}%</span>
</div>
<div className="goal-bar"><i style={{ width: g.pct + "%", background: g.c }} /></div>
<div className="goal-sub">{g.sub}</div>
</div>
))}
</div>
</section>
{/* 메일 요약 */}
<section className="card">
<div className="ch">
<div className="ico"><I d="mail" /></div>
<div className="htext"><h3>메일 요약</h3></div>
<span className="count">{D.emailCount}</span>
</div>
<div className="mails">
{D.emails.map((m, i) => (
<div className="mail-row" key={i}>
<span className="mail-dot" style={{ background: TONE[m.tone] }} />
<div className="mail-body">
<div className="mail-from">
<b>{m.from}</b>
{m.task && <span className="mail-task"> </span>}
<span className="t">{m.time}</span>
</div>
<div className="mail-subj">{m.subj}</div>
<div className="mail-sum">{m.summary}</div>
</div>
</div>
))}
</div>
</section>
{/* 건강 */}
<section className="card">
<div className="ch">
<div className="ico"><I d="heart" /></div>
<div className="htext"><h3>건강 & 웰니스</h3></div>
</div>
<div className="rings">
{D.health.rings.map((r, i) => {
const ico = { footprints: "foot", moonZz: "zzz", droplet: "drop" }[r.icon];
return (
<div className="ring" key={i}>
<div className="dial" style={{ background: `conic-gradient(${r.c} ${r.pct}%, var(--card-2) 0)` }}>
<I d={ico} />
</div>
<div className="rv">{r.val}</div>
<div className="rl">{r.label}</div>
</div>
);
})}
</div>
<div className="health-nudge"><I d="drop" />{D.health.nudge}</div>
</section>
{/* 지출 */}
<section className="card">
<div className="ch">
<div className="ico"><I d="wallet" /></div>
<div className="htext"><h3>이번 지출</h3></div>
</div>
<div className="fin-big"><b>{D.finance.spentPct}%</b><span className="u"> </span></div>
<div className="fin-delta">{D.finance.delta}</div>
<div className="fin-bar"><i style={{ width: D.finance.spentPct + "%" }} /></div>
<div className="fin-range">
<span>{D.finance.spent}</span><span>{D.finance.budget}</span>
</div>
<div className="fin-subs">
{D.finance.subs.map((s, i) => (
<div className="fin-sub" key={i}>
<span className="nm">{s.name}</span>
<span className="dt">{s.date}</span>
<span className="amt">{s.amt}</span>
</div>
))}
</div>
</section>
</div>
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);