23599b9c18
Deploy momo-erp / deploy (push) Failing after 42s
- html-to-image 의 toPng 가 Pretendard CDN 임베드 단계에서 fail 하면 캡처 전체가 깨짐 (Windows Chrome 에서 자주 발생). skipFonts + cacheBust + jpeg fallback 추가 - 거래명세표(orders) / 발주서(procurements) 양쪽이 같은 코드를 복붙으로 갖고 있던 걸 lib/capture-share.ts 로 통합 - 실패 시 err.message 를 swal 에 노출 (이전엔 "잠시 후 다시 시도하세요" 만 떠서 사용자가 원인 추적 불가) - navigator.share 의 AbortError(사용자 취소) 는 silent 처리 + 그 외엔 다운로드 폴백 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
// 거래명세표 / 발주서 등 DOM 영역을 PNG 로 캡처해 공유 또는 다운로드.
|
|
// Windows Chrome 에서 html-to-image 의 toPng 가 외부 폰트(Pretendard CDN) 임베드 단계에서
|
|
// 실패하는 케이스를 회피하기 위해 skipFonts + cacheBust + jpeg fallback 을 사용한다.
|
|
|
|
import Swal from "sweetalert2";
|
|
|
|
interface CaptureShareOptions {
|
|
/** 캡처 대상 DOM (영역 외 버튼은 미리 .js-no-export 클래스로 hide 처리됨) */
|
|
node: HTMLElement;
|
|
/** 저장 파일명 베이스 (확장자 제외) */
|
|
filename: string;
|
|
/** 공유 타이틀 (Web Share API) */
|
|
shareTitle: string;
|
|
/** 공유 텍스트 */
|
|
shareText?: string;
|
|
}
|
|
|
|
export async function captureAndShare({ node, filename, shareTitle, shareText }: 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"; });
|
|
|
|
let dataUrl: string;
|
|
try {
|
|
const mod = await import("html-to-image");
|
|
const baseOpts = {
|
|
backgroundColor: "#ffffff",
|
|
pixelRatio: 2,
|
|
cacheBust: true,
|
|
// Windows Chrome 에서 외부 CDN 폰트(Pretendard) 임베드 단계가
|
|
// CORS/네트워크 문제로 실패하면 toPng 전체가 깨짐 → 폰트 임베드 스킵
|
|
skipFonts: true,
|
|
} as const;
|
|
|
|
try {
|
|
dataUrl = await mod.toPng(node, baseOpts);
|
|
} catch (pngErr) {
|
|
console.warn("[capture] toPng 실패, toJpeg 로 재시도:", pngErr instanceof Error ? pngErr.message : pngErr);
|
|
dataUrl = await mod.toJpeg(node, { ...baseOpts, quality: 0.95 });
|
|
}
|
|
} catch (err) {
|
|
// 복원 후 에러 표시
|
|
prev.forEach((p) => { p.el.style.display = p.display; });
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
console.error("[capture] 캡처 자체 실패:", err);
|
|
await Swal.fire({
|
|
icon: "error",
|
|
title: "이미지 캡처 실패",
|
|
html: `<div class="text-left text-xs">${msg}</div><br><span class="text-xs text-slate-500">브라우저 콘솔(F12)에 자세한 로그가 있습니다.</span>`,
|
|
});
|
|
return;
|
|
} finally {
|
|
// 캡처 끝나면 (성공/실패 무관) 숨겼던 요소 복원
|
|
prev.forEach((p) => { p.el.style.display = p.display; });
|
|
}
|
|
|
|
// 공유 또는 다운로드 단계
|
|
try {
|
|
const ext = dataUrl.startsWith("data:image/jpeg") ? "jpg" : "png";
|
|
const mime = ext === "jpg" ? "image/jpeg" : "image/png";
|
|
const blob = await (await fetch(dataUrl)).blob();
|
|
const file = new File([blob], `${filename}.${ext}`, { type: mime });
|
|
|
|
// Web Share API (모바일 + 일부 데스크톱) - files 공유 지원 시
|
|
const navAny = navigator as Navigator & { canShare?: (data: { files: File[] }) => boolean };
|
|
if (navAny.canShare && navAny.canShare({ files: [file] })) {
|
|
try {
|
|
await navigator.share({ files: [file], title: shareTitle, text: shareText ?? "" });
|
|
return;
|
|
} catch (shareErr) {
|
|
// 사용자가 취소했거나 share API 실패 → 다운로드로 폴백
|
|
const name = (shareErr as Error)?.name;
|
|
if (name === "AbortError") return; // 사용자 취소면 그냥 종료
|
|
console.warn("[capture] navigator.share 실패, 다운로드로 폴백:", shareErr);
|
|
}
|
|
}
|
|
|
|
// 폴백: 다운로드
|
|
const a = document.createElement("a");
|
|
a.href = dataUrl;
|
|
a.download = `${filename}.${ext}`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
Swal.fire({
|
|
icon: "success",
|
|
title: "이미지 저장 완료",
|
|
text: "다운로드한 이미지를 카톡 등에 첨부해 보내세요.",
|
|
timer: 1500,
|
|
showConfirmButton: false,
|
|
});
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
console.error("[capture] 공유/다운로드 단계 실패:", err);
|
|
Swal.fire({
|
|
icon: "error",
|
|
title: "이미지 공유 실패",
|
|
html: `<div class="text-left text-xs">${msg}</div>`,
|
|
});
|
|
}
|
|
}
|