From 3c9a95332298450c7e0f75bfb08944439e1a3739 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 1 Dec 2025 03:09:00 +0000 Subject: (최겸)구매 일반계약 템플릿 자동 연동 및 매핑 기능 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../general-contract-review-request-dialog.tsx | 281 ++++++--------------- 1 file changed, 84 insertions(+), 197 deletions(-) (limited to 'lib/general-contracts/detail/general-contract-review-request-dialog.tsx') diff --git a/lib/general-contracts/detail/general-contract-review-request-dialog.tsx b/lib/general-contracts/detail/general-contract-review-request-dialog.tsx index b487ae25..c31ce4ac 100644 --- a/lib/general-contracts/detail/general-contract-review-request-dialog.tsx +++ b/lib/general-contracts/detail/general-contract-review-request-dialog.tsx @@ -9,11 +9,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' -import { Input } from '@/components/ui/input' import { toast } from 'sonner' import { FileText, - Upload, Eye, Send, CheckCircle, @@ -23,10 +21,12 @@ import { getBasicInfo, getContractItems, getSubcontractChecklist, - uploadContractReviewFile, sendContractReviewRequest, - getContractById + getContractById, + getContractTemplateByContractType, + getStorageInfo } from '../service' +import { mapContractDataToTemplateVariables } from '../utils' interface ContractReviewRequestDialogProps { contract: Record @@ -38,6 +38,7 @@ interface ContractSummary { basicInfo: Record items: Record[] subcontractChecklist: Record | null + storageInfo?: Record[] } export function ContractReviewRequestDialog({ @@ -58,63 +59,6 @@ export function ContractReviewRequestDialog({ const contractId = contract.id as number const userId = session?.user?.id || '' - // LOI 템플릿용 변수 매핑 함수 - const mapContractSummaryToLOITemplate = (contractSummary: ContractSummary) => { - const { basicInfo, items } = contractSummary - const firstItem = items && items.length > 0 ? items[0] : {} - - // 날짜 포맷팅 헬퍼 함수 - const formatDate = (date: unknown) => { - if (!date) return '' - try { - const d = new Date(date) - return d.toLocaleDateString('ko-KR') - } catch { - return '' - } - } - - return { - // 날짜 관련 (템플릿에서 {{todayDate}} 형식으로 사용) - todayDate: new Date().toLocaleDateString('ko-KR'), - - // 벤더 정보 - vendorName: basicInfo?.vendorName || '', - representativeName: '', // 벤더 대표자 이름 - 현재 데이터에 없음, 향후 확장 가능 - - // 계약 기본 정보 - contractNumber: basicInfo?.contractNumber || '', - - // 프로젝트 정보 - projectNumber: '', // 프로젝트 코드 - 현재 데이터에 없음, 향후 확장 가능 - projectName: basicInfo?.projectName || '', - project: basicInfo?.projectName || '', - - // 아이템 정보 - item: firstItem?.itemInfo || '', - - // 무역 조건 - incoterms: basicInfo?.deliveryTerm || '', // Incoterms 대신 deliveryTerm 사용 - shippingLocation: basicInfo?.shippingLocation || '', - - // 금액 및 통화 - contractCurrency: basicInfo?.currency || '', - contractAmount: basicInfo?.contractAmount || '', - totalAmount: basicInfo?.contractAmount || '', // totalAmount가 없으면 contractAmount 사용 - - // 수량 - quantity: firstItem?.quantity || '', - - // 납기일 - contractDeliveryDate: formatDate(basicInfo?.contractDeliveryDate), - - // 지급 조건 - paymentTerm: basicInfo?.paymentTerm || '', - - // 유효기간 - validityEndDate: formatDate(basicInfo?.endDate), // validityEndDate 대신 endDate 사용 - } - } // 1단계: 계약 현황 수집 const collectContractSummary = React.useCallback(async () => { @@ -164,6 +108,18 @@ export function ContractReviewRequestDialog({ } catch { console.log('Subcontract Checklist 데이터 없음') } + + // 임치(물품보관) 계약 정보 확인 (SG) + try { + if (summary.basicInfo?.contractType === 'SG') { + const storageData = await getStorageInfo(contractId) + if (storageData && storageData.length > 0) { + summary.storageInfo = storageData + } + } + } catch { + console.log('임치계약 정보 없음') + } console.log('contractSummary 구조:', summary) console.log('basicInfo 내용:', summary.basicInfo) @@ -176,56 +132,43 @@ export function ContractReviewRequestDialog({ } }, [contractId]) - // 3단계: 파일 업로드 처리 - const handleFileUpload = async (file: File) => { - // 파일 확장자 검증 - const allowedExtensions = ['.doc', '.docx'] - const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.')) - - if (!allowedExtensions.includes(fileExtension)) { - toast.error('Word 문서(.doc, .docx) 파일만 업로드 가능합니다.') - return - } - - if (!userId) { - toast.error('로그인이 필요합니다.') + // 2단계: PDF 생성 및 미리보기 (PDFTron 사용) - 템플릿 자동 로드 + const generatePdf = async () => { + if (!contractSummary) { + toast.error('계약 정보가 필요합니다.') return } setIsLoading(true) try { - // 서버액션을 사용하여 파일 저장 (조건검토용) - const result = await uploadContractReviewFile( - contractId, - file, - userId - ) + // 1. 계약 유형에 맞는 템플릿 조회 + const contractType = contractSummary.basicInfo.contractType as string + const templateResult = await getContractTemplateByContractType(contractType) - if (result.success) { - setUploadedFile(file) - toast.success('파일이 업로드되었습니다.') - } else { - throw new Error(result.error || '파일 업로드 실패') + if (!templateResult.success || !templateResult.template) { + throw new Error(templateResult.error || '템플릿을 찾을 수 없습니다.') } - } catch (error) { - console.error('Error uploading file:', error) - toast.error('파일 업로드 중 오류가 발생했습니다.') - } finally { - setIsLoading(false) - } - } - // 4단계: PDF 생성 및 미리보기 (PDFTron 사용) - const generatePdf = async () => { - if (!uploadedFile || !contractSummary) { - toast.error('업로드된 파일과 계약 정보가 필요합니다.') - return - } + const template = templateResult.template - setIsLoading(true) - try { - // PDFTron을 사용해서 변수 치환 및 PDF 변환 - // @ts-expect-error - PDFTron WebViewer dynamic import + // 2. 템플릿 파일 다운로드 + const templateResponse = await fetch("/api/contracts/get-template", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ templatePath: template.filePath }), + }) + + if (!templateResponse.ok) { + throw new Error("템플릿 파일을 다운로드할 수 없습니다.") + } + + const templateBlob = await templateResponse.blob() + const templateFile = new File([templateBlob], template.fileName || "template.docx", { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + }) + + // 3. PDFTron을 사용해서 변수 치환 및 PDF 변환 + // @ts-ignore const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer) // 임시 WebViewer 인스턴스 생성 (DOM에 추가하지 않음) @@ -246,34 +189,34 @@ export function ContractReviewRequestDialog({ const { Core } = instance const { createDocument } = Core - // 템플릿 문서 생성 및 변수 치환 - const templateDoc = await createDocument(uploadedFile, { - filename: uploadedFile.name, - extension: 'docx', - }) + // 템플릿 문서 생성 및 변수 치환 + const templateDoc = await createDocument(templateFile, { + filename: templateFile.name, + extension: 'docx', + }) - // LOI 템플릿용 변수 매핑 - const mappedTemplateData = mapContractSummaryToLOITemplate(contractSummary) + // 템플릿 변수 매핑 + const mappedTemplateData = mapContractDataToTemplateVariables(contractSummary) - console.log("🔄 변수 치환 시작:", mappedTemplateData) - await templateDoc.applyTemplateValues(mappedTemplateData as Record) - console.log("✅ 변수 치환 완료") + console.log("🔄 변수 치환 시작:", mappedTemplateData) + await templateDoc.applyTemplateValues(mappedTemplateData as any) + console.log("✅ 변수 치환 완료") - // PDF 변환 - const fileData = await templateDoc.getFileData() - const pdfBuffer = await (Core as { officeToPDFBuffer: (data: unknown, options: { extension: string }) => Promise }).officeToPDFBuffer(fileData, { extension: 'docx' }) + // PDF 변환 + const fileData = await templateDoc.getFileData() + const pdfBuffer = await (Core as any).officeToPDFBuffer(fileData, { extension: 'docx' }) - console.log(`✅ PDF 변환 완료: ${uploadedFile.name}`, `크기: ${pdfBuffer.byteLength} bytes`) + console.log(`✅ PDF 변환 완료: ${templateFile.name}`, `크기: ${pdfBuffer.byteLength} bytes`) - // PDF 버퍼를 Blob URL로 변환하여 미리보기 - const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' }) - const pdfUrl = URL.createObjectURL(pdfBlob) - setGeneratedPdfUrl(pdfUrl) - - // PDF 버퍼를 상태에 저장 (최종 전송 시 사용) - setGeneratedPdfBuffer(new Uint8Array(pdfBuffer)) - - toast.success('PDF가 생성되었습니다.') + // PDF 버퍼를 Blob URL로 변환하여 미리보기 + const pdfBlob = new Blob([pdfBuffer], { type: 'application/pdf' }) + const pdfUrl = URL.createObjectURL(pdfBlob) + setGeneratedPdfUrl(pdfUrl) + + // PDF 버퍼를 상태에 저장 (최종 전송 시 사용) + setGeneratedPdfBuffer(new Uint8Array(pdfBuffer)) + + toast.success('PDF가 생성되었습니다.') } finally { // 임시 WebViewer 정리 @@ -283,7 +226,8 @@ export function ContractReviewRequestDialog({ } catch (error) { console.error('❌ PDF 생성 실패:', error) - toast.error('PDF 생성 중 오류가 발생했습니다.') + const errorMessage = error instanceof Error ? error.message : '알 수 없는 오류' + toast.error(`PDF 생성 중 오류가 발생했습니다: ${errorMessage}`) } finally { setIsLoading(false) } @@ -298,7 +242,7 @@ export function ContractReviewRequestDialog({ setIsLoading(true) try { - // @ts-expect-error - PDFTron WebViewer dynamic import + // @ts-ignore - PDFTron WebViewer dynamic import const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer) // 기존 인스턴스가 있다면 정리 @@ -350,13 +294,13 @@ export function ContractReviewRequestDialog({ setPdfViewerInstance(instance) // PDF 버퍼를 Blob으로 변환 - const pdfBlob = new Blob([generatedPdfBuffer], { type: 'application/pdf' }) + const pdfBlob = new Blob([generatedPdfBuffer as any], { type: 'application/pdf' }) const pdfUrl = URL.createObjectURL(pdfBlob) console.log("🔄 PDF Blob URL 생성:", pdfUrl) // 문서 로드 console.log("🔄 문서 로드 시작") - const { documentViewer } = instance.Core + const { documentViewer } = (instance as any).Core // 문서 로드 이벤트 대기 await new Promise((resolve, reject) => { @@ -406,7 +350,7 @@ export function ContractReviewRequestDialog({ return } - const pdfBlob = new Blob([generatedPdfBuffer], { type: 'application/pdf' }) + const pdfBlob = new Blob([generatedPdfBuffer as any], { type: 'application/pdf' }) const pdfUrl = URL.createObjectURL(pdfBlob) const link = document.createElement('a') @@ -426,6 +370,8 @@ export function ContractReviewRequestDialog({ if (pdfViewerInstance) { try { console.log("🔄 WebViewer 인스턴스 정리") + // @ts-expect-error - PDFTron WebViewer dynamic import + // @ts-ignore pdfViewerInstance.UI.dispose() } catch (error) { console.warn('WebViewer 정리 중 오류:', error) @@ -500,7 +446,6 @@ export function ContractReviewRequestDialog({ closePdfPreview() // 상태 초기화 setCurrentStep(1) - setUploadedFile(null) setGeneratedPdfUrl(null) setGeneratedPdfBuffer(null) setIsPdfPreviewVisible(false) @@ -520,15 +465,12 @@ export function ContractReviewRequestDialog({ - + 1. 미리보기 - 2. 템플릿 업로드 - - - 3. PDF 미리보기 + 2. PDF 미리보기 @@ -645,7 +587,7 @@ export function ContractReviewRequestDialog({
계약성립조건: {contractSummary?.basicInfo?.contractEstablishmentConditions && - Object.entries(contractSummary.basicInfo.contractEstablishmentConditions) + Object.entries(contractSummary.basicInfo.contractEstablishmentConditions as Record) .filter(([, value]) => value === true) .map(([key]) => key) .join(', ') || '없음'} @@ -653,7 +595,7 @@ export function ContractReviewRequestDialog({
계약해지조건: {contractSummary?.basicInfo?.contractTerminationConditions && - Object.entries(contractSummary.basicInfo.contractTerminationConditions) + Object.entries(contractSummary.basicInfo.contractTerminationConditions as Record) .filter(([, value]) => value === true) .map(([key]) => key) .join(', ') || '없음'} @@ -680,9 +622,9 @@ export function ContractReviewRequestDialog({
{contractSummary.items.slice(0, 3).map((item: Record, index: number) => (
-
{item.itemInfo || item.description || `품목 ${index + 1}`}
+
{String(item.itemInfo || item.description || `품목 ${index + 1}`)}
- 수량: {item.quantity || 0} | 단가: {item.contractUnitPrice || item.unitPrice || 0} + 수량: {String(item.quantity || 0)} | 단가: {String(item.contractUnitPrice || item.unitPrice || 0)}
))} @@ -734,62 +676,8 @@ export function ContractReviewRequestDialog({
- {/* 2단계: 문서 업로드 */} + {/* 2단계: PDF 미리보기 (자동 생성) */} - - - - - 계약서 템플릿 업로드 - - - -
-

일반계약 표준문서 관리 페이지에 접속하여, 원하는 양식의 계약서를 다운받아 수정 후 업로드하세요.

-
- - { - const file = e.target.files?.[0] - if (file) handleFileUpload(file) - }} - /> -

- Word 문서(.doc, .docx) 파일만 업로드 가능합니다. -

-
- - {uploadedFile && ( -
-
- - 업로드 완료 -
-

{uploadedFile.name}

-
- )} -
-
-
- -
- - -
-
- - {/* 3단계: PDF 미리보기 */} - @@ -870,7 +758,7 @@ export function ContractReviewRequestDialog({
-