From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../general-contract-approval-request-dialog.tsx | 1312 ++++++++++++++++++++ 1 file changed, 1312 insertions(+) create mode 100644 lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx (limited to 'lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx') diff --git a/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx new file mode 100644 index 00000000..f05fe9ef --- /dev/null +++ b/lib/general-contracts_old/detail/general-contract-approval-request-dialog.tsx @@ -0,0 +1,1312 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useSession } from 'next-auth/react' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Input } from '@/components/ui/input' +import { toast } from 'sonner' +import { + FileText, + Upload, + Eye, + Send, + CheckCircle, + Download, + AlertCircle +} from 'lucide-react' +import { ContractDocuments } from './general-contract-documents' +import { getActiveContractTemplates } from '@/lib/bidding/service' +import { type BasicContractTemplate } from '@/db/schema' +import { + getBasicInfo, + getContractItems, + getCommunicationChannel, + getLocation, + getFieldServiceRate, + getOffsetDetails, + getSubcontractChecklist, + uploadContractApprovalFile, + sendContractApprovalRequest +} from '../service' + +interface ContractApprovalRequestDialogProps { + contract: Record + open: boolean + onOpenChange: (open: boolean) => void +} + +interface ContractSummary { + basicInfo: Record + items: Record[] + communicationChannel: Record | null + location: Record | null + fieldServiceRate: Record | null + offsetDetails: Record | null + subcontractChecklist: Record | null +} + +export function ContractApprovalRequestDialog({ + contract, + open, + onOpenChange +}: ContractApprovalRequestDialogProps) { + const { data: session } = useSession() + const [currentStep, setCurrentStep] = useState(1) + const [contractSummary, setContractSummary] = useState(null) + const [uploadedFile, setUploadedFile] = useState(null) + const [generatedPdfUrl, setGeneratedPdfUrl] = useState(null) + const [generatedPdfBuffer, setGeneratedPdfBuffer] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [pdfViewerInstance, setPdfViewerInstance] = useState(null) + const [isPdfPreviewVisible, setIsPdfPreviewVisible] = useState(false) + + // 기본계약 관련 상태 + const [selectedBasicContracts, setSelectedBasicContracts] = useState>([]) + const [isLoadingBasicContracts, setIsLoadingBasicContracts] = useState(false) + + 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: any) => { + 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 사용 + } + } + + // 기본계약 생성 함수 (최종 전송 시점에 호출) + const generateBasicContractPdf = async ( + vendorId: number, + contractType: string, + templateName: string + ): Promise<{ buffer: number[], fileName: string }> => { + try { + // 1. 템플릿 데이터 준비 (서버 액션 호출) + const prepareResponse = await fetch("/api/contracts/prepare-template", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + templateName, + vendorId, + }), + }); + + if (!prepareResponse.ok) { + const errorText = await prepareResponse.text(); + throw new Error(`템플릿 준비 실패 (${prepareResponse.status}): ${errorText}`); + } + + const { template, templateData } = await prepareResponse.json(); + + // 2. 템플릿 파일 다운로드 + const templateResponse = await fetch("/api/contracts/get-template", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ templatePath: template.filePath }), + }); + + const templateBlob = await templateResponse.blob(); + const templateFile = new window.File([templateBlob], "template.docx", { + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + }); + + // 3. PDFTron WebViewer로 PDF 변환 + const { default: WebViewer } = await import("@pdftron/webviewer"); + + const tempDiv = document.createElement('div'); + tempDiv.style.display = 'none'; + document.body.appendChild(tempDiv); + + try { + const instance = await WebViewer( + { + path: "/pdftronWeb", + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + fullAPI: true, + enableOfficeEditing: true, + }, + tempDiv + ); + + const { Core } = instance; + const { createDocument } = Core; + + const templateDoc = await createDocument(templateFile, { + filename: templateFile.name, + extension: 'docx', + }); + + // 변수 치환 적용 + await templateDoc.applyTemplateValues(templateData); + await new Promise(resolve => setTimeout(resolve, 3000)); + + const fileData = await templateDoc.getFileData(); + const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' }); + + const fileName = `${contractType}_${contractSummary?.basicInfo?.vendorCode || vendorId}_${Date.now()}.pdf`; + + instance.UI.dispose(); + return { + buffer: Array.from(pdfBuffer), + fileName + }; + + } finally { + if (tempDiv.parentNode) { + document.body.removeChild(tempDiv); + } + } + + } catch (error) { + console.error(`기본계약 PDF 생성 실패 (${contractType}):`, error); + throw error; + } + }; + + // 기본계약 생성 및 선택 초기화 + const initializeBasicContracts = React.useCallback(async () => { + if (!contractSummary?.basicInfo) return; + + setIsLoadingBasicContracts(true); + try { + // 기본적으로 사용할 수 있는 계약서 타입들 + const availableContracts: Array<{ + type: string; + templateName: string; + checked: boolean; + }> = [ + { type: "NDA", templateName: "비밀", checked: false }, + { type: "General_GTC", templateName: "General GTC", checked: false }, + { type: "기술자료", templateName: "기술", checked: false } + ]; + + // 프로젝트 코드가 있으면 Project GTC도 추가 + if (contractSummary.basicInfo.projectCode) { + availableContracts.push({ + type: "Project_GTC", + templateName: contractSummary.basicInfo.projectCode as string, + checked: false + }); + } + + setSelectedBasicContracts(availableContracts); + } catch (error) { + console.error('기본계약 초기화 실패:', error); + toast.error('기본계약 초기화에 실패했습니다.'); + } finally { + setIsLoadingBasicContracts(false); + } + }, [contractSummary]); + + // 기본계약 선택 토글 + const toggleBasicContract = (type: string) => { + setSelectedBasicContracts(prev => + prev.map(contract => + contract.type === type + ? { ...contract, checked: !contract.checked } + : contract + ) + ); + }; + + + // 1단계: 계약 현황 수집 + const collectContractSummary = React.useCallback(async () => { + setIsLoading(true) + try { + // 각 컴포넌트에서 활성화된 데이터만 수집 + const summary: ContractSummary = { + basicInfo: {}, + items: [], + communicationChannel: null, + location: null, + fieldServiceRate: null, + offsetDetails: null, + subcontractChecklist: null + } + + // Basic Info 확인 (항상 활성화) + try { + const basicInfoData = await getBasicInfo(contractId) + if (basicInfoData && basicInfoData.success) { + summary.basicInfo = basicInfoData.data || {} + } + } catch { + console.log('Basic Info 데이터 없음') + } + + // 품목 정보 확인 + try { + const itemsData = await getContractItems(contractId) + if (itemsData && itemsData.length > 0) { + summary.items = itemsData + } + } catch { + console.log('품목 정보 데이터 없음') + } + + // 각 컴포넌트의 활성화 상태 및 데이터 확인 + try { + // Communication Channel 확인 + const commData = await getCommunicationChannel(contractId) + if (commData && commData.enabled) { + summary.communicationChannel = commData + } + } catch { + console.log('Communication Channel 데이터 없음') + } + + try { + // Location 확인 + const locationData = await getLocation(contractId) + if (locationData && locationData.enabled) { + summary.location = locationData + } + } catch { + console.log('Location 데이터 없음') + } + + try { + // Field Service Rate 확인 + const fieldServiceData = await getFieldServiceRate(contractId) + if (fieldServiceData && fieldServiceData.enabled) { + summary.fieldServiceRate = fieldServiceData + } + } catch { + console.log('Field Service Rate 데이터 없음') + } + + try { + // Offset Details 확인 + const offsetData = await getOffsetDetails(contractId) + if (offsetData && offsetData.enabled) { + summary.offsetDetails = offsetData + } + } catch { + console.log('Offset Details 데이터 없음') + } + + try { + // Subcontract Checklist 확인 + const subcontractData = await getSubcontractChecklist(contractId) + if (subcontractData && subcontractData.success && subcontractData.enabled) { + summary.subcontractChecklist = subcontractData.data + } + } catch { + console.log('Subcontract Checklist 데이터 없음') + } + + console.log('contractSummary 구조:', summary) + console.log('basicInfo 내용:', summary.basicInfo) + setContractSummary(summary) + } catch (error) { + console.error('Error collecting contract summary:', error) + toast.error('계약 정보를 수집하는 중 오류가 발생했습니다.') + } finally { + setIsLoading(false) + } + }, [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('로그인이 필요합니다.') + return + } + + setIsLoading(true) + try { + // 서버액션을 사용하여 파일 저장 (본 계약문서로 고정) + const result = await uploadContractApprovalFile( + contractId, + file, + userId + ) + + if (result.success) { + setUploadedFile(file) + toast.success('파일이 업로드되었습니다.') + } else { + throw new Error(result.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 + } + + setIsLoading(true) + try { + // 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 + + // 템플릿 문서 생성 및 변수 치환 + const templateDoc = await createDocument(uploadedFile, { + filename: uploadedFile.name, + extension: 'docx', + }) + + // LOI 템플릿용 변수 매핑 + const mappedTemplateData = mapContractSummaryToLOITemplate(contractSummary) + + console.log("🔄 변수 치환 시작:", mappedTemplateData) + await templateDoc.applyTemplateValues(mappedTemplateData as any) + console.log("✅ 변수 치환 완료") + + // PDF 변환 + const fileData = await templateDoc.getFileData() + const pdfBuffer = await (Core as any).officeToPDFBuffer(fileData, { extension: 'docx' }) + + console.log(`✅ PDF 변환 완료: ${uploadedFile.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가 생성되었습니다.') + + } finally { + // 임시 WebViewer 정리 + instance.UI.dispose() + document.body.removeChild(tempDiv) + } + + } catch (error) { + console.error('❌ PDF 생성 실패:', error) + toast.error('PDF 생성 중 오류가 발생했습니다.') + } finally { + setIsLoading(false) + } + } + + // PDF 미리보기 기능 + const openPdfPreview = async () => { + if (!generatedPdfBuffer) { + toast.error('생성된 PDF가 없습니다.') + return + } + + setIsLoading(true) + try { + // @ts-ignore + const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer) + + // 기존 인스턴스가 있다면 정리 + if (pdfViewerInstance) { + console.log("🔄 기존 WebViewer 인스턴스 정리") + try { + pdfViewerInstance.UI.dispose() + } catch (error) { + console.warn('기존 WebViewer 정리 중 오류:', error) + } + setPdfViewerInstance(null) + } + + // 미리보기용 컨테이너 확인 + let previewDiv = document.getElementById('pdf-preview-container') + if (!previewDiv) { + console.log("🔄 컨테이너 생성") + previewDiv = document.createElement('div') + previewDiv.id = 'pdf-preview-container' + previewDiv.className = 'w-full h-full' + previewDiv.style.width = '100%' + previewDiv.style.height = '100%' + + // 실제 컨테이너에 추가 + const actualContainer = document.querySelector('[data-pdf-container]') + if (actualContainer) { + actualContainer.appendChild(previewDiv) + } + } + + console.log("🔄 WebViewer 인스턴스 생성 시작") + + // WebViewer 인스턴스 생성 (문서 없이) + const instance = await Promise.race([ + WebViewer( + { + path: "/pdftronWeb", + licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + fullAPI: true, + }, + previewDiv + ), + new Promise((_, reject) => + setTimeout(() => reject(new Error('WebViewer 초기화 타임아웃')), 30000) + ) + ]) + + console.log("🔄 WebViewer 인스턴스 생성 완료") + setPdfViewerInstance(instance) + + // PDF 버퍼를 Blob으로 변환 + const pdfBlob = new Blob([generatedPdfBuffer], { type: 'application/pdf' }) + const pdfUrl = URL.createObjectURL(pdfBlob) + console.log("🔄 PDF Blob URL 생성:", pdfUrl) + + // 문서 로드 + console.log("🔄 문서 로드 시작") + const { documentViewer } = instance.Core + + // 문서 로드 이벤트 대기 + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('문서 로드 타임아웃')) + }, 20000) + + const onDocumentLoaded = () => { + clearTimeout(timeout) + documentViewer.removeEventListener('documentLoaded', onDocumentLoaded) + documentViewer.removeEventListener('documentError', onDocumentError) + console.log("🔄 문서 로드 완료") + resolve(true) + } + + const onDocumentError = (error: any) => { + clearTimeout(timeout) + documentViewer.removeEventListener('documentLoaded', onDocumentLoaded) + documentViewer.removeEventListener('documentError', onDocumentError) + console.error('문서 로드 오류:', error) + reject(error) + } + + documentViewer.addEventListener('documentLoaded', onDocumentLoaded) + documentViewer.addEventListener('documentError', onDocumentError) + + // 문서 로드 시작 + documentViewer.loadDocument(pdfUrl) + }) + + setIsPdfPreviewVisible(true) + toast.success('PDF 미리보기가 준비되었습니다.') + + } catch (error) { + console.error('PDF 미리보기 실패:', error) + toast.error(`PDF 미리보기 중 오류가 발생했습니다: ${error.message}`) + } finally { + setIsLoading(false) + } + } + + // PDF 다운로드 기능 + const downloadPdf = () => { + if (!generatedPdfBuffer) { + toast.error('다운로드할 PDF가 없습니다.') + return + } + + const pdfBlob = new Blob([generatedPdfBuffer], { type: 'application/pdf' }) + const pdfUrl = URL.createObjectURL(pdfBlob) + + const link = document.createElement('a') + link.href = pdfUrl + link.download = `contract_${contractId}_${Date.now()}.pdf` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + URL.revokeObjectURL(pdfUrl) + toast.success('PDF가 다운로드되었습니다.') + } + + // PDF 미리보기 닫기 + const closePdfPreview = () => { + console.log("🔄 PDF 미리보기 닫기 시작") + if (pdfViewerInstance) { + try { + console.log("🔄 WebViewer 인스턴스 정리") + pdfViewerInstance.UI.dispose() + } catch (error) { + console.warn('WebViewer 정리 중 오류:', error) + } + setPdfViewerInstance(null) + } + + // 컨테이너 정리 + const previewDiv = document.getElementById('pdf-preview-container') + if (previewDiv) { + try { + previewDiv.innerHTML = '' + } catch (error) { + console.warn('컨테이너 정리 중 오류:', error) + } + } + + setIsPdfPreviewVisible(false) + console.log("🔄 PDF 미리보기 닫기 완료") + } + + // 최종 전송 + const handleFinalSubmit = async () => { + if (!generatedPdfUrl || !contractSummary || !generatedPdfBuffer) { + toast.error('생성된 PDF가 필요합니다.') + return + } + + if (!userId) { + toast.error('로그인이 필요합니다.') + return + } + + setIsLoading(true) + try { + // 기본계약서 생성 (최종 전송 시점에) + let generatedBasicContractPdfs: Array<{ key: string; buffer: number[]; fileName: string }> = []; + + const contractsToGenerate = selectedBasicContracts.filter(c => c.checked); + if (contractsToGenerate.length > 0) { + // vendorId 조회 + let vendorId: number | undefined; + try { + const basicInfoData = await getBasicInfo(contractId); + if (basicInfoData && basicInfoData.success && basicInfoData.data) { + vendorId = basicInfoData.data.vendorId; + } + } catch (error) { + console.error('vendorId 조회 실패:', error); + } + + if (vendorId) { + toast.info('기본계약서를 생성하는 중입니다...'); + + for (const contract of contractsToGenerate) { + try { + const pdf = await generateBasicContractPdf(vendorId, contract.type, contract.templateName); + generatedBasicContractPdfs.push({ + key: `${vendorId}_${contract.type}_${contract.templateName}`, + ...pdf + }); + } catch (error) { + console.error(`${contract.type} 계약서 생성 실패:`, error); + // 개별 실패는 전체를 중단하지 않음 + } + } + + if (generatedBasicContractPdfs.length > 0) { + toast.success(`${generatedBasicContractPdfs.length}개의 기본계약서가 생성되었습니다.`); + } + } + } + + // 서버액션을 사용하여 계약승인요청 전송 + const result = await sendContractApprovalRequest( + contractSummary, + generatedPdfBuffer, + 'contractDocument', + userId, + generatedBasicContractPdfs + ) + + if (result.success) { + toast.success('계약승인요청이 전송되었습니다.') + onOpenChange(false) + } else { + // 서버에서 이미 처리된 에러 메시지 표시 + toast.error(result.error || '계약승인요청 전송 실패') + return + } + } catch (error: any) { + console.error('Error submitting approval request:', error) + + // 데이터베이스 중복 키 오류 처리 + if (error.message && error.message.includes('duplicate key value violates unique constraint')) { + toast.error('이미 존재하는 계약번호입니다. 다른 계약번호를 사용해주세요.') + return + } + + // 다른 오류에 대한 일반적인 처리 + toast.error('계약승인요청 전송 중 오류가 발생했습니다.') + } finally { + setIsLoading(false) + } + } + + // 다이얼로그가 열릴 때 1단계 데이터 수집 + useEffect(() => { + if (open && currentStep === 1) { + collectContractSummary() + } + }, [open, currentStep, collectContractSummary]) + + // 계약 요약이 준비되면 기본계약 초기화 + useEffect(() => { + if (contractSummary && currentStep === 2) { + const loadBasicContracts = async () => { + await initializeBasicContracts() + } + loadBasicContracts() + } + }, [contractSummary, currentStep, initializeBasicContracts]) + + // 다이얼로그가 닫힐 때 PDF 뷰어 정리 + useEffect(() => { + if (!open) { + closePdfPreview() + } + }, [open]) + + + return ( + + + + + + 계약승인요청 + + + + + + + 1. 계약 현황 정리 + + + 2. 기본계약 체크 + + + 3. 문서 업로드 + + + 4. PDF 미리보기 + + + + {/* 1단계: 계약 현황 정리 */} + + + + + + 작성된 계약 현황 + + + + {isLoading ? ( +
+
+

계약 정보를 수집하는 중...

+
+ ) : ( +
+ {/* 기본 정보 (필수) */} +
+
+ + + 필수 +
+
+
+ 계약번호: {String(contractSummary?.basicInfo?.contractNumber || '')} +
+
+ 계약명: {String(contractSummary?.basicInfo?.contractName || '')} +
+
+ 벤더: {String(contractSummary?.basicInfo?.vendorName || '')} +
+
+ 프로젝트: {String(contractSummary?.basicInfo?.projectName || '')} +
+
+ 계약유형: {String(contractSummary?.basicInfo?.contractType || '')} +
+
+ 계약상태: {String(contractSummary?.basicInfo?.contractStatus || '')} +
+
+ 계약금액: {String(contractSummary?.basicInfo?.contractAmount || '')} {String(contractSummary?.basicInfo?.currency || '')} +
+
+ 계약기간: {String(contractSummary?.basicInfo?.startDate || '')} ~ {String(contractSummary?.basicInfo?.endDate || '')} +
+
+ 사양서 유형: {String(contractSummary?.basicInfo?.specificationType || '')} +
+
+ 단가 유형: {String(contractSummary?.basicInfo?.unitPriceType || '')} +
+
+ 연결 PO번호: {String(contractSummary?.basicInfo?.linkedPoNumber || '')} +
+
+ 연결 입찰번호: {String(contractSummary?.basicInfo?.linkedBidNumber || '')} +
+
+
+ + {/* 지급/인도 조건 */} +
+
+ + + 필수 +
+
+
+ 지급조건: {String(contractSummary?.basicInfo?.paymentTerm || '')} +
+
+ 세금 유형: {String(contractSummary?.basicInfo?.taxType || '')} +
+
+ 인도조건: {String(contractSummary?.basicInfo?.deliveryTerm || '')} +
+
+ 인도유형: {String(contractSummary?.basicInfo?.deliveryType || '')} +
+
+ 선적지: {String(contractSummary?.basicInfo?.shippingLocation || '')} +
+
+ 하역지: {String(contractSummary?.basicInfo?.dischargeLocation || '')} +
+
+ 계약납기: {String(contractSummary?.basicInfo?.contractDeliveryDate || '')} +
+
+ 위약금: {contractSummary?.basicInfo?.liquidatedDamages ? '적용' : '미적용'} +
+
+
+ + {/* 추가 조건 */} +
+
+ + + 필수 +
+
+
+ 연동제 정보: {String(contractSummary?.basicInfo?.interlockingSystem || '')} +
+
+ 계약성립조건: + {contractSummary?.basicInfo?.contractEstablishmentConditions && + Object.entries(contractSummary.basicInfo.contractEstablishmentConditions) + .filter(([, value]) => value === true) + .map(([key]) => key) + .join(', ') || '없음'} +
+
+ 계약해지조건: + {contractSummary?.basicInfo?.contractTerminationConditions && + Object.entries(contractSummary.basicInfo.contractTerminationConditions) + .filter(([, value]) => value === true) + .map(([key]) => key) + .join(', ') || '없음'} +
+
+
+ + {/* 품목 정보 */} +
+
+ 0} + disabled + /> + + 선택 +
+ {contractSummary?.items && contractSummary.items.length > 0 ? ( +
+

+ 총 {contractSummary.items.length}개 품목이 입력되어 있습니다. +

+
+ {contractSummary.items.slice(0, 3).map((item: Record, index: number) => ( +
+
{item.itemInfo || item.description || `품목 ${index + 1}`}
+
+ 수량: {item.quantity || 0} | 단가: {item.contractUnitPrice || item.unitPrice || 0} +
+
+ ))} + {contractSummary.items.length > 3 && ( +
+ ... 외 {contractSummary.items.length - 3}개 품목 +
+ )} +
+
+ ) : ( +

+ 품목 정보가 입력되지 않았습니다. +

+ )} +
+ + {/* 커뮤니케이션 채널 */} +
+
+ + + 선택 +
+

