// === Dashboard screen === function Dashboard({ goto, products, orders, productsSource, ordersSource, ordersLoading, ordersDays, productsMeta }) { const { PRODUCTS, ORDERS, SALES_TREND } = window.MOCK; const isLive = productsSource === "live" || ordersSource === "live"; // 사용할 데이터 소스 const usedProducts = productsSource === "live" ? products : PRODUCTS; const usedOrders = ordersSource === "live" ? orders : ORDERS; // 라이브 통계 계산 (orders 기반) const liveStats = useMemo(() => { if (ordersSource !== "live") return null; // 매출 인정 상태: paid/shipped/delivered (취소/반품 제외) const validOrders = usedOrders.filter(o => ["paid", "shipped", "delivered"].includes(o.status)); // 일별 매출/주문수 const byDate = new Map(); validOrders.forEach(o => { const date = (o.date || "").slice(0, 10); if (!date) return; if (!byDate.has(date)) byDate.set(date, { revenue: 0, orders: 0 }); const e = byDate.get(date); e.revenue += o.totalPaymentAmount || 0; e.orders++; }); // 트렌드 라인 (ordersDays 기간만큼) const trendDays = ordersDays || 7; const trend = []; for (let i = trendDays - 1; i >= 0; i--) { const d = new Date(Date.now() - i * 24 * 3600 * 1000); const isoDate = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0"); const label = String(d.getMonth() + 1).padStart(2, "0") + "/" + String(d.getDate()).padStart(2, "0"); const entry = byDate.get(isoDate) || { revenue: 0, orders: 0 }; trend.push({ d: label, isToday: i === 0, revenue: entry.revenue, orders: entry.orders }); } // 베스트 상품 (orders item 합산) const productSales = new Map(); validOrders.forEach(o => { o.items.forEach(it => { const pid = it.pid; if (!pid) return; if (!productSales.has(pid)) productSales.set(pid, { pid, name: it.name || "", qty: 0, revenue: 0 }); const e = productSales.get(pid); e.qty += it.qty; e.revenue += (it.price || 0) * it.qty; }); }); const topProducts = Array.from(productSales.values()).sort((a, b) => b.qty - a.qty).slice(0, 5); return { trend, todayRevenue: trend[trend.length - 1]?.revenue || 0, yesterdayRevenue: trend[trend.length - 2]?.revenue || 0, totalRevenue: trend.reduce((s, e) => s + e.revenue, 0), totalOrders: trend.reduce((s, e) => s + e.orders, 0), newOrders: usedOrders.filter(o => o.status === "paid").length, canceledOrders: usedOrders.filter(o => o.status === "canceled").length, topProducts, recentOrders: usedOrders.slice(0, 5), validCount: validOrders.length, }; }, [ordersSource, usedOrders, ordersDays]); // 표시할 값 (라이브 vs mock) const todayRevenue = liveStats ? liveStats.todayRevenue : SALES_TREND[SALES_TREND.length - 1].revenue; const yesterdayRevenue = liveStats ? liveStats.yesterdayRevenue : SALES_TREND[SALES_TREND.length - 2].revenue; const revenueChange = yesterdayRevenue > 0 ? ((todayRevenue - yesterdayRevenue) / yesterdayRevenue) * 100 : 0; const totalRevenue = liveStats ? liveStats.totalRevenue : SALES_TREND.reduce((s, d) => s + d.revenue, 0); const totalOrders = liveStats ? liveStats.totalOrders : SALES_TREND.reduce((s, d) => s + d.orders, 0); const newOrders = liveStats ? liveStats.newOrders : usedOrders.filter(o => o.status === "paid").length; const outOfStock = usedProducts.filter(p => p.status === "outofstock").length; const activeProducts = usedProducts.filter(p => p.status === "active").length; const totalProducts = productsSource === "live" ? (productsMeta?.totalElements || usedProducts.length) : usedProducts.length; const topProducts = liveStats ? liveStats.topProducts : [...usedProducts].sort((a, b) => b.sales30d - a.sales30d).slice(0, 5); const recentOrders = liveStats ? liveStats.recentOrders : usedOrders.slice(0, 5); const trendData = liveStats ? liveStats.trend : SALES_TREND.map(d => ({ ...d, isToday: d.d === "05/10" })); const chartMax = Math.max(1, ...trendData.map(d => d.revenue)); const trendLabel = liveStats ? `최근 ${ordersDays || 7}일` : "최근 14일"; const today = new Date(); const todayLabel = `${today.getFullYear()}년 ${today.getMonth() + 1}월 ${today.getDate()}일`; return (

