/* 아리 — 여행·출장 도우미 페이지 좌측 레일로 전환: 다가오는 출장 · 새 여행 계획(AI) · 찜·가격추적 */ const { useState, useEffect, useRef } = React; const I = window.AriShell.Icon; const Topbar = window.AriShell.Topbar; const SubRail = window.AriShell.SubRail; const T = window.AriTrip; /* 페이지 전용 아이콘 (각 JSX는 독립 스코프) */ const TP_P = { train: "M8 3h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V7a4 4 0 0 1 4-4z|M4 11h16|M9 15h.01|M15 15h.01|M8 19l-2 3|M16 19l2 3", bed: "M2 4v16|M2 8h18a2 2 0 0 1 2 2v10|M2 17h20|M6 8v9", food: "M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2|M7 2v20|M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3zm0 0v7", pin: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z|M12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6z", rain: "M16 13v6M8 13v6M12 15v6|M20 16.6A5 5 0 0 0 18 7h-1.3A8 8 0 1 0 4 14.9", plane: "M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5c-.2.5-.1 1 .3 1.3L9 12l-2 3H4l-1 1 3 2 2 3 1-1v-3l3-2 3.5 5.3c.3.4.8.5 1.3.3l.5-.2c.4-.3.6-.7.5-1.2z", star: "M12 2.5l2.9 5.9 6.5.9-4.7 4.6 1.1 6.5L12 18.8 6.2 21l1.1-6.5L2.6 9.8l6.5-.9z", down: "M12 5v14M19 12l-7 7-7-7", up2: "M12 19V5M5 12l7-7 7 7", 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", }; function TI({ d }) { return ( {TP_P[d].split("|").map((s, i) => )} ); } const TP_KIND = { move: { local: true, d: "train" }, meet: { local: false, d: "users" }, talk: { local: false, d: "mic" }, meal: { local: true, d: "food" }, }; function KindIcon({ kind }) { const k = TP_KIND[kind] || TP_KIND.meet; return {k.local ? : }; } function Leg({ leg }) { return (
{leg.mode}
{leg.from}{leg.ft}
{leg.to}{leg.tt}
{leg.seat}{leg.note}
); } function Spark({ vals, color }) { const w = 100, h = 48; const min = Math.min(...vals), max = Math.max(...vals); const span = max - min || 1; const pts = vals.map((v, i) => { const x = (i / (vals.length - 1)) * w; const y = h - 6 - ((v - min) / span) * (h - 12); return x.toFixed(1) + "," + y.toFixed(1); }).join(" "); const last = pts.split(" ").pop().split(","); return ( ); } /* ============ 다가오는 출장 ============ */ const TKEY = "ariTp.check"; function loadCheck() { try { return JSON.parse(localStorage.getItem(TKEY)) || {}; } catch (e) { return {}; } } function UpcomingView({ day, setDay, done, flip, doneN, allN }) { const curDay = T.days.find((d) => d.id === day); const pct = Math.round((T.expense.planned / T.expense.budget) * 100); return (
{/* 히어로 */}
{T.trip.dday} · 출장
{T.trip.title} {T.trip.dates}

{T.trip.brief}

{T.trip.purpose} {T.trip.weather.map((wx) => ( {wx.icon === "sun" ? : }{wx.d} {wx.t} ))}
{T.stay.name}
{T.stay.desc}
{T.stay.conf}
{/* 아리가 미리 해둔 일 */}

아리가 미리 해둔 일

{T.prep.map((p, i) => (
{p.text}
))}
예매·예약 같은 결제 처리는 모두 결재함에서 승인받았어요.
{/* 체크리스트 */}

체크리스트

아리가 만든 준비물
{doneN}/{allN}
{T.check.map((g) => (
{g.name}
{g.items.map((it) => (
flip(it.id)}>
{it.text}
{it.auto && {it.auto}}
))}
))}
{/* 일정표 */}

일정표

이동·회의·식사까지 한 줄로
{T.days.map((d) => ( ))}
{curDay.items.map((ev, i) => (
{ev.t}
{ev.title}
{ev.meta}
{ev.hot && 핵심}
))}
{/* 경비 */}

경비

법인카드 · 자동 정산
{(T.expense.planned / 10000).toFixed(1)}만/ 예산 {(T.expense.budget / 10000).toFixed(0)}만원
{pct}% 사용 예정{((T.expense.budget - T.expense.planned) / 10000).toFixed(1)}만 여유
{T.expense.rows.map((r) => (
{r.name} {r.amt} {r.state === "paid" ? "결제 완료" : r.state === "hold" ? "결제 예정" : "예상"}
))}
{T.expense.note}
); } /* ============ 새 여행 계획 (AI 플래너) ============ */ function PlanView({ onSave }) { const P = T.planner; const [text, setText] = useState(""); const [phase, setPhase] = useState("input"); // input | research | result const [step, setStep] = useState(0); const [result, setResult] = useState(null); const [day, setDay] = useState(0); const [saved, setSaved] = useState(false); const timers = useRef([]); const pickResult = (q) => { const idx = P.examples.findIndex((e) => e === q.trim()); const r = P.results[idx >= 0 ? idx : 0]; if (idx < 0 && q.trim()) return { ...r, title: q.trim().slice(0, 18), custom: true }; return r; }; const run = () => { if (!text.trim()) return; const r = pickResult(text); setResult(r); setDay(0); setSaved(false); setPhase("research"); setStep(0); timers.current.forEach(clearTimeout); timers.current = P.research.map((_, i) => setTimeout(() => setStep(i + 1), 650 * (i + 1)) ); timers.current.push(setTimeout(() => setPhase("result"), 650 * (P.research.length + 1))); }; useEffect(() => () => timers.current.forEach(clearTimeout), []); const reset = () => { setPhase("input"); setText(""); setResult(null); }; if (phase === "input") { return (
새 여행 계획

어디로, 언제, 누구와 가는지 한 문장이면 돼요. 항공·숙소·일정·예산은 아리가 다 조사해서 가져올게요.

setText(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") run(); }} placeholder="예: 다음 달 말 제주 가족여행 3박 4일, 예산 150만원" aria-label="여행 설명" autoFocus />
{P.examples.map((e) => ( ))}

아리가 조사하는 것들

말 한마디로 시작돼요
{P.research.map((r) => (
{r.label}{r.detail}
))}
); } if (phase === "research") { return (
조사 중…

‘{result && result.title}’ 계획을 짜고 있어요. 잠시만요.

{P.research.map((r, i) => { const st = i < step ? "done" : i === step ? "now" : "wait"; return (
{st === "done" ? : }
{r.label}{r.detail}
{st === "now" && } {st === "done" && 완료}
); })}
); } /* result */ const r = result; const cur = r.days[day]; const bpct = Math.round((parseInt(r.budget.total.replace(/,/g, "")) / parseInt(r.budget.cap.replace(/,/g, ""))) * 100); return (
아리의 추천 계획
{r.title} {r.meta}

{r.weather}
{r.sources.join(" · ")} 를 종합했어요
{/* 이동 + 숙소 */}

이동 · 숙소

아리가 고른 최적 옵션
이동
{r.transport.map((o) => (
{o.mode}
{o.name}{o.pick && 추천}
{o.desc}
{o.price}{o.note && {o.note}}
))}
숙소
{r.stay.map((o) => (
{o.name}{o.pick && 추천}
{o.desc}
{o.price}{o.unit}
))}
{/* 일정 초안 */}

일정 초안

동선·날씨까지 맞춘 추천
{r.days.map((d, i) => ( ))}
{cur.items.map((ev, i) => (
{ev.t}
{ev.title}
))}
{/* 예상 경비 + 준비물 */}

예상 경비

{(parseInt(r.budget.total.replace(/,/g, "")) / 10000).toFixed(0)}만/ 예산 {(parseInt(r.budget.cap.replace(/,/g, "")) / 10000).toFixed(0)}만원
{bpct}% · 예산 안{((parseInt(r.budget.cap.replace(/,/g, "")) - parseInt(r.budget.total.replace(/,/g, ""))) / 10000).toFixed(0)}만 여유
{r.budget.rows.map((b) => (
{b.name}{b.amt}
))}

준비물

아리가 미리 챙겨뒀어요
{r.checklist.map((c, i) => (
{c}
))}
); } /* ============ 찜 · 가격 추적 ============ */ function SavedView() { const [items, setItems] = useState(T.saved); const toggle = (id) => setItems(items.map((x) => (x.id === id ? { ...x, watch: !x.watch } : x))); return (
{items.map((s) => (

{s.title}

{s.tag}

{s.note}

{s.now} {s.delta}
{s.hint}
목표가에 닿으면 바로 알려드려요
))}
); } /* ============ 셸 ============ */ const TP_RAIL = [ { id: "upcoming", icon: "pin", label: "다가오는 출장" }, { id: "plan", icon: "plus", label: "새 여행 계획" }, { sep: true }, { id: "saved", icon: "heart", label: "찜 · 가격 추적" }, ]; const TP_HEAD = { upcoming: { title: "여행", em: "준비는 아리가 해둘게요" }, plan: { title: "새 여행 계획", em: "말하면 아리가 다 조사해요" }, saved: { title: "찜한 여행", em: "가격이 내리면 알려드려요" }, }; function App() { const [view, setView] = useState("upcoming"); const [day, setDay] = useState("d1"); const [done, setDone] = useState(loadCheck); const [toast, setToast] = useState(null); const flip = (id) => { const n = { ...done, [id]: !done[id] }; setDone(n); localStorage.setItem(TKEY, JSON.stringify(n)); }; const allItems = T.check.flatMap((g) => g.items); const doneN = allItems.filter((it) => done[it.id]).length; const onSave = (r) => { setToast("‘" + r.title + "’ 계획을 저장했어요 — 찜·가격 추적에서 확인하세요."); setTimeout(() => setToast(null), 3600); }; const h = TP_HEAD[view]; return (
다음 일정 {T.trip.dday} · {T.trip.title} 준비물 {doneN}/{allItems.length} 챙김

{h.title} {h.em}

{view === "upcoming" && } {view === "plan" && } {view === "saved" && }
{toast &&
{toast}
}
); } ReactDOM.createRoot(document.getElementById("root")).render();