feat(capture): 거래명세표/매입발주서 이미지 공유 가로 출력

- src/lib/capture-share.ts 에 forceWidth 옵션 추가 → 캡처 직전 임시로 node width 강제 + 즉시 원복
- 출고 처리(거래명세표) 와 매입발주서 관리의 이미지 공유 호출에 forceWidth: 1100 적용
- 모바일 화면(좁은 viewport)에서 좁아진 표/품명 셀이 한 줄로 펼쳐져 엑셀 가로 출력처럼 캡처됨
- m/orders 페이지의 inline captureAndShare 를 capture-share lib 으로 통일 (toJpeg fallback / AbortError 처리 공유)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-05-12 23:44:37 +09:00
parent 20e6255aa3
commit 550fb12913
4 changed files with 46 additions and 21 deletions
+1
View File
@@ -457,6 +457,7 @@ function StatementPreview({
filename: `${order.ORDER_NO}_거래명세표`,
shareTitle: `${order.ORDER_NO} 거래명세표`,
shareText: order.COMPANY_NAME,
forceWidth: 1100, // 엑셀 가로 출력처럼 펼쳐서 캡처
});
};
@@ -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, // 엑셀 가로 출력처럼 펼쳐서 캡처
});
};
+9 -20
View File
@@ -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 (
+35 -1
View File
@@ -13,14 +13,41 @@ interface CaptureShareOptions {
shareTitle: string;
/** 공유 텍스트 */
shareText?: string;
/** 캡처 시 가로폭 강제 — 모바일 좁은 layout 을 엑셀 가로출력처럼 펼쳐 캡처할 때 사용 (px) */
forceWidth?: number;
}
export async function captureAndShare({ node, filename, shareTitle, shareText }: CaptureShareOptions): Promise<void> {
export async function captureAndShare({ node, filename, shareTitle, shareText, forceWidth }: CaptureShareOptions): Promise<void> {
// 거래처에 보낼 이미지에서 내부 정보(.js-no-export)는 잠시 숨긴다.
const hideEls = node.querySelectorAll<HTMLElement>(".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;
}
}
// 공유 또는 다운로드 단계