대시보드

{todayLabel} · {isLive ? "네이버 라이브" : "샘플 데이터"} {isLive && liveStats && ` · 실 매출 인정 주문 ${liveStats.validCount}건${liveStats.canceledOrders > 0 ? ` · 취소 ${liveStats.canceledOrders}` : ""}`}

{ordersLoading && (
네이버에서 주문 데이터 불러오는 중...
)}
} spark={trendData.map(d => d.revenue)} /> 0 ? `발송 대기 ${newOrders}건` : "대기 주문 없음"} deltaPositive={newOrders > 0} icon={} /> } /> } />
{/* Sales chart */}
매출 추이
{trendLabel}{isLive && " (paid/shipped/delivered 합산)"}
{isLive ? (
기간 변경은 주문/판매 내역에서
) : (
)}
{trendData.every(d => d.revenue === 0) ? (
{trendLabel} 동안 매출 데이터가 없습니다
) : (
{trendData.map((d, i) => { const h = (d.revenue / chartMax) * 178; return (
{d.isToday && d.revenue > 0 && (
{Math.round(d.revenue / 10000)}만
)}
{d.d}
); })}
)}
{/* Quick actions */}
빠른 작업
} label="상품 가격 수정" desc="개별 또는 일괄 수정" onClick={() => goto("products")} /> } label="일괄 가격 변경" desc="키워드/카테고리 필터로 일괄" onClick={() => goto("bulk")} accent /> } label="로젠 송장 출력" desc={`${newOrders}건 발송 대기`} onClick={() => goto("shipping")} /> } label="ISVM 상품 등록" desc="공급사 카탈로그 → 스토어 연동" onClick={() => goto("isvm")} />
{/* Top products */}
베스트 상품 {liveStats ? `(${trendLabel} 판매 기준)` : "TOP 5"}
{topProducts.length === 0 ? (
판매 데이터가 없습니다
) : topProducts.map((p, i) => (
{i + 1}
{!isLive && }
{p.name}
{liveStats ? `매출 ${formatKRW(p.revenue)}` : `${formatKRW(p.price)} · 재고 ${p.stock}`}
{liveStats ? `${p.qty}개` : `${p.sales30d}건`}
{trendLabel} 판매
))}
{/* Recent orders */}
최근 주문
{recentOrders.length === 0 ? (
{trendLabel} 동안 주문이 없습니다
) : recentOrders.map((o, i) => { const isLiveOrder = ordersSource === "live"; const firstItem = !isLiveOrder ? window.MOCK.PRODUCTS.find(x => x.id === o.items[0].pid) : null; const productName = isLiveOrder ? (o.items[0]?.name || "") : (firstItem?.name || ""); const totalPrice = o.totalPaymentAmount || (isLiveOrder ? (o.items[0]?.price || 0) * (o.items[0]?.qty || 0) : o.items.reduce((s, it) => { const p = window.MOCK.PRODUCTS.find(x => x.id === it.pid); return s + (p ? p.price * it.qty : 0); }, 0)); return (
{!isLiveOrder && }
{o.buyer} · {o.items.length === 1 ? productName : `${productName} 외 ${o.items.length - 1}건`}
{(o.date || "").split(" ")[1]} · {String(o.id || "").slice(-8)}
{formatKRW(totalPrice)}
); })}
); } function StatTile({ label, value, delta, deltaText, deltaPositive, icon, spark }) { const showDelta = delta !== undefined && delta !== 0 && !isNaN(delta); const isUp = showDelta ? delta >= 0 : deltaPositive; return (
{icon} {label}
{value}
{showDelta ? (
{isUp ? : } {Math.abs(delta).toFixed(1)}% (전일 대비)
) : (
{deltaText}
)} {spark &&
}
); } function QuickAction({ icon, label, desc, onClick, accent }) { return ( ); } window.Dashboard = Dashboard;