// === Logen courier shipping label / invoice === function ShippingScreen({ orders, setOrders, ordersSource, reloadOrders }) { const { PRODUCTS } = window.MOCK; const isLive = ordersSource === "live"; const paidOrders = orders.filter(o => o.status === "paid"); const [selected, setSelected] = useState(new Set(paidOrders.map(o => o.id))); const [previewMode, setPreviewMode] = useState("table"); // table / labels // 라이브에서 페이지 진입 시 paid가 0건이면 안내 — 자동 로드는 OrdersScreen에서 처리됨 const orderTotal = (o) => { if (o.totalPaymentAmount) return o.totalPaymentAmount; return o.items.reduce((s, it) => { if (it.price) return s + it.price * it.qty; const p = PRODUCTS.find(x => x.id === it.pid); return s + (p ? p.price * it.qty : 0); }, 0); }; const itemsLabel = (o) => o.items.map(it => { if (it.name) return `${it.name} ${it.qty}개`; const p = PRODUCTS.find(x => x.id === it.pid); return `${p?.name || it.pid} ${it.qty}개`; }).join(" + "); const toggleOne = (id) => { const s = new Set(selected); if (s.has(id)) s.delete(id); else s.add(id); setSelected(s); }; const toggleAll = () => { if (selected.size === paidOrders.length) setSelected(new Set()); else setSelected(new Set(paidOrders.map(o => o.id))); }; const selectedOrders = paidOrders.filter(o => selected.has(o.id)); // 발송 처리 모달 state const [dispatchOpen, setDispatchOpen] = useState(false); const [trackingMap, setTrackingMap] = useState({}); // pid -> trackingNumber const [bulkPaste, setBulkPaste] = useState(""); const [deliveryCompanyCode, setDeliveryCompanyCode] = useState("KGB"); const [dispatchSaving, setDispatchSaving] = useState(false); const [dispatchResult, setDispatchResult] = useState(null); // 로젠 API 발급 state const [logenOpen, setLogenOpen] = useState(false); const [logenStep, setLogenStep] = useState(null); // null | "registering" | "querying" | "done" | "error" const [logenResult, setLogenResult] = useState(null); const logenReady = window.API.logenConfigured && window.API.logenConfigured(); const setTracking = (pid, value) => setTrackingMap(prev => ({ ...prev, [pid]: value })); const applyBulkPaste = () => { // 한 줄에 하나씩 송장번호. 선택된 주문 순서대로 매핑. const lines = bulkPaste.split(/\r?\n/).map(s => s.trim()).filter(Boolean); const mapping = {}; selectedOrders.forEach((o, i) => { if (lines[i]) mapping[o.id] = lines[i]; }); setTrackingMap(prev => ({ ...prev, ...mapping })); setBulkPaste(""); toast(`${Object.keys(mapping).length}개 송장번호 일괄 입력`); }; const submitDispatch = async () => { const list = selectedOrders.map(o => ({ productOrderId: o.productOrderId || o.id, trackingNumber: (trackingMap[o.id] || "").trim(), deliveryCompanyCode, })).filter(d => d.trackingNumber); if (list.length === 0) { toast("입력된 송장번호가 없습니다"); return; } if (list.length < selectedOrders.length) { if (!confirm(`송장번호가 ${selectedOrders.length - list.length}건 비어있습니다. 입력된 ${list.length}건만 처리할까요?`)) return; } setDispatchSaving(true); setDispatchResult(null); try { const result = await window.API.naverDispatchOrders(list); setDispatchResult({ ok: true, list, response: result }); // 성공 시 로컬에서 해당 주문 status를 shipped로 (라이브는 사용자가 새로고침해서 다시 가져와도 됨) setOrders(prev => prev.map(o => { const matched = list.find(d => d.productOrderId === (o.productOrderId || o.id)); if (!matched) return o; return { ...o, status: "shipped", trackingNumber: matched.trackingNumber }; })); toast(`${list.length}건 발송 처리 완료`); } catch (err) { console.error("[dispatch]", err); setDispatchResult({ ok: false, error: err.message }); toast("발송 처리 실패: " + err.message); } finally { setDispatchSaving(false); } }; // 보내는분 정보 — 라이브 모드면 첫 주문의 takingAddress, 없으면 기본값 const senderInfo = useMemo(() => { if (isLive) { const first = orders.find(o => o.sender); if (first?.sender) return first.sender; } return { name: "스마트팜", tel: "02-1234-5678", address: "서울시 송파구 위례성대로 1", zipcode: "", }; }, [orders, isLive]); const printNow = () => { window.print(); }; const downloadXlsx = () => { // 로젠택배 일반 양식 (실제 양식 받아오면 컬럼 조정 가능) const headers = ["수령인명", "수령인전화번호", "수령인우편번호", "수령인주소", "품목명", "수량", "박스수량", "운임구분", "배송메시지", "주문번호"]; const rows = selectedOrders.map(o => [ o.buyer, o.phone, o.zipcode, o.address, itemsLabel(o), o.items.reduce((s, it) => s + it.qty, 0), 1, "선불", o.memo || "", o.productOrderId || o.id ]); const csv = "" + [headers, ...rows].map(r => r.map(x => `"${String(x).replace(/"/g, '""')}"`).join(",")).join("\r\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `LOGEN_송장_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); toast(`로젠 송장 양식 ${selectedOrders.length}건 다운로드 완료`); }; // 로젠 API 송장 발급 워크플로우: 등록 → 송장번호 조회 → 출력 팝업 const runLogenWorkflow = async () => { if (selectedOrders.length === 0) { toast("선택된 주문이 없습니다"); return; } setLogenOpen(true); setLogenResult(null); setLogenStep("registering"); try { // STEP 1: 주문 등록 (복수건) const reg = await window.API.logenRegisterOrders(selectedOrders); const takeDt = reg.takeDt; const perOrder = reg.perOrder || []; const fixTakeNos = perOrder.filter(r => r.fixTakeNo).map(r => ({ fixTakeNo: r.fixTakeNo })); const failedReg = perOrder.filter(r => r.resultCd !== "TRUE"); // STEP 2: 송장번호 조회 (등록된 fixTakeNo 기준) setLogenStep("querying"); let slipMap = {}; if (fixTakeNos.length > 0) { try { const inq = await window.API.logenInquirySlipNo(fixTakeNos); // 응답을 fixTakeNo → slipNo로 매핑 const inqData = inq.data || inq.results || []; inqData.forEach(it => { const slipNo = (it.data1 || it.slips || [])[0]?.slipNo || it.slipNo; if (it.fixTakeNo && slipNo) slipMap[it.fixTakeNo] = slipNo; }); } catch (err) { console.warn("[logen inquiry skip]", err.message); } } // STEP 3: 출력 팝업 URL (새 탭으로 열기는 사용자 액션 후 실행) let popupUrl = null; try { const popup = await window.API.logenGetPrintPopupUrl(takeDt); popupUrl = popup.url || popup.popupUrl || popup.data?.url || null; } catch (err) { console.warn("[logen popup URL skip]", err.message); } setLogenStep("done"); setLogenResult({ ok: true, takeDt, registered: perOrder, failedReg, slipMap, popupUrl, ordersById: Object.fromEntries(selectedOrders.map((o, i) => [o.id, perOrder[i]])), }); } catch (err) { console.error("[logen workflow]", err); setLogenStep("error"); setLogenResult({ ok: false, error: err.message }); } }; // 로젠에서 받은 송장번호를 발송 처리 모달의 trackingMap에 자동 매핑 const applyLogenSlipNos = () => { if (!logenResult?.slipMap) return; const mapping = {}; selectedOrders.forEach((o, i) => { const reg = logenResult.registered[i]; const fixTakeNo = reg?.fixTakeNo; const slip = fixTakeNo && logenResult.slipMap[fixTakeNo]; if (slip) mapping[o.id] = slip; }); const cnt = Object.keys(mapping).length; if (cnt === 0) { toast("매핑할 송장번호가 없습니다"); return; } setTrackingMap(prev => ({ ...prev, ...mapping })); setLogenOpen(false); setDispatchOpen(true); setBulkPaste(""); setDispatchResult(null); toast(`${cnt}건 송장번호 자동 입력 — 네이버 발송 처리 모달로 이동`); }; const markShipped = () => { if (isLive) { // 라이브 모드: 송장번호 입력 모달 띄움 setDispatchOpen(true); setTrackingMap({}); setBulkPaste(""); setDispatchResult(null); return; } setOrders(prev => prev.map(o => selected.has(o.id) ? { ...o, status: "shipped" } : o)); setSelected(new Set()); toast(`${selectedOrders.length}건 발송 처리되었습니다`); }; return ( <>

