diff --git a/frontend/app/(main)/COMPANY_16/sales/estimate/template1/pop/[contractObjid]/page.tsx b/frontend/app/(main)/COMPANY_16/sales/estimate/template1/pop/[contractObjid]/page.tsx index a8bdd8c2..9b57fd72 100644 --- a/frontend/app/(main)/COMPANY_16/sales/estimate/template1/pop/[contractObjid]/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/estimate/template1/pop/[contractObjid]/page.tsx @@ -286,6 +286,66 @@ export default function EstimateTemplate1Page() { window.print(); } + // PDF 다운로드 — wace fn_generatePdf 1:1 (클라이언트 html2canvas + jsPDF) + async function handleDownloadPdf() { + if (!confirm("PDF로 다운로드 하시겠습니까?")) return; + try { + // html2canvas-pro: oklab/oklch 등 모던 CSS color function 지원 (Tailwind 4 호환) + const html2canvas = (await import("html2canvas-pro")).default; + const { jsPDF } = await import("jspdf"); + + const container = document.querySelector(".estimate-container") as HTMLElement | null; + if (!container) return; + + const canvas = await html2canvas(container, { + scale: 2, + useCORS: true, + logging: false, + backgroundColor: "#ffffff", + // 클론된 DOM에서 input/textarea/select → 텍스트 노드로 교체 (글자 잘림 방지) + onclone: (doc) => { + const replaceWithText = (el: HTMLElement, text: string) => { + const span = doc.createElement("span"); + span.textContent = text; + const style = el.getAttribute("style") || ""; + span.setAttribute("style", style + ";display:inline-block;white-space:pre-wrap;"); + el.parentNode?.replaceChild(span, el); + }; + doc.querySelectorAll('input[type="text"],input[type="date"]').forEach(el => replaceWithText(el, el.value || "")); + doc.querySelectorAll("textarea").forEach(el => replaceWithText(el, el.value || "")); + doc.querySelectorAll("select").forEach(el => { + const opt = el.options[el.selectedIndex]; + replaceWithText(el as unknown as HTMLElement, opt?.text || ""); + }); + // 인쇄 비대상 요소 숨김 + doc.querySelectorAll(".no-print, .btn-area, .delete-btn-cell button").forEach(el => { el.style.display = "none"; }); + }, + }); + + const imgData = canvas.toDataURL("image/jpeg", 0.85); + const pdf = new jsPDF("p", "mm", "a4"); + const imgWidth = 210, pageHeight = 297; + const imgHeight = canvas.height * imgWidth / canvas.width; + let heightLeft = imgHeight; + let position = 0; + + pdf.addImage(imgData, "JPEG", 0, position, imgWidth, imgHeight, undefined, "FAST"); + heightLeft -= pageHeight; + while (heightLeft > 1) { + position = heightLeft - imgHeight; + pdf.addPage(); + pdf.addImage(imgData, "JPEG", 0, position, imgWidth, imgHeight, undefined, "FAST"); + heightLeft -= pageHeight; + } + + const fileName = (estimateNo || "견적서") + ".pdf"; + pdf.save(fileName); + } catch (e: any) { + console.error("PDF 생성 오류", e); + alert("PDF 생성 중 오류가 발생했습니다.\n" + (e?.message ?? "")); + } + } + function handleClose() { // 새 탭으로 열린 경우 닫기 시도 + 안되면 router back if (window.opener) { @@ -668,8 +728,7 @@ export default function EstimateTemplate1Page() { {/* 버튼 영역 (고정 하단) */}
- +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index df34f0cb..0cc50fe9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -68,9 +68,10 @@ "exceljs": "^4.4.0", "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", + "html2canvas-pro": "^2.0.2", "isomorphic-dompurify": "^2.28.0", "jsbarcode": "^3.12.1", - "jspdf": "^3.0.3", + "jspdf": "^3.0.4", "leaflet": "^1.9.4", "lucide-react": "^0.525.0", "mammoth": "^1.11.0", @@ -10942,6 +10943,19 @@ "node": ">=8.0.0" } }, + "node_modules/html2canvas-pro": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html2canvas-pro/-/html2canvas-pro-2.0.2.tgz", + "integrity": "sha512-9G/t0XgCZWonLwL0JwI7su6NdbOPUY7Ur4Ihpp8+XMaW9ibA2nDXF181Jr6tm94k8lX6sthpaXB3XqEnsMd5Cw==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -11696,12 +11710,12 @@ } }, "node_modules/jspdf": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.3.tgz", - "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", + "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.9", + "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, diff --git a/frontend/package.json b/frontend/package.json index 1edcfab3..a0d6e7d6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -77,9 +77,10 @@ "exceljs": "^4.4.0", "html-to-image": "^1.11.13", "html2canvas": "^1.4.1", + "html2canvas-pro": "^2.0.2", "isomorphic-dompurify": "^2.28.0", "jsbarcode": "^3.12.1", - "jspdf": "^3.0.3", + "jspdf": "^3.0.4", "leaflet": "^1.9.4", "lucide-react": "^0.525.0", "mammoth": "^1.11.0",