// === Main app shell === const { useState: useStateApp, useEffect: useEffectApp } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "light" }/*EDITMODE-END*/; function App() { const [route, setRoute] = useStateApp("dashboard"); const [products, setProducts] = useStateApp(window.MOCK.PRODUCTS); const [orders, setOrders] = useStateApp(window.MOCK.ORDERS); const [detailId, setDetailId] = useStateApp(null); const [sidebarOpen, setSidebarOpen] = useStateApp(false); const [tweaksOpen, setTweaksOpen] = useStateApp(false); // 데이터 소스: mock(샘플) vs live(네이버 실제). loading/error는 별도. const [productsSource, setProductsSource] = useStateApp("mock"); const [productsLoading, setProductsLoading] = useStateApp(false); const [productsError, setProductsError] = useStateApp(null); const [productsMeta, setProductsMeta] = useStateApp(null); const [productsPage, setProductsPage] = useStateApp(1); const [productsSearch, setProductsSearch] = useStateApp(""); const PAGE_SIZE = 100; // 일괄 작업용: 전체 상품을 페이지네이션 돌면서 모두 가져옴. 메인 products state는 건드리지 않음. // 일괄 가격 수정 화면이 사용. progress는 { done, total } 콜백. const [allProducts, setAllProducts] = useStateApp(null); const [allProductsLoading, setAllProductsLoading] = useStateApp(false); const [allProductsProgress, setAllProductsProgress] = useStateApp(null); const loadAllNaverProducts = async ({ onProgress } = {}) => { if (!window.API.naverConfigured()) { throw new Error("API 자격증명이 설정되지 않았습니다"); } setAllProductsLoading(true); setAllProductsProgress({ done: 0, total: 0 }); try { const ALL_PAGE_SIZE = 500; // 네이버 max const accumulated = []; let page = 1; let totalPages = 1; let totalElements = 0; do { const result = await window.API.naverFetchProducts({ page, size: ALL_PAGE_SIZE }); accumulated.push(...result.products); totalPages = result.totalPages || 1; totalElements = result.totalElements || accumulated.length; const progress = { done: accumulated.length, total: totalElements, page, totalPages }; setAllProductsProgress(progress); onProgress && onProgress(progress); page += 1; if (page <= totalPages) await new Promise(r => setTimeout(r, 200)); // rate limit } while (page <= totalPages); setAllProducts(accumulated); return accumulated; } finally { setAllProductsLoading(false); } }; // opts: { page?, searchKeyword?: string | "" } — undefined면 현재 state 사용, 빈 문자열은 검색 해제 const loadNaverProducts = (opts) => { const nextPage = opts?.page !== undefined ? opts.page : productsPage; const nextKeyword = opts?.searchKeyword !== undefined ? opts.searchKeyword : productsSearch; setProductsLoading(true); setProductsError(null); const params = { page: nextPage, size: PAGE_SIZE }; if (nextKeyword) params.searchKeyword = nextKeyword; return window.API.naverFetchProducts(params) .then(({ products: live, ...meta }) => { setProducts(live); setProductsSource("live"); setProductsMeta(meta); if (opts?.page !== undefined) setProductsPage(opts.page); if (opts?.searchKeyword !== undefined) { setProductsSearch(opts.searchKeyword); // 검색 변경 시 페이지 리셋 if (opts.page === undefined) setProductsPage(1); } }) .catch(err => { console.error("[naverFetchProducts]", err); setProductsError(err.message); window.toast && window.toast("네이버 상품 조회 실패: " + err.message); }) .finally(() => setProductsLoading(false)); }; // 마운트 시 자격증명 있으면 자동 로드 (products + orders) useEffectApp(() => { if (window.API.naverConfigured()) { loadNaverProducts(); loadNaverOrders({ days: 7 }); } }, []); // 주문 라이브 로드 const [ordersSource, setOrdersSource] = useStateApp("mock"); const [ordersLoading, setOrdersLoading] = useStateApp(false); const [ordersError, setOrdersError] = useStateApp(null); const [ordersProgress, setOrdersProgress] = useStateApp(null); const [ordersDays, setOrdersDays] = useStateApp(7); const loadNaverOrders = (opts) => { const days = opts?.days || ordersDays; setOrdersLoading(true); setOrdersError(null); setOrdersProgress({ done: 0, total: days }); return window.API.naverFetchOrders({ days, onProgress: (p) => setOrdersProgress(p), }) .then(live => { setOrders(live); setOrdersSource("live"); if (opts?.days !== undefined) setOrdersDays(opts.days); window.toast && window.toast("최근 " + days + "일 주문 " + live.length + "건 불러옴"); }) .catch(err => { console.error("[naverFetchOrders]", err); setOrdersError(err.message); window.toast && window.toast("주문 조회 실패: " + err.message); }) .finally(() => { setOrdersLoading(false); setOrdersProgress(null); }); }; // Theme tweak const [tweaks, setTweak] = window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, () => {}]; useEffectApp(() => { document.documentElement.setAttribute("data-theme", tweaks.theme); }, [tweaks.theme]); // Tweaks panel host registration useEffectApp(() => { const handler = (e) => { if (!e.data || typeof e.data !== "object") return; if (e.data.type === "__activate_edit_mode") setTweaksOpen(true); if (e.data.type === "__deactivate_edit_mode") setTweaksOpen(false); }; window.addEventListener("message", handler); window.parent.postMessage({ type: "__edit_mode_available" }, "*"); return () => window.removeEventListener("message", handler); }, []); const navItems = [ { key: "dashboard", label: "대시보드", icon: window.I.Dashboard }, { key: "products", label: "상품 관리", icon: window.I.Box, badge: products.filter(p => p.status === "outofstock").length || null }, { key: "bulk", label: "일괄 가격 수정", icon: window.I.Tag }, { key: "orders", label: "주문/판매 내역", icon: window.I.ShoppingCart, badge: orders.filter(o => o.status === "paid").length || null }, { key: "shipping", label: "로젠 송장 출력", icon: window.I.Truck }, { key: "customer", label: "고객 응대", icon: window.I.MessageCircle }, { key: "manual", label: "수동 등록", icon: window.I.Plus }, { key: "isvm", label: "ISVM 상품 연동", icon: window.I.Plug }, { key: "settings", label: "API 설정", icon: window.I.Settings }, ]; const goto = (k) => { setRoute(k); setSidebarOpen(false); window.scrollTo(0, 0); }; const openDetail = (pid) => setDetailId(pid); const closeDetail = () => setDetailId(null); const titleMap = { dashboard: "대시보드", products: "상품 관리", bulk: "일괄 가격 수정", orders: "주문/판매 내역", shipping: "로젠 송장 출력", isvm: "ISVM 상품 연동", manual: "수동 등록", customer: "고객 응대", settings: "API 설정" }; return (