로젠택배 송장 출력

{isLive ? "네이버 라이브 · " : ""}발송 대기 {paidOrders.length}건 · 로젠택배 표준 양식 (수령인/주소/연락처/품목/수량)

{isLive && senderInfo.tel !== "02-1234-5678" && (

보내는분: {senderInfo.name} ({senderInfo.tel}) · {senderInfo.address}

)}
{isLive && reloadOrders && ( )} {logenReady && ( )}
0 && selected.size === paidOrders.length} onChange={toggleAll} /> 전체 선택 ({selected.size}/{paidOrders.length})
로젠택배 운임구분: 선불 · 박스 1박스 기본
{paidOrders.length === 0 ? (
발송 대기 중인 주문이 없습니다
{isLive ? "주문/판매 내역에서 기간을 늘려서 다시 조회해보세요 (PAYED 상태 주문만 표시)" : "결제완료 상태의 주문이 여기에 표시됩니다"}
) : previewMode === "table" ? (
{paidOrders.map(o => ( ))}
수령인명 전화번호 우편번호 주소 품목명 수량 운임 배송메시지 주문번호
toggleOne(o.id)} /> {o.buyer} {o.phone} {o.zipcode} {o.address} {itemsLabel(o)} {o.items.reduce((s, it) => s + it.qty, 0)} 선불 {o.memo || "-"} {o.productOrderId || o.id}
) : (
{selectedOrders.map(o => )}
)}
{/* 발송 처리 모달 (라이브) */} !dispatchSaving && setDispatchOpen(false)} size="lg" title="네이버 발송 처리 (송장번호 등록)" footer={ dispatchResult?.ok ? ( ) : ( <> ) }> {dispatchResult ? (
{dispatchResult.ok ? ( <>
✓ {dispatchResult.list.length}건 발송 처리 완료
네이버 응답:
                  {JSON.stringify(dispatchResult.response, null, 2)}
                
) : (
실패: {dispatchResult.error}
네이버 dispatch endpoint/payload 추측이 틀렸을 수 있습니다. 콘솔에서 _raw_post로 다양한 path 시도해서 정확한 spec 찾기:
{`fetch(c.proxy_url+'/naver/_raw_post', {
  method:'POST', headers:{...,'Content-Type':'application/json'},
  body:JSON.stringify({_path:'/external/...', payload:{...}})
})`}
                  
)}
) : ( <>