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:
@@ -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, // 엑셀 가로 출력처럼 펼쳐서 캡처
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
// 공유 또는 다운로드 단계
|
||||
|
||||
Reference in New Issue
Block a user