+ {contractSummary?.communicationChannel + ? '정보가 입력되어 있습니다.' + : '정보가 입력되지 않았습니다.'} +

+
+ + {/* 위치 정보 */} +
+
+ + + 선택 +
+

+ {contractSummary?.location + ? '정보가 입력되어 있습니다.' + : '정보가 입력되지 않았습니다.'} +

+
+ + {/* 현장 서비스 요율 */} +
+
+ + + 선택 +
+

+ {contractSummary?.fieldServiceRate + ? '정보가 입력되어 있습니다.' + : '정보가 입력되지 않았습니다.'} +

+
+ + {/* 오프셋 세부사항 */} +
+
+ + + 선택 +
+

+ {contractSummary?.offsetDetails + ? '정보가 입력되어 있습니다.' + : '정보가 입력되지 않았습니다.'} +

+
+ + {/* 하도급 체크리스트 */} +
+
+ + + 선택 +
+

+ {contractSummary?.subcontractChecklist + ? '정보가 입력되어 있습니다.' + : '정보가 입력되지 않았습니다.'} +

+
+
+ )} +
+
+ +
+ +
+
+ + {/* 2단계: 기본계약 체크 */} + + + + + + 기본계약서 선택 + +

+ 벤더에게 발송할 기본계약서를 선택해주세요. (템플릿이 있는 계약서만 선택 가능합니다.) +

+
+ + {isLoadingBasicContracts ? ( +
+
+

기본계약 템플릿을 불러오는 중...

+
+ ) : ( +
+ {selectedBasicContracts.length > 0 ? ( +
+
+

필요한 기본계약서

+ + {selectedBasicContracts.filter(c => c.checked).length}개 선택됨 + +
+ +
+ {selectedBasicContracts.map((contract) => ( +
+
+ toggleBasicContract(contract.type)} + /> +
+ +

+ 템플릿: {contract.templateName} +

+
+
+ + {contract.checked ? "선택됨" : "미선택"} + +
+ ))} +
+ +
+ ) : ( +
+ +

기본계약서 목록을 불러올 수 없습니다.

+

잠시 후 다시 시도해주세요.

+
+ )} + +
+ )} +
+
+ +
+ + +
+
+ + {/* 3단계: 문서 업로드 */} + + + + + + 계약서 업로드 + + + +
+

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

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

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

+
+ + {/* ContractDocuments 컴포넌트 사용 */} + {/*
+ + +
*/} + + {uploadedFile && ( +
+
+ + 업로드 완료 +
+

{uploadedFile.name}

+
+ )} +
+
+
+ +
+ + +
+
+ + {/* 4단계: PDF 미리보기 */} + + + + + + PDF 미리보기 + + + + {!generatedPdfUrl ? ( +
+ +
+ ) : ( +
+
+
+ + PDF 생성 완료 +
+
+ +
+
+

생성된 PDF

+
+ + +
+
+ + {/* PDF 미리보기 영역 */} +
+ {isPdfPreviewVisible ? ( + <> +
+ +
+
+ + ) : ( +
+
+ +

미리보기 버튼을 클릭하여 PDF를 확인하세요

+
+
+ )} +
+
+
+ )} + + + +
+ + +
+ + + +
+ )} \ No newline at end of file -- cgit v1.2.3