From 3c9a95332298450c7e0f75bfb08944439e1a3739 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<string, unknown>
@@ -38,6 +38,7 @@ interface ContractSummary {
   basicInfo: Record<string, unknown>
   items: Record<string, unknown>[]
   subcontractChecklist: Record<string, unknown> | null
+  storageInfo?: Record<string, unknown>[]
 }
 
 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<string, unknown>)
-      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<Uint8Array> }).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({
         </DialogHeader>
 
         <Tabs value={currentStep.toString()} className="w-full">
-          <TabsList className="grid w-full grid-cols-3">
+          <TabsList className="grid w-full grid-cols-2">
             <TabsTrigger value="1" disabled={currentStep < 1}>
               1. 미리보기
             </TabsTrigger>
             <TabsTrigger value="2" disabled={currentStep < 2}>
-              2. 템플릿 업로드
-            </TabsTrigger>
-            <TabsTrigger value="3" disabled={currentStep < 3}>
-              3. PDF 미리보기
+              2. PDF 미리보기
             </TabsTrigger>
           </TabsList>
 
@@ -645,7 +587,7 @@ export function ContractReviewRequestDialog({
                         <div>
                           <span className="font-medium">계약성립조건:</span> 
                           {contractSummary?.basicInfo?.contractEstablishmentConditions && 
-                            Object.entries(contractSummary.basicInfo.contractEstablishmentConditions)
+                            Object.entries(contractSummary.basicInfo.contractEstablishmentConditions as Record<string, unknown>)
                               .filter(([, value]) => value === true)
                               .map(([key]) => key)
                               .join(', ') || '없음'}
@@ -653,7 +595,7 @@ export function ContractReviewRequestDialog({
                         <div>
                           <span className="font-medium">계약해지조건:</span>
                           {contractSummary?.basicInfo?.contractTerminationConditions && 
-                            Object.entries(contractSummary.basicInfo.contractTerminationConditions)
+                            Object.entries(contractSummary.basicInfo.contractTerminationConditions as Record<string, unknown>)
                               .filter(([, value]) => value === true)
                               .map(([key]) => key)
                               .join(', ') || '없음'}
@@ -680,9 +622,9 @@ export function ContractReviewRequestDialog({
                           <div className="max-h-32 overflow-y-auto">
                             {contractSummary.items.slice(0, 3).map((item: Record<string, unknown>, index: number) => (
                               <div key={index} className="text-xs bg-gray-50 p-2 rounded">
-                                <div className="font-medium">{item.itemInfo || item.description || `품목 ${index + 1}`}</div>
+                                <div className="font-medium">{String(item.itemInfo || item.description || `품목 ${index + 1}`)}</div>
                                 <div className="text-muted-foreground">
-                                  수량: {item.quantity || 0} | 단가: {item.contractUnitPrice || item.unitPrice || 0}
+                                  수량: {String(item.quantity || 0)} | 단가: {String(item.contractUnitPrice || item.unitPrice || 0)}
                                 </div>
                               </div>
                             ))}
@@ -734,62 +676,8 @@ export function ContractReviewRequestDialog({
             </div>
           </TabsContent>
 
-          {/* 2단계: 문서 업로드 */}
+          {/* 2단계: PDF 미리보기 (자동 생성) */}
           <TabsContent value="2" className="space-y-4">
-            <Card>
-              <CardHeader>
-                <CardTitle className="flex items-center gap-2">
-                  <Upload className="h-5 w-5 text-blue-600" />
-                  계약서 템플릿 업로드
-                </CardTitle>
-              </CardHeader>
-              <CardContent className="space-y-4">
-                <div className="space-y-4">
-                <p className="text-lg text-muted-foreground">일반계약 표준문서 관리 페이지에 접속하여, 원하는 양식의 계약서를 다운받아 수정 후 업로드하세요.</p>
-                  <div>
-                    <Label htmlFor="file-upload-review">파일 업로드</Label>
-                    <Input
-                      id="file-upload-review"
-                      type="file"
-                      accept=".doc,.docx"
-                      onChange={(e) => {
-                        const file = e.target.files?.[0]
-                        if (file) handleFileUpload(file)
-                      }}
-                    />
-                    <p className="text-sm text-muted-foreground mt-1">
-                      Word 문서(.doc, .docx) 파일만 업로드 가능합니다.
-                    </p>
-                  </div>
-
-                  {uploadedFile && (
-                    <div className="border rounded-lg p-4 bg-green-50">
-                      <div className="flex items-center gap-2">
-                        <CheckCircle className="h-4 w-4 text-green-600" />
-                        <span className="font-medium text-green-900">업로드 완료</span>
-                      </div>
-                      <p className="text-sm text-green-800 mt-1">{uploadedFile.name}</p>
-                    </div>
-                  )}
-                </div>
-              </CardContent>
-            </Card>
-
-            <div className="flex justify-between">
-              <Button variant="outline" onClick={() => setCurrentStep(1)}>
-                이전 단계
-              </Button>
-              <Button
-                onClick={() => setCurrentStep(3)}
-                disabled={!uploadedFile}
-              >
-                다음 단계
-              </Button>
-            </div>
-          </TabsContent>
-
-          {/* 3단계: PDF 미리보기 */}
-          <TabsContent value="3" className="space-y-4">
             <Card>
               <CardHeader>
                 <CardTitle className="flex items-center gap-2">
@@ -870,7 +758,7 @@ export function ContractReviewRequestDialog({
             </Card>
 
             <div className="flex justify-between">
-              <Button variant="outline" onClick={() => setCurrentStep(2)}>
+              <Button variant="outline" onClick={() => setCurrentStep(1)}>
                 이전 단계
               </Button>
               <Button
@@ -888,4 +776,3 @@ export function ContractReviewRequestDialog({
     </Dialog>
   )
 }
-
-- 
cgit v1.2.3

