diff --git a/src/app/(main)/m/admin/orders/page.tsx b/src/app/(main)/m/admin/orders/page.tsx index a905c70..42eb446 100644 --- a/src/app/(main)/m/admin/orders/page.tsx +++ b/src/app/(main)/m/admin/orders/page.tsx @@ -457,6 +457,7 @@ function StatementPreview({ filename: `${order.ORDER_NO}_거래명세표`, shareTitle: `${order.ORDER_NO} 거래명세표`, shareText: order.COMPANY_NAME, + forceWidth: 1100, // 엑셀 가로 출력처럼 펼쳐서 캡처 }); }; diff --git a/src/app/(main)/m/admin/procurements/page.tsx b/src/app/(main)/m/admin/procurements/page.tsx index 0398b98..2894e69 100644 --- a/src/app/(main)/m/admin/procurements/page.tsx +++ b/src/app/(main)/m/admin/procurements/page.tsx @@ -297,6 +297,7 @@ function ProcurementForm({ detail, vendors, onSetVendor, onSetMemo, onSetTerm, o filename: `${detail.proc.PROC_NO}_발주서`, shareTitle: `${detail.proc.PROC_NO} 발주서`, shareText: detail.proc.VENDOR_NAME ?? "", + forceWidth: 1100, // 엑셀 가로 출력처럼 펼쳐서 캡처 }); }; diff --git a/src/app/(main)/m/orders/page.tsx b/src/app/(main)/m/orders/page.tsx index 3f1c4c9..744099a 100644 --- a/src/app/(main)/m/orders/page.tsx +++ b/src/app/(main)/m/orders/page.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { Download, Image as ImageIcon, X, Eye, Trash2 } from "lucide-react"; import Swal from "sweetalert2"; import { downloadXlsx } from "@/lib/xlsx-export"; +import { captureAndShare as captureAndShareLib } from "@/lib/capture-share"; interface Order { OBJID: string; @@ -238,26 +239,14 @@ function DetailModal({ order, items, supplier, onClose, onCancel, onReload }: { }; const captureAndShare = async () => { - try { - const mod = await import("html-to-image"); - if (!ref.current) return; - const dataUrl = await mod.toPng(ref.current, { backgroundColor: "#ffffff", pixelRatio: 2 }); - const blob = await (await fetch(dataUrl)).blob(); - const file = new File([blob], `${order.ORDER_NO}_거래명세표.png`, { type: "image/png" }); - const navAny = navigator as Navigator & { canShare?: (data: { files: File[] }) => boolean }; - if (navAny.canShare && navAny.canShare({ files: [file] })) { - await navigator.share({ files: [file], title: `${order.ORDER_NO} 거래명세표` }); - return; - } - const a = document.createElement("a"); - a.href = dataUrl; - a.download = `${order.ORDER_NO}_거래명세표.png`; - a.click(); - Swal.fire({ icon: "success", title: "이미지 저장 완료", text: "다운로드한 이미지를 카톡 등에 첨부해 보내세요.", timer: 1500, showConfirmButton: false }); - } catch (err) { - console.error(err); - Swal.fire({ icon: "error", title: "이미지 캡처 실패", text: "잠시 후 다시 시도하세요." }); - } + if (!ref.current) return; + await captureAndShareLib({ + node: ref.current, + filename: `${order.ORDER_NO}_거래명세표`, + shareTitle: `${order.ORDER_NO} 거래명세표`, + shareText: order.COMPANY_NAME ?? "", + forceWidth: 1100, // 엑셀 가로 출력처럼 펼쳐서 캡처 + }); }; return ( diff --git a/src/lib/capture-share.ts b/src/lib/capture-share.ts index 06cd477..a12571d 100644 --- a/src/lib/capture-share.ts +++ b/src/lib/capture-share.ts @@ -13,14 +13,41 @@ interface CaptureShareOptions { shareTitle: string; /** 공유 텍스트 */ shareText?: string; + /** 캡처 시 가로폭 강제 — 모바일 좁은 layout 을 엑셀 가로출력처럼 펼쳐 캡처할 때 사용 (px) */ + forceWidth?: number; } -export async function captureAndShare({ node, filename, shareTitle, shareText }: CaptureShareOptions): Promise { +export async function captureAndShare({ node, filename, shareTitle, shareText, forceWidth }: CaptureShareOptions): Promise { // 거래처에 보낼 이미지에서 내부 정보(.js-no-export)는 잠시 숨긴다. const hideEls = node.querySelectorAll(".js-no-export"); const prev: { el: HTMLElement; display: string }[] = []; hideEls.forEach((el) => { prev.push({ el, display: el.style.display }); el.style.display = "none"; }); + // forceWidth: 캡처 직전 임시로 node 의 width 를 강제하여 가로 layout 으로 펼친다. + // 모바일 화면(좁은 viewport)에서 좁게 줄어든 표/품명을 한 줄로 펼치기 위함. + const widthPrev: { + width: string; minWidth: string; maxWidth: string; + paddingLeft: string; paddingRight: string; + } | null = forceWidth + ? { + width: node.style.width, + minWidth: node.style.minWidth, + maxWidth: node.style.maxWidth, + paddingLeft: node.style.paddingLeft, + paddingRight: node.style.paddingRight, + } + : null; + if (forceWidth) { + node.style.width = `${forceWidth}px`; + node.style.minWidth = `${forceWidth}px`; + node.style.maxWidth = `${forceWidth}px`; + // 좌우 padding 이 작아도 보기 좋게 — 캡처 직전이라 시각적 영향 없음 (즉시 원복) + if (!node.style.paddingLeft) node.style.paddingLeft = "24px"; + if (!node.style.paddingRight) node.style.paddingRight = "24px"; + // layout reflow 강제 + void node.offsetWidth; + } + let dataUrl: string; try { const mod = await import("html-to-image"); @@ -53,6 +80,13 @@ export async function captureAndShare({ node, filename, shareTitle, shareText }: } finally { // 캡처 끝나면 (성공/실패 무관) 숨겼던 요소 복원 prev.forEach((p) => { p.el.style.display = p.display; }); + if (widthPrev) { + node.style.width = widthPrev.width; + node.style.minWidth = widthPrev.minWidth; + node.style.maxWidth = widthPrev.maxWidth; + node.style.paddingLeft = widthPrev.paddingLeft; + node.style.paddingRight = widthPrev.paddingRight; + } } // 공유 또는 다운로드 단계