From 71f4e15800b0cf771d1dddab6cc46fc7c2a17c51 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 11 Aug 2025 00:19:29 +0000 Subject: (최겸) PQ 기본계약DocxToPdf 한글화 적용 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pdftron/serverSDK/createBasicContractPdf.ts | 133 ++++-------- lib/vendors/table/request-pq-dialog.tsx | 267 ++++++++++++++---------- pages/api/pdftron/createBasicContractPdf.ts | 16 +- 3 files changed, 199 insertions(+), 217 deletions(-) diff --git a/lib/pdftron/serverSDK/createBasicContractPdf.ts b/lib/pdftron/serverSDK/createBasicContractPdf.ts index a2e0b350..706508e6 100644 --- a/lib/pdftron/serverSDK/createBasicContractPdf.ts +++ b/lib/pdftron/serverSDK/createBasicContractPdf.ts @@ -1,4 +1,7 @@ const { PDFNet } = require("@pdftron/pdfnet-node"); +const fs = require('fs').promises; +const path = require('path'); +import { file as tmpFile } from "tmp-promise"; type CreateBasicContractPdf = ( templateBuffer: Buffer, @@ -15,99 +18,43 @@ export const createBasicContractPdf: CreateBasicContractPdf = async ( templateBuffer, templateData ) => { - const main = async () => { - await PDFNet.initialize(process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY); + const result = await PDFNet.runWithCleanup(async () => { console.log("🔄 PDFTron 기본계약서 PDF 변환 시작"); console.log("📝 템플릿 데이터:", JSON.stringify(templateData, null, 2)); - // 템플릿 데이터가 있는 경우 변수 치환 후 PDF 변환 - if (Object.keys(templateData).length > 0) { - console.log("🔄 템플릿 변수 치환 시작"); - - try { - // createReport.ts 방식처럼 템플릿 변수 치환 (UTF-8 인코딩 지원) - const options = new PDFNet.Convert.OfficeToPDFOptions(); - - // UTF-8 인코딩 명시 설정 시도 - try { - options.setCharset("UTF-8"); - console.log("✅ UTF-8 인코딩 설정 완료"); - } catch (charsetError) { - console.warn("⚠️ UTF-8 인코딩 설정 실패, 기본 설정 사용:", charsetError); - } - - // 템플릿 데이터를 UTF-8로 명시적으로 인코딩 - const templateDataJson = JSON.stringify(templateData, null, 2); - const utf8TemplateData = Buffer.from(templateDataJson, 'utf8').toString('utf8'); - console.log("📝 UTF-8 인코딩된 템플릿 데이터:", utf8TemplateData); - - const tempPath = `/tmp/temp_template_${Date.now()}.docx`; - - // 파일도 UTF-8로 저장 (바이너리 데이터는 그대로 유지) - require('fs').writeFileSync(tempPath, templateBuffer, { encoding: null }); // 바이너리로 저장 + // 임시 파일 생성 + const { path: tempDocxPath, cleanup } = await tmpFile({ + postfix: ".docx", + }); + + try { + // 템플릿 버퍼를 임시 파일로 저장 + await fs.writeFile(tempDocxPath, templateBuffer); + + let resultDoc; + + // 템플릿 데이터가 있는 경우 변수 치환, 없으면 단순 변환 + if (templateData && Object.keys(templateData).length > 0) { + console.log("🔄 템플릿 변수 치환 시작"); - // Office 템플릿 생성 및 변수 치환 - const templateDoc = await PDFNet.Convert.createOfficeTemplateWithPath( - tempPath, - options + const template = await PDFNet.Convert.createOfficeTemplateWithPath( + tempDocxPath ); - - const filledDoc = await templateDoc.fillTemplateJson(utf8TemplateData); - - // 임시 파일 삭제 - require('fs').unlinkSync(tempPath); - - console.log("✅ 템플릿 변수 치환 및 PDF 변환 완료"); - - const buffer = await filledDoc.saveMemoryBuffer( - PDFNet.SDFDoc.SaveOptions.e_linearized + resultDoc = await template.fillTemplateJson( + JSON.stringify(templateData) ); - - return { - result: true, - buffer, - }; - } catch (templateError) { - console.warn("⚠️ 템플릿 변수 치환 실패, 기본 변환 수행:", templateError); - - // 템플릿 처리 실패 시 기본 PDF 변환만 수행 (UTF-8 인코딩 적용) - const fallbackOptions = new PDFNet.Convert.OfficeToPDFOptions(); - try { - fallbackOptions.setCharset("UTF-8"); - } catch (charsetError) { - console.warn("⚠️ 폴백 UTF-8 인코딩 설정 실패:", charsetError); - } + console.log("✅ 템플릿 변수 치환 및 PDF 변환 완료"); + } else { + console.log("📄 단순 PDF 변환 수행"); - const buf = await PDFNet.Convert.office2PDFBuffer(templateBuffer, fallbackOptions); - const templateDoc = await PDFNet.PDFDoc.createFromBuffer(buf); + resultDoc = await PDFNet.Convert.office2PDF(tempDocxPath); - const buffer = await templateDoc.saveMemoryBuffer( - PDFNet.SDFDoc.SaveOptions.e_linearized - ); - - return { - result: true, - buffer, - }; + console.log("✅ 단순 PDF 변환 완료"); } - } else { - // 템플릿 데이터가 없는 경우 단순 변환 (UTF-8 인코딩 적용) - console.log("📄 단순 PDF 변환 수행 (UTF-8 인코딩)"); - - const simpleOptions = new PDFNet.Convert.OfficeToPDFOptions(); - try { - simpleOptions.setCharset("UTF-8"); - console.log("✅ 단순 변환 UTF-8 인코딩 설정 완료"); - } catch (charsetError) { - console.warn("⚠️ 단순 변환 UTF-8 인코딩 설정 실패:", charsetError); - } - - const buf = await PDFNet.Convert.office2PDFBuffer(templateBuffer, simpleOptions); - const templateDoc = await PDFNet.PDFDoc.createFromBuffer(buf); - - const buffer = await templateDoc.saveMemoryBuffer( + + const buffer = await resultDoc.saveMemoryBuffer( PDFNet.SDFDoc.SaveOptions.e_linearized ); @@ -115,23 +62,13 @@ export const createBasicContractPdf: CreateBasicContractPdf = async ( result: true, buffer, }; + + } finally { + // 임시 파일 정리 + await cleanup(); } - }; - - const result = await PDFNet.runWithCleanup( - main, + }, process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY - ) - .catch((err: any) => { - console.error("❌ PDFTron 기본계약서 PDF 변환 오류:", err); - return { - result: false, - error: err, - }; - }) - .then(async (data: any) => { - return data; - }); - + ); return result; }; diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx index 1df2d72c..226a053f 100644 --- a/lib/vendors/table/request-pq-dialog.tsx +++ b/lib/vendors/table/request-pq-dialog.tsx @@ -43,6 +43,7 @@ import { useSession } from "next-auth/react" import { DatePicker } from "@/components/ui/date-picker" import { getALLBasicContractTemplates } from "@/lib/basic-contract/service" import type { BasicContractTemplate } from "@/db/schema" +// import { PQContractViewer } from "../pq-contract-viewer" // 더 이상 사용하지 않음 interface RequestPQDialogProps extends React.ComponentPropsWithoutRef { vendors: Row["original"][] @@ -78,6 +79,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro const [selectedTemplateIds, setSelectedTemplateIds] = React.useState([]) const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false) + React.useEffect(() => { if (type === "PROJECT") { setIsLoadingProjects(true) @@ -104,6 +106,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro setPqItems("") setExtraNote("") setSelectedTemplateIds([]) + } }, [props.open]) @@ -116,7 +119,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro startApproveTransition(async () => { try { // 1단계: PQ 생성 - console.log("🚀 1단계: PQ 생성 시작") + console.log("🚀 PQ 생성 시작") const { error: pqError } = await requestPQVendors({ ids: vendors.map((v) => v.id), userId: Number(session.user.id), @@ -133,128 +136,156 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro toast.error(`PQ 생성 실패: ${pqError}`) return } - console.log("✅ 1단계: PQ 생성 완료") + console.log("✅ PQ 생성 완료") + toast.success("PQ가 성공적으로 요청되었습니다") - // 2단계 & 3단계: 기본계약서 템플릿이 선택된 경우에만 실행 (여러 템플릿 처리) + // 2단계: 기본계약서 템플릿이 선택된 경우 백그라운드에서 처리 if (selectedTemplateIds.length > 0) { - console.log(`🚀 2단계 & 3단계: ${selectedTemplateIds.length}개 템플릿 처리 시작`) + const templates = basicContractTemplates.filter(t => + selectedTemplateIds.includes(t.id) + ) - let successCount = 0 - let errorCount = 0 - const errors: string[] = [] - - // 템플릿별로 반복 처리 - for (let i = 0; i < selectedTemplateIds.length; i++) { - const templateId = selectedTemplateIds[i] - const selectedTemplate = basicContractTemplates.find(t => t.id === templateId) - - if (!selectedTemplate) { - console.error(`템플릿 ID ${templateId}를 찾을 수 없습니다`) - errorCount++ - errors.push(`템플릿 ID ${templateId}를 찾을 수 없습니다`) - continue - } - - try { - console.log(`📄 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 2단계: DOCX to PDF 변환 시작`) - - // 템플릿 파일을 가져와서 PDF로 변환 - const formData = new FormData() - - // 템플릿 파일 가져오기 (서버에서 파일 읽기) - const templateResponse = await fetch('/api/basic-contract/get-template', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ templateId }) - }) - - if (!templateResponse.ok) { - throw new Error(`템플릿 파일을 가져올 수 없습니다: ${selectedTemplate.templateName}`) - } - console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 템플릿 파일 가져오기 완료`) - - const templateBlob = await templateResponse.blob() - const templateFile = new File([templateBlob], selectedTemplate.fileName || 'template.docx') - - // 템플릿 데이터 생성 (첫 번째 협력업체 정보 기반) - const firstVendor = vendors[0] - const templateData = { - // 영문 변수명으로 변경 (PDFTron이 한글 변수명을 지원하지 않음) - vendor_name: firstVendor?.vendorName || '협력업체명', - address: firstVendor?.address || '주소', - representative_name: firstVendor?.representativeName || '대표자명', - today_date: new Date().toLocaleDateString('ko-KR'), - } - - console.log(`📝 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 생성된 템플릿 데이터:`, templateData) - - formData.append('templateFile', templateFile) - formData.append('outputFileName', `${selectedTemplate.templateName}_converted.pdf`) - formData.append('templateData', JSON.stringify(templateData)) - - // PDF 변환 호출 - const pdfResponse = await fetch('/api/pdftron/createBasicContractPdf', { - method: 'POST', - body: formData, - }) - console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - PDF 변환 호출 완료`) - - if (!pdfResponse.ok) { - const errorText = await pdfResponse.text() - throw new Error(`PDF 변환 실패 (${selectedTemplate.templateName}): ${errorText}`) - } - - const pdfBuffer = await pdfResponse.arrayBuffer() - console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - PDF 변환 완료`) - - // 3단계: 변환된 PDF로 기본계약 생성 - console.log(`📋 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 3단계: 기본계약 생성 시작`) - const { error: contractError } = await requestBasicContractInfo({ - vendorIds: vendors.map((v) => v.id), - requestedBy: Number(session.user.id), - templateId, - pdfBuffer: new Uint8Array(pdfBuffer), // ArrayBuffer를 Uint8Array로 변환하여 전달 - }) - - if (contractError) { - console.error(`기본계약 생성 오류 (${selectedTemplate.templateName}):`, contractError) - errorCount++ - errors.push(`${selectedTemplate.templateName}: ${contractError}`) - } else { - console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 3단계: 기본계약 생성 완료`) - successCount++ - } - } catch (templateError) { - console.error(`템플릿 처리 오류 (${selectedTemplate.templateName}):`, templateError) - errorCount++ - errors.push(`${selectedTemplate.templateName}: ${templateError instanceof Error ? templateError.message : '알 수 없는 오류'}`) - } - } - - // 결과 토스트 메시지 - if (successCount > 0 && errorCount === 0) { - toast.success(`PQ 요청 및 ${successCount}개 기본계약서 생성이 모두 완료되었습니다!`) - } else if (successCount > 0 && errorCount > 0) { - toast.success(`PQ는 성공적으로 요청되었습니다. ${successCount}개 기본계약서 성공, ${errorCount}개 실패`) - console.error('기본계약서 생성 오류들:', errors) - } else if (errorCount > 0) { - toast.error(`PQ는 성공적으로 요청되었지만, 모든 기본계약서 생성이 실패했습니다`) - console.error('기본계약서 생성 오류들:', errors) - } - } else { - // 기본계약서 템플릿이 선택되지 않은 경우 - toast.success("PQ가 성공적으로 요청되었습니다") + console.log("📋 기본계약서 백그라운드 처리 시작", templates.length, "개 템플릿") + await processBasicContractsInBackground(templates, vendors) } - + + // 완료 후 다이얼로그 닫기 props.onOpenChange?.(false) onSuccess?.() + } catch (error) { - console.error('전체 프로세스 오류:', error) + console.error('PQ 생성 오류:', error) toast.error(`처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) } }) } + // 백그라운드에서 기본계약서 처리 + const processBasicContractsInBackground = async (templates: BasicContractTemplate[], vendors: any[]) => { + if (!session?.user?.id) { + toast.error("인증 정보가 없습니다") + return + } + + try { + const totalContracts = templates.length * vendors.length + let processedCount = 0 + + // 각 벤더별로, 각 템플릿을 처리 + for (let vendorIndex = 0; vendorIndex < vendors.length; vendorIndex++) { + const vendor = vendors[vendorIndex] + + // 벤더별 템플릿 데이터 생성 + const templateData = { + vendor_name: vendor.vendorName || '협력업체명', + address: vendor.address || '주소', + representative_name: vendor.representativeName || '대표자명', + today_date: new Date().toLocaleDateString('ko-KR'), + } + + console.log(`🔄 벤더 ${vendorIndex + 1}/${vendors.length} 템플릿 데이터:`, templateData) + + // 해당 벤더에 대해 각 템플릿을 순차적으로 처리 + for (let templateIndex = 0; templateIndex < templates.length; templateIndex++) { + const template = templates[templateIndex] + processedCount++ + + console.log(`📄 처리 중: ${vendor.vendorName} - ${template.templateName} (${processedCount}/${totalContracts})`) + + // 개별 벤더에 대한 기본계약 생성 + await processTemplate(template, templateData, [vendor]) + + console.log(`✅ 완료: ${vendor.vendorName} - ${template.templateName}`) + } + } + + toast.success(`총 ${totalContracts}개 기본계약이 모두 생성되었습니다`) + + } catch (error) { + console.error('기본계약 처리 중 오류:', error) + toast.error(`기본계약 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`) + } + } + + const processTemplate = async (template: BasicContractTemplate, templateData: any, vendors: any[]) => { + try { + // 1. 템플릿 파일 가져오기 + const templateResponse = await fetch('/api/basic-contract/get-template', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ templateId: template.id }) + }) + + if (!templateResponse.ok) { + throw new Error(`템플릿 파일을 가져올 수 없습니다: ${template.templateName}`) + } + + const templateBlob = await templateResponse.blob() + + // 2. PDFTron을 사용해서 변수 치환 및 PDF 변환 + // @ts-ignore + const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer) + + // 임시 WebViewer 인스턴스 생성 (DOM에 추가하지 않음) + const tempDiv = document.createElement('div') + tempDiv.style.display = 'none' + document.body.appendChild(tempDiv) + + const instance = await WebViewer( + { + path: "/pdftronWeb", + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + fullAPI: true, + }, + tempDiv + ) + + try { + const { Core } = instance + const { createDocument } = Core + + // 3. 템플릿 문서 생성 및 변수 치환 + const templateDoc = await createDocument(templateBlob, { + filename: template.fileName || 'template.docx', + extension: 'docx', + }) + + console.log("🔄 변수 치환 시작:", templateData) + await templateDoc.applyTemplateValues(templateData) + console.log("✅ 변수 치환 완료") + + // 4. PDF 변환 + const fileData = await templateDoc.getFileData() + const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' }) + + console.log(`✅ PDF 변환 완료: ${template.templateName}`, `크기: ${pdfBuffer.byteLength} bytes`) + + // 5. 기본계약 생성 요청 + const { error: contractError } = await requestBasicContractInfo({ + vendorIds: vendors.map((v) => v.id), + requestedBy: Number(session!.user.id), + templateId: template.id, + pdfBuffer: new Uint8Array(pdfBuffer), + }) + + if (contractError) { + throw new Error(contractError) + } + + console.log(`✅ 기본계약 생성 완료: ${template.templateName}`) + + } finally { + // 임시 WebViewer 정리 + instance.UI.dispose() + document.body.removeChild(tempDiv) + } + + } catch (error) { + console.error(`❌ 템플릿 처리 실패: ${template.templateName}`, error) + throw error + } + } + const dialogContent = (
{/* 선택된 협력업체 정보 */} @@ -309,7 +340,15 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro setDueDate(date ? date.toISOString().slice(0, 10) : "")} + onSelect={(date?: Date) => { + if (date) { + // 한국 시간대로 날짜 변환 (UTC 변환으로 인한 날짜 변경 방지) + const kstDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000) + setDueDate(kstDate.toISOString().slice(0, 10)) + } else { + setDueDate("") + } + }} placeholder="마감일 선택" />
@@ -421,6 +460,8 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro + + ) } @@ -452,6 +493,8 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro + + ) } diff --git a/pages/api/pdftron/createBasicContractPdf.ts b/pages/api/pdftron/createBasicContractPdf.ts index 1122c022..376d8540 100644 --- a/pages/api/pdftron/createBasicContractPdf.ts +++ b/pages/api/pdftron/createBasicContractPdf.ts @@ -294,13 +294,15 @@ export default async function handler( // 4. 원본 파일 읽기 const originalBuffer = await fs.readFile(templateFile.filepath); - + // const publicDir = path.join(process.cwd(), "public", "basicContract"); + // const testBuffer = await fs.readFile(path.join(publicDir, "test123.docx")); + // console.log(testBuffer); // 5. DRM 복호화 처리 (보안 검증 포함) - console.log(`🔐 [${requestId}] DRM 복호화 시작: ${templateFile.originalFilename || 'unknown'}`); - const decryptedBuffer = await decryptBufferWithDRM( - originalBuffer, - templateFile.originalFilename || 'template.docx' - ); + // console.log(`🔐 [${requestId}] DRM 복호화 시작: ${templateFile.originalFilename || 'unknown'}`); + // const decryptedBuffer = await decryptBufferWithDRM( + // originalBuffer, + // templateFile.originalFilename || 'template.docx' + // ); // 6. 복호화된 버퍼로 기본계약서 PDF 생성 console.log(`📄 [${requestId}] 기본계약서 PDF 생성 시작`); @@ -308,7 +310,7 @@ export default async function handler( result, buffer: pdfBuffer, error, - } = await createBasicContractPdf(decryptedBuffer, templateData); + } = await createBasicContractPdf(originalBuffer, templateData); if (result && pdfBuffer) { console.log(`✅ [${requestId}] 기본계약서 PDF 생성 성공: ${outputFileName}`); -- cgit v1.2.3