'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를 확인하세요

)}
)}
)}