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.

676 lines
35 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden 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.

/* 아리 — 라이프 케어(Life Care)
건강·웰니스 / 금융·소비 / 지식·학습(제2의 뇌)
아리가 각 영역의 데이터를 읽고 분석·코칭·리마인더를 미리 준비해 둔다. */
const { useState, useEffect, useRef, useMemo } = React;
const L = window.LifeData;
/* ---------------- 아이콘 ---------------- */
const P = {
spark: "M12 3l1.9 4.6L18.5 9.5l-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9z|M19 13l.7 1.8 1.8.7-1.8.7-.7 1.8-.7-1.8-1.8-.7 1.8-.7z",
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",
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.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7z",
wallet: "M19 7V5a2 2 0 0 0-2-2H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4|M18 12a1 1 0 0 0 0 2h3v-2z",
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",
chev: "M9 6l6 6-6 6",
arrow: "M5 12h14|M13 6l6 6-6 6",
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",
refresh: "M21 12a9 9 0 1 1-3-6.7|M21 3v5h-5",
tick: "M20 6 9 17l-5-5",
clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z|M12 7v5l3 2",
zap: "M13 2 3 14h9l-1 8 10-12h-9z",
send: "M22 2 11 13|M22 2 15 22l-4-9-9-4z",
steps: "M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0zM20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0z",
flame: "M8.5 14.5A2.5 2.5 0 0 0 11 17a2.5 2.5 0 0 0 2.5-2.5c0-1.38-.5-2-1-3-1.07-2.14-.22-4.05 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.15.43-2.29 1-3a2.5 2.5 0 0 0 2.5 2.5z",
water: "M12 2.7S6 9.5 6 14a6 6 0 0 0 12 0c0-4.5-6-11.3-6-11.3z",
stretch: "M22 12h-4l-3 9L9 3l-3 9H2",
pill: "M10.5 20.5 3.5 13.5a5 5 0 0 1 7-7l7 7a5 5 0 0 1-7 7z|M8.5 8.5l7 7",
feather: "M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z|M16 8 2 22|M17.5 15H9",
food: "M3 2v7c0 1.1.9 2 2 2a2 2 0 0 0 2-2V2M5 2v20|M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3zm0 0v7",
bag: "M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z|M3 6h18|M16 10a4 4 0 0 1-8 0",
car: "M5 17a2 2 0 1 0 4 0 2 2 0 0 0-4 0zM15 17a2 2 0 1 0 4 0 2 2 0 0 0-4 0z|M5 17H3v-4l2-5h11l3 5v4h-2M5 13h14",
coffee: "M17 8h1a4 4 0 1 1 0 8h-1|M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4z|M6 2v2M10 2v2M14 2v2",
repeat: "M17 2l4 4-4 4M3 11V9a4 4 0 0 1 4-4h14M7 22l-4-4 4-4M21 13v2a4 4 0 0 1-4 4H3",
ticket: "M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2z|M13 5v2M13 17v2M13 11v2",
play: "M6 3l15 9-15 9z",
music: "M9 18V5l12-2v13|M6 18a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
alert: "M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h16.9a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z|M12 9v4M12 17h.01",
trending: "M22 7l-8.5 8.5-5-5L2 17|M16 7h6v6",
article: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z|M14 2v6h6|M9 13h6M9 17h5",
note: "M12 20h9|M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z",
idea: "M9 18h6|M10 22h4|M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.1V17h6v-.2c0-.8.4-1.6 1-2.1A7 7 0 0 0 12 2z",
highlight: "M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z",
folder: "M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
link: "M9 17H7A5 5 0 0 1 7 7h2M15 7h2a5 5 0 0 1 0 10h-2M8 12h8",
layers: "M12 2 2 7l10 5 10-5z|M2 17l10 5 10-5M2 12l10 5 10-5",
};
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 MODULES = [
{ id: "today", label: "오늘의 라이프", sub: "통합 코칭", icon: "spark", tone: "coral" },
{ id: "health", label: "건강 & 웰니스", sub: "활동·수면·습관", icon: "heart", tone: "coral" },
{ id: "finance", label: "금융 & 소비", sub: "예산·구독·자산", icon: "wallet", tone: "blue" },
{ id: "knowledge", label: "지식 & 학습", sub: "제2의 뇌", icon: "brain", tone: "violet" },
];
function load(k, fb) { try { const v = localStorage.getItem("ariL." + k); return v === null ? fb : JSON.parse(v); } catch (e) { return fb; } }
function save(k, v) { try { localStorage.setItem("ariL." + k, JSON.stringify(v)); } catch (e) {} }
const won = (n) => "₩" + n.toLocaleString("ko-KR");
/* ===================== 액티비티 링 ===================== */
function Ring({ r }) {
return (
<div className="ring-card">
<div className="ring" style={{ "--p": r.pct, "--c": `var(--${r.tone})` }}>
<div className="r-in">
<I d={r.icon} />
<b>{r.val}</b>
</div>
</div>
<div className="rc-label">{r.label}{r.unit && <span className="t-muted"> {r.unit}</span>}</div>
<div className="rc-goal">목표 {r.goal}</div>
</div>
);
}
/* ===================== 건강 모듈 ===================== */
function HealthModule({ habits, onHabit, toast }) {
const h = L.health;
return (
<>
<div className="mod-head">
<div className="mh-ic bg-coral"><I d="heart" /></div>
<div className="mh-txt">
<h2>건강 & 웰니스</h2>
<div className="mh-sub">Apple Health · Apple Watch 연동 · 오늘 {new Date().getHours()} 기준</div>
</div>
<div className="mh-act">
<span className="c-act" onClick={() => toast("건강 리포트를 생성하고 있어요", "coral")}><I d="layers" />주간 리포트</span>
</div>
</div>
<div className="card glassy" style={{ borderColor: "color-mix(in oklab, var(--coral) 22%, var(--line-2))" }}>
<div style={{ display: "flex", gap: 13, alignItems: "flex-start" }}>
<div className="ch-ava" style={{ width: 38, height: 38, borderRadius: 11, background: "var(--coral)", color: "#fff", display: "grid", placeItems: "center", flexShrink: 0, boxShadow: "0 6px 16px -6px var(--coral)" }}><I d="spark" /></div>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: "var(--coral)" }}>아리의 건강 코칭</div>
<p style={{ fontSize: 13.5, color: "var(--ink-2)", lineHeight: 1.6, margin: "6px 0 0" }} dangerouslySetInnerHTML={{ __html: h.coach }} />
</div>
</div>
</div>
<div className="lgrid">
<div className="col-7">
<div className="card">
<div className="card-head">
<div className="c-ic bg-coral"><I d="flame" /></div>
<div className="c-h"><b>오늘의 활동</b><span> </span></div>
</div>
<div className="rings3">{h.rings.map((r) => <Ring key={r.label} r={r} />)}</div>
</div>
</div>
<div className="col-5">
<div className="card">
<div className="card-head">
<div className="c-ic bg-blue"><I d="stretch" /></div>
<div className="c-h"><b>바이탈</b><span> </span></div>
</div>
<div className="vitals">
{h.vitals.map((v) => (
<div className="vital-row" key={v.label}>
<div className={"v-dot bg-" + v.tone}><I d={v.trend === "up" ? "trending" : v.trend === "down" ? "heart" : "clock"} /></div>
<div className="v-main"><b>{v.label}</b><span>{v.note}</span></div>
<div className="v-val">{v.val}{v.unit && <small>{v.unit}</small>}</div>
</div>
))}
</div>
</div>
</div>
<div className="col-7">
<div className="card">
<div className="card-head">
<div className="c-ic bg-violet"><I d="moon" /></div>
<div className="c-h"><b>수면 분석</b><span> · 7 </span></div>
</div>
<div className="sleep-top">
<div className="sleep-score" style={{ "--p": h.sleep.score }}>
<b>{h.sleep.score}</b><span></span>
</div>
<div className="sleep-meta">
<div className="sm-total">{h.sleep.total}</div>
<div className="sm-label">권장보다 18 부족</div>
</div>
</div>
<div className="sleep-bar">
{h.sleep.stages.map((s) => (
<span key={s.label} style={{ width: s.pct + "%", background: s.tone === "muted" ? "var(--card-3)" : `var(--${s.tone})` }} />
))}
</div>
<div className="sleep-legend">
{h.sleep.stages.map((s) => (
<span className="lg" key={s.label}><i style={{ background: s.tone === "muted" ? "var(--faint)" : `var(--${s.tone})` }} />{s.label} {s.pct}%</span>
))}
</div>
<div className="sleep-week">
{h.sleep.week.map((v, i) => (
<div key={i} className={"sw" + (v >= 86 ? " peak" : "")} style={{ height: v + "%" }} title={v + "점"} />
))}
</div>
<div className="sleep-week-labels">
{["월", "화", "수", "목", "금", "토", "일"].map((d) => <span key={d}>{d}</span>)}
</div>
<div className="sleep-note">{h.sleep.note}</div>
</div>
</div>
<div className="col-5">
<div className="card">
<div className="card-head">
<div className="c-ic bg-green"><I d="check" /></div>
<div className="c-h"><b>건강한 습관</b><span> </span></div>
<span className="c-act"><I d="plus" />추가</span>
</div>
<div className="habits">
{habits.map((hb) => {
const done = hb.done >= hb.total;
return (
<div className={"habit" + (done ? " done" : "")} key={hb.id}>
<div className="habit-top">
<div className={"h-ic bg-" + hb.tone}><I d={hb.icon} /></div>
<div className="h-tx"><b>{hb.title}</b><span>{hb.goal}</span></div>
<span className="habit-streak"><I d="flame" />{hb.streak}</span>
</div>
<div className="habit-prog">
<div className="hp-bar"><i style={{ width: Math.min(100, (hb.done / hb.total) * 100) + "%", background: done ? "var(--green)" : `var(--${hb.tone})` }} /></div>
<span className="hp-n">{hb.done}/{hb.total}</span>
</div>
<div className="habit-foot">
<span className="habit-at"><I d="bell" />{hb.at}</span>
<button className={"habit-btn" + (done ? " done" : "")} onClick={() => onHabit(hb.id)}>
<I d={done ? "tick" : "plus"} w={done ? 3 : 1.9} />{done ? "완료" : "기록"}
</button>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
</>
);
}
/* ===================== 금융 모듈 ===================== */
function FinanceModule({ toast, paused, onPause }) {
const f = L.finance;
const b = f.budget;
return (
<>
<div className="mod-head">
<div className="mh-ic bg-blue"><I d="wallet" /></div>
<div className="mh-txt">
<h2>금융 & 소비</h2>
<div className="mh-sub">우리카드 · 토스 · 카카오뱅크 연동 · 6 예산</div>
</div>
<div className="mh-act"><span className="c-act" onClick={() => toast("이번 달 가계부를 정리하고 있어요", "blue")}><I d="layers" />월간 정산</span></div>
</div>
<div className="card glassy" style={{ borderColor: "color-mix(in oklab, var(--blue) 22%, var(--line-2))" }}>
<div style={{ display: "flex", gap: 13, alignItems: "flex-start" }}>
<div style={{ width: 38, height: 38, borderRadius: 11, background: "var(--blue)", color: "#fff", display: "grid", placeItems: "center", flexShrink: 0, boxShadow: "0 6px 16px -6px var(--blue)" }}><I d="spark" /></div>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 12, fontWeight: 700, color: "var(--blue)" }}>아리의 소비 코칭</div>
<p style={{ fontSize: 13.5, color: "var(--ink-2)", lineHeight: 1.6, margin: "6px 0 0" }} dangerouslySetInnerHTML={{ __html: f.coach }} />
</div>
</div>
</div>
<div className="lgrid">
<div className="col-5">
<div className="card">
<div className="card-head">
<div className="c-ic bg-blue"><I d="wallet" /></div>
<div className="c-h"><b>이달 예산</b><span>{b.daysLeft} </span></div>
</div>
<div className="budget-top">
<span className="bt-spent">{won(b.spent)}</span>
<span className="bt-of">/ {won(b.limit)}</span>
</div>
<span className="budget-delta t-green"><I d="trending" />지난달 대비 {Math.abs(b.deltaPct)}% 적게 썼어요</span>
<div className="budget-gauge"><i style={{ width: b.pct + "%" }} /></div>
<div className="budget-foot"><span>{b.pct}% 사용</span><span> {won(b.limit - b.spent)}</span></div>
<div style={{ marginTop: 16, paddingTop: 14, borderTop: "1px solid var(--line)" }}>
<div className="insights">
{f.insights.map((ins, i) => (
<div className={"insight " + ins.kind} key={i}>
<div className="in-ic"><I d={ins.icon} /></div>
<div className="in-main"><b>{ins.title}</b><p>{ins.body}</p></div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="col-7">
<div className="card">
<div className="card-head">
<div className="c-ic bg-violet"><I d="layers" /></div>
<div className="c-h"><b>자동 분류된 지출</b><span> </span></div>
</div>
<div className="cats">
{f.cats.map((c) => (
<div className="cat-row" key={c.name}>
<div className={"ca-ic bg-" + c.tone}><I d={c.icon} /></div>
<div className="ca-main">
<div className="ca-name">{c.name}{c.over && <span className="over-tag">예산 빠름</span>}</div>
<div className="ca-bar"><i style={{ width: c.pct * 2.4 + "%", background: c.over ? "var(--coral)" : `var(--${c.tone})` }} /></div>
</div>
<div className="ca-amt">{won(c.amt)}<small>{c.pct}%</small></div>
</div>
))}
</div>
</div>
<div className="card" style={{ marginTop: 16 }}>
<div className="card-head">
<div className="c-ic bg-green"><I d="repeat" /></div>
<div className="c-h"><b>구독 결제 예정</b><span> </span></div>
<span className="c-act"><I d="bell" />알림 켜짐</span>
</div>
<div className="subs">
{f.subs.map((s) => {
const isPaused = paused[s.name];
return (
<div className={"sub-row" + (s.inDays <= 3 && !isPaused ? " soon" : "")} key={s.name} style={isPaused ? { opacity: 0.55 } : null}>
<div className={"su-ic bg-" + s.tone}><I d={s.icon} /></div>
<div className="su-main"><b>{s.name}</b><span>{s.note}{isPaused ? " · " : ""}</span></div>
<div className="su-when">
<div className="sw-date">{s.date}</div>
<div className={"sw-in" + (s.inDays > 3 ? " far" : "")}>{isPaused ? "정지" : s.inDays + "일 뒤"}</div>
</div>
<div className="su-amt">{won(s.amt)}</div>
<button className="c-act" style={{ flexShrink: 0 }} onClick={() => onPause(s.name)} aria-label="일시정지">
<I d={isPaused ? "refresh" : "more"} />
</button>
</div>
);
})}
</div>
</div>
</div>
</div>
</>
);
}
/* ===================== 지식 모듈 (제2의 뇌) ===================== */
function KnowledgeModule({ toast }) {
const k = L.knowledge;
const [q, setQ] = useState("");
const [answer, setAnswer] = useState(null);
const ask = (text) => {
if (!text.trim()) return;
setQ(text);
setAnswer({
q: text,
body: "저장하신 자료를 종합하면 — <b>검증 가능한 작은 단계로 분해</b>하고, 계획·실행·반성을 분리할 때 에이전트 신뢰성이 가장 크게 올라갑니다. ‘분기 회고’ 메모의 리텐션 +6%p 결과와도 연결돼요. 온보딩 자동화에 <b>오케스트레이터-워커 패턴</b>을 먼저 적용해보길 추천드려요.",
sources: ["AI 에이전트 설계 패턴", "분기 회고 메모", "재무 자동화 시스템"],
});
};
const filtered = useMemo(() => {
const s = q.trim().toLowerCase();
if (!s || answer) return k.items;
return k.items.filter((it) => (it.title + it.excerpt + it.tags.join(" ")).toLowerCase().includes(s));
}, [q, answer]);
return (
<>
<div className="mod-head">
<div className="mh-ic bg-violet"><I d="brain" /></div>
<div className="mh-txt">
<h2>지식 & 학습 · 제2의 </h2>
<div className="mh-sub">{k.stats.items.toLocaleString()} 저장 · 이번 +{k.stats.thisWeek} · {k.stats.collections} 컬렉션</div>
</div>
<div className="mh-act"><span className="c-act" onClick={() => toast("새 항목을 스크랩할 준비가 됐어요", "violet")}><I d="plus" />스크랩</span></div>
</div>
<div>
<div className="kn-search">
<I d="spark" />
<input
value={q}
onChange={(e) => { setQ(e.target.value); if (answer) setAnswer(null); }}
onKeyDown={(e) => e.key === "Enter" && ask(q)}
placeholder="저장한 모든 것에 자연어로 물어보세요…"
/>
<button className="ks-go" onClick={() => ask(q)} aria-label="검색"><I d="arrow" /></button>
</div>
<div className="kn-suggest">
{k.suggested.map((s, i) => (
<button className="kn-chip" key={i} onClick={() => ask(s)}><I d="spark" />{s}</button>
))}
</div>
</div>
{answer && (
<div className="kn-answer">
<div className="ka-ava"><I d="spark" /></div>
<div className="ka-main">
<div className="ka-q">"{answer.q}"</div>
<div className="ka-body" dangerouslySetInnerHTML={{ __html: answer.body }} />
<div className="ka-src">
{answer.sources.map((s, i) => <span className="src-chip" key={i}><I d="article" />{s}</span>)}
</div>
</div>
</div>
)}
<div className="lgrid">
<div className="col-8">
<div className="mod-head" style={{ marginBottom: 13 }}>
<div className="mh-txt"><h2 style={{ fontSize: 16 }}>{answer ? "관련 자료" : q.trim() ? "검색 결과" : "최근 스크랩"}</h2></div>
<div className="mh-act"><span className="c-act"><I d="refresh" />자동 태깅됨</span></div>
</div>
<div className="scraps">
{filtered.map((it) => (
<div className="scrap" key={it.id}>
<div className="scrap-top">
<div className={"sc-ic bg-" + it.tone}><I d={it.icon} /></div>
<div className="sc-h">
<b>{it.title}</b>
<div className="sc-meta">{it.src}<span className="dotsep" />{it.time}</div>
</div>
<span className={"scrap-type bg-" + it.tone}>{({ article: "아티클", note: "메모", idea: "아이디어", highlight: "하이라이트" })[it.type]}</span>
</div>
<p className="scrap-excerpt">{it.excerpt}</p>
<div className="scrap-ai">
<div className="sa-ic"><I d="spark" /></div>
<p dangerouslySetInnerHTML={{ __html: "<b>아리 — </b>" + it.ai }} />
</div>
<div className="scrap-foot">
{it.tags.map((t, i) => <span className={"tag" + (i === 0 ? " auto" : "")} key={i}>{t}</span>)}
<span className="sf-spacer" />
<button className="sf-act" onClick={() => toast("요약을 정리했어요", "violet")}><I d="layers" />요약</button>
<button className="sf-act" onClick={() => toast("열어보는 중…", "violet")}><I d="link" />열기</button>
</div>
</div>
))}
{filtered.length === 0 && <div className="kn-empty">{q} 해당하는 자료가 없어요. 다르게 물어보거나 새로 스크랩해 보세요.</div>}
</div>
</div>
<div className="col-4">
<div className="card">
<div className="card-head">
<div className="c-ic bg-violet"><I d="folder" /></div>
<div className="c-h"><b>컬렉션</b><span> </span></div>
</div>
<div className="collections">
{k.collections.map((c) => (
<button className="coll" key={c.name} onClick={() => toast(`'${c.name}' 컬렉션을 여는 중…`, "violet")}>
<div className={"co-ic bg-" + c.tone}><I d="layers" /></div>
<div className="co-tx"><b>{c.name}</b><span>{c.count}</span></div>
</button>
))}
</div>
</div>
</div>
</div>
</>
);
}
/* ===================== 오늘 (통합) ===================== */
function TodayModule({ go, habits, toast }) {
const h = L.health, f = L.finance, k = L.knowledge;
const nextSub = f.subs[0];
const pendingHabits = habits.filter((x) => x.done < x.total);
return (
<>
<div className="coach-hero">
<div className="ch-ava"><I d="spark" /></div>
<div className="ch-main">
<div className="ch-eyebrow">아리 라이프 코치 · 오늘 아침</div>
<h1>오늘의 컨디션은 좋아요. 활동만 조금 챙기면 완벽해요 </h1>
<p>
어젯밤 <b>7시간 12</b> 86. <b> </b> .
이번 소비는 지난달보다 <b>8% 적고</b> , <b> Netflix·Spotify </b> .
저장하신 <b>AI 에이전트 설계 </b> .
</p>
</div>
</div>
<div className="lgrid">
{/* 건강 스냅샷 */}
<div className="col-4">
<div className="card" style={{ cursor: "pointer" }} onClick={() => go("health")}>
<div className="card-head">
<div className="c-ic bg-coral"><I d="heart" /></div>
<div className="c-h"><b>건강 & 웰니스</b><span>··</span></div>
<span className="c-act"><I d="arrow" /></span>
</div>
<div className="rings3">{h.rings.map((r) => <Ring key={r.label} r={r} />)}</div>
<div className="sleep-note" style={{ marginTop: 14 }}> 마시기 <b>5/8</b> · 3 .</div>
</div>
</div>
{/* 금융 스냅샷 */}
<div className="col-4">
<div className="card" style={{ cursor: "pointer" }} onClick={() => go("finance")}>
<div className="card-head">
<div className="c-ic bg-blue"><I d="wallet" /></div>
<div className="c-h"><b>금융 & 소비</b><span>6 </span></div>
<span className="c-act"><I d="arrow" /></span>
</div>
<div className="budget-top">
<span className="bt-spent" style={{ fontSize: 26 }}>{won(f.budget.spent)}</span>
<span className="bt-of">/ {won(f.budget.limit)}</span>
</div>
<span className="budget-delta t-green"><I d="trending" />지난달보다 8% 절약</span>
<div className="budget-gauge"><i style={{ width: f.budget.pct + "%" }} /></div>
<div className="sub-row soon" style={{ marginTop: 14 }}>
<div className={"su-ic bg-" + nextSub.tone}><I d={nextSub.icon} /></div>
<div className="su-main"><b>{nextSub.name}</b><span> </span></div>
<div className="su-when"><div className="sw-date">{nextSub.date}</div><div className="sw-in">{nextSub.inDays} </div></div>
<div className="su-amt">{won(nextSub.amt)}</div>
</div>
</div>
</div>
{/* 지식 스냅샷 */}
<div className="col-4">
<div className="card" style={{ cursor: "pointer" }} onClick={() => go("knowledge")}>
<div className="card-head">
<div className="c-ic bg-violet"><I d="brain" /></div>
<div className="c-h"><b>지식 & 학습</b><span>2 </span></div>
<span className="c-act"><I d="arrow" /></span>
</div>
<div className="kn-search" style={{ padding: "11px 14px", borderWidth: 1 }} onClick={(e) => { e.stopPropagation(); go("knowledge"); }}>
<I d="spark" />
<span style={{ flex: 1, fontSize: 13.5, color: "var(--faint)" }}>저장한 것에 물어보기</span>
</div>
<div className="scraps" style={{ marginTop: 12 }}>
{k.items.slice(0, 2).map((it) => (
<div key={it.id} style={{ display: "flex", gap: 10, alignItems: "center", padding: "9px 0", borderTop: "1px solid var(--line)" }}>
<div className={"sc-ic bg-" + it.tone} style={{ width: 30, height: 30, borderRadius: 9, display: "grid", placeItems: "center", flexShrink: 0 }}><I d={it.icon} /></div>
<div style={{ minWidth: 0 }}>
<b style={{ fontSize: 12.5, fontWeight: 600, display: "block", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{it.title}</b>
<span style={{ fontSize: 10.5, color: "var(--muted)" }}>{it.src} · {it.time}</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* 오늘의 리마인더 */}
<div className="col-12">
<div className="card">
<div className="card-head">
<div className="c-ic bg-amber"><I d="bell" /></div>
<div className="c-h"><b>오늘의 리마인더</b><span> </span></div>
</div>
<div className="lgrid" style={{ gap: 11 }}>
{pendingHabits.map((hb) => (
<div className="col-4" key={hb.id}>
<div className="insight">
<div className={"in-ic bg-" + hb.tone}><I d={hb.icon} /></div>
<div className="in-main"><b>{hb.title}</b><p>{hb.at} · {hb.done}/{hb.total} 완료</p></div>
</div>
</div>
))}
<div className="col-4">
<div className="insight alert">
<div className="in-ic"><I d="repeat" /></div>
<div className="in-main"><b>구독 결제 임박</b><p>{nextSub.date} {nextSub.name} {won(nextSub.amt)} </p></div>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
}
/* ===================== 앱 ===================== */
function App() {
const [theme, setTheme] = useState(() => load("theme", "light"));
const [mod, setMod] = useState(() => load("mod", "today"));
const [habits, setHabits] = useState(() => load("habits", L.health.habits));
const [paused, setPaused] = useState(() => load("paused", {}));
const [toasts, setToasts] = useState([]);
const shellRef = useRef(null);
const tid = useRef(0);
useEffect(() => { document.documentElement.setAttribute("data-theme", theme); save("theme", theme); }, [theme]);
useEffect(() => save("mod", mod), [mod]);
useEffect(() => save("habits", habits), [habits]);
useEffect(() => save("paused", paused), [paused]);
useEffect(() => { const el = shellRef.current; const id = requestAnimationFrame(() => el && el.classList.add("entered")); return () => cancelAnimationFrame(id); }, []);
const toast = (msg, tone) => {
const id = ++tid.current;
setToasts((t) => [...t, { id, msg, tone }]);
setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 2500);
};
const onHabit = (id) => {
setHabits((hs) => hs.map((h) => h.id === id && h.done < h.total ? { ...h, done: h.done + 1 } : h));
toast("습관을 기록했어요", "green");
};
const onPause = (name) => {
setPaused((p) => ({ ...p, [name]: !p[name] }));
toast(paused[name] ? "구독을 다시 활성화했어요" : "구독을 일시정지했어요", paused[name] ? "green" : "coral");
};
return (
<div className="mpage" 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 === "life" ? "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="lwork">
{/* 좌측: 모듈 + 연결 소스 */}
<aside className="subnav">
{MODULES.map((m) => (
<button key={m.id} className={"mod-row t-" + m.tone + (mod === m.id ? " on" : "")} onClick={() => setMod(m.id)}>
<div className="m-ic"><I d={m.icon} /></div>
<div className="m-txt"><b>{m.label}</b><span>{m.sub}</span></div>
</button>
))}
<div className="sn-label">연결된 데이터</div>
<div className="src-list">
{[...L.sources.health, ...L.sources.finance.slice(0, 2), ...L.sources.knowledge.slice(0, 2)].map((s) => (
<button className="src-row" key={s.id} onClick={() => toast(s.on ? `${s.name} 동기화 중…` : `${s.name} 연결 화면을 여는 중…`, s.tone)}>
<span className={"src-dot" + (s.on ? "" : " off")} style={{ background: `var(--${s.tone})` }} />
<div className="src-info"><b>{s.name}</b><span>{s.kind}</span></div>
<span className={"src-stat" + (s.on ? "" : " off")}>{s.on ? "연결됨" : "연결"}</span>
</button>
))}
<button className="src-add" onClick={() => toast("데이터 소스 연결 화면을 여는 중…", "blue")}><I d="plus" />소스 연결하기</button>
</div>
<div className="sn-foot">
<div className="av-lg"></div>
<div className="txt"><b>지우님</b><span>Pro · 9 </span></div>
</div>
</aside>
{/* 본문 */}
<main className="lmain">
{mod === "today" && <TodayModule go={setMod} habits={habits} toast={toast} />}
{mod === "health" && <HealthModule habits={habits} onHabit={onHabit} toast={toast} />}
{mod === "finance" && <FinanceModule toast={toast} paused={paused} onPause={onPause} />}
{mod === "knowledge" && <KnowledgeModule toast={toast} />}
</main>
</div>
<div className="toast-wrap">
{toasts.map((t) => (
<div className="toast" key={t.id}>
<span className="t-dot" style={{ background: `var(--${t.tone || "blue"})` }} />
{t.msg}
</div>
))}
</div>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<App />);