// Shared atoms used across all 5 variations. // Compact and self-contained — no external imports. const { useState, useMemo } = React; const fmtKRW = (n, short = false) => { if (n == null) return "—"; const abs = Math.abs(n); if (short) { if (abs >= 1e8) return `${(n / 1e8).toFixed(2)}억`; if (abs >= 1e4) { // One decimal under ~100만 so small figures (fees, deltas, harvest) don't // round to a misleading single-digit '만' (₩12,345 → '1.2만', not '1만'). const man = n / 1e4; return abs < 1e6 ? `${man.toFixed(1)}만` : `${Math.round(man).toLocaleString()}만`; } } return n.toLocaleString("ko-KR"); }; const fmtPct = (n, signed = true) => { if (n == null || !Number.isFinite(n)) return "—"; const s = signed && n >= 0 ? "+" : ""; return `${s}${n.toFixed(2)}%`; }; const fmtSigned = (n, short = false) => { if (n == null || !Number.isFinite(n)) return "—"; const s = n >= 0 ? "+" : "−"; return `${s}₩${fmtKRW(Math.abs(n), short)}`; }; const upDown = n => (Number.isFinite(n) ? (n >= 0 ? "up" : "down") : ""); // ─── Toss top app bar ─── function AppBar({ title, leading = "‹", trailing, onLeading }) { return (
{leading ? ( ) : null}
{title}
{trailing}
); } // ─── Bottom tab bar (5 tabs matching Streamlit nav) ─── function TabBar({ active = "home", onTab }) { const tabs = [ { id: "home", label: "홈", icon: "M3 11l9-8 9 8v10a1 1 0 01-1 1h-5v-7h-6v7H4a1 1 0 01-1-1V11z" }, { id: "accounts", label: "계좌", icon: "M3 7h18v12H3zM3 7l3-3h12l3 3" }, { id: "tax", label: "세금", icon: "M5 4h11l3 3v13H5z M9 9h6 M9 13h6 M9 17h4" }, { id: "flow", label: "자금", icon: "M4 12h12 M12 8l4 4-4 4 M20 6v12" }, { id: "history", label: "내역", icon: "M12 8v4l3 2 M3 12a9 9 0 109-9" }, ]; return (
{tabs.map(t => ( ))}
); } // ─── Account avatar (initial-style colored badge) ─── function AccountAvatar({ name, kind = "domestic" }) { const colorMap = { domestic: "linear-gradient(135deg,#3b82f6,#1e40af)", us: "linear-gradient(135deg,#ef4444,#991b1b)", isa: "linear-gradient(135deg,#8a4dab,#6f2b8f)", }; const ch = (name || "?").slice(0, 2); return (
{ch}
); } // ─── Flow row icon by category ─── const FLOW_META = { deposit: { cls: "dep", ico: "↓", label: "입금" }, withdrawal: { cls: "wd", ico: "↑", label: "출금" }, internal_transfer: { cls: "int", ico: "⇄", label: "계좌이체" }, internal_in: { cls: "int", ico: "⇄", label: "이체 입금" }, internal_out: { cls: "int", ico: "⇄", label: "이체 출금" }, fx_krw_to_usd: { cls: "fx", ico: "₩→$", label: "환전" }, fx_usd_to_krw: { cls: "fx", ico: "$→₩", label: "환전" }, krx_buy: { cls: "buy", ico: "B", label: "매수" }, krx_sell: { cls: "sell", ico: "S", label: "매도" }, us_buy: { cls: "buy", ico: "B", label: "매수" }, us_sell: { cls: "sell", ico: "S", label: "매도" }, dividend: { cls: "div", ico: "₩", label: "배당" }, interest: { cls: "int", ico: "%", label: "이자" }, }; function FlowIcon({ category }) { const m = FLOW_META[category] || FLOW_META.deposit; return
{m.ico}
; } Object.assign(window, { fmtKRW, fmtPct, fmtSigned, upDown, AppBar, TabBar, AccountAvatar, FlowIcon, FLOW_META, });