'use client' import * as React from 'react' import { Button } from '@/components/ui/button' import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' import { Progress } from '@/components/ui/progress' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { cn } from '@/lib/utils' import { Mail, Building2, Calendar, FileText, CheckCircle, Info, RefreshCw, Plus, X } from 'lucide-react' import { sendBiddingBasicContracts, getSelectedVendorsForBidding, getExistingBasicContractsForBidding } from '../../pre-quote/service' import { getActiveContractTemplates } from '../../service' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' interface VendorContractRequirement { vendorId: number vendorName: string vendorCode?: string vendorCountry?: string contactPerson?: string contactEmail?: string ndaYn?: boolean generalGtcYn?: boolean projectGtcYn?: boolean agreementYn?: boolean biddingCompanyId: number biddingId: number } interface BasicContractTemplate { id: number templateName: string revision: number status: string filePath: string | null validityPeriod: number | null legalReviewRequired: boolean createdAt: Date | null } interface SelectedContract { templateId: number templateName: string contractType: string checked: boolean } interface BiddingInvitationDialogProps { open: boolean onOpenChange: (open: boolean) => void vendors: VendorContractRequirement[] biddingId: number biddingTitle: string projectName?: string onSend: (data: { vendors: Array<{ vendorId: number vendorName: string vendorCode?: string vendorCountry?: string selectedMainEmail: string additionalEmails: string[] contractRequirements: { ndaYn: boolean generalGtcYn: boolean projectGtcYn: boolean agreementYn: boolean } biddingCompanyId: number biddingId: number hasExistingContracts?: boolean }> generatedPdfs: Array<{ key: string buffer: number[] fileName: string }> message?: string }) => Promise } export function BiddingInvitationDialog({ open, onOpenChange, vendors, biddingId, biddingTitle, projectName, onSend, }: BiddingInvitationDialogProps) { const { toast } = useToast() const [isPending, startTransition] = useTransition() // 기본계약 관련 상태 const [existingContracts, setExistingContracts] = React.useState([]) const [isGeneratingPdfs, setIsGeneratingPdfs] = React.useState(false) const [pdfGenerationProgress, setPdfGenerationProgress] = React.useState(0) const [currentGeneratingContract, setCurrentGeneratingContract] = React.useState('') // 기본계약서 템플릿 관련 상태 const [availableTemplates, setAvailableTemplates] = React.useState([]) const [selectedContracts, setSelectedContracts] = React.useState([]) const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false) const [additionalMessage, setAdditionalMessage] = React.useState('') // 선택된 업체들 (사전견적에서 선정된 업체들만) const selectedVendors = React.useMemo(() => vendors.filter(vendor => vendor.ndaYn || vendor.generalGtcYn || vendor.projectGtcYn || vendor.agreementYn), [vendors] ) // 기존 계약이 있는 업체들과 없는 업체들 분리 const vendorsWithExistingContracts = React.useMemo(() => selectedVendors.filter(vendor => existingContracts.some((ec: any) => ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId ) ), [selectedVendors, existingContracts] ) const vendorsWithoutExistingContracts = React.useMemo(() => selectedVendors.filter(vendor => !existingContracts.some((ec: any) => ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId ) ), [selectedVendors, existingContracts] ) // 다이얼로그가 열릴 때 기존 계약 조회 및 템플릿 로드 React.useEffect(() => { if (open) { const fetchInitialData = async () => { setIsLoadingTemplates(true); try { const [contractsResult, templatesData] = await Promise.all([ getSelectedVendorsForBidding(biddingId), getActiveContractTemplates(), ]); // 기존 계약 조회 (사전견적에서 보낸 기본계약 확인) - 서버 액션 사용 const existingContracts = await getExistingBasicContractsForBidding(biddingId); setExistingContracts(existingContracts.success ? existingContracts.contracts || [] : []); // 템플릿 로드 (4개 타입만 필터링) // 4개 템플릿 타입만 필터링: 비밀, General, Project, 기술자료 const allowedTemplateNames = ['비밀', 'General GTC', '기술', '기술자료']; const rawTemplates = templatesData.templates || []; const filteredTemplates = rawTemplates.filter((template: any) => allowedTemplateNames.some(allowedName => template.templateName.includes(allowedName) || allowedName.includes(template.templateName) ) ); setAvailableTemplates(filteredTemplates as any); const initialSelected = filteredTemplates.map((template: any) => ({ templateId: template.id, templateName: template.templateName, contractType: template.templateName, checked: false })); setSelectedContracts(initialSelected); } catch (error) { console.error('초기 데이터 로드 실패:', error); toast({ title: '오류', description: '기본 정보를 불러오는 데 실패했습니다.', variant: 'destructive', }); setAvailableTemplates([]); setSelectedContracts([]); } finally { setIsLoadingTemplates(false); } } fetchInitialData(); } }, [open, biddingId, toast]); const handleOpenChange = (open: boolean) => { onOpenChange(open) if (!open) { setSelectedContracts([]) setAdditionalMessage('') setIsGeneratingPdfs(false) setPdfGenerationProgress(0) setCurrentGeneratingContract('') } } // 기본계약서 선택 토글 const toggleContractSelection = (templateId: number) => { setSelectedContracts(prev => prev.map(contract => contract.templateId === templateId ? { ...contract, checked: !contract.checked } : contract ) ) } // 모든 기본계약서 선택/해제 const toggleAllContractSelection = (checked: boolean | 'indeterminate') => { setSelectedContracts(prev => prev.map(contract => ({ ...contract, checked: !!checked })) ) } // PDF 생성 유틸리티 함수 const generateBasicContractPdf = async ( template: BasicContractTemplate, vendorId: number ): Promise<{ buffer: number[]; fileName: string }> => { try { // 1. 템플릿 데이터 준비 (서버 API 호출) const prepareResponse = await fetch("/api/contracts/prepare-template", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ templateName: template.templateName, vendorId, }), }); if (!prepareResponse.ok) { throw new Error("템플릿 준비 실패"); } const { template: preparedTemplate, templateData } = await prepareResponse.json(); // 2. 템플릿 파일 다운로드 const templateResponse = await fetch("/api/contracts/get-template", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ templatePath: preparedTemplate.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, }, tempDiv ); const { Core } = instance; const { createDocument } = Core; const templateDoc = await createDocument(templateFile, { filename: templateFile.name, extension: 'docx', }); // 변수 치환 적용 await templateDoc.applyTemplateValues(templateData); // PDF 변환 const fileData = await templateDoc.getFileData(); const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' }); const fileName = `${template.templateName}_${Date.now()}.pdf`; return { buffer: Array.from(pdfBuffer), // Uint8Array를 일반 배열로 변환 fileName }; } finally { if (tempDiv.parentNode) { document.body.removeChild(tempDiv); } } } catch (error) { console.error(`기본계약 PDF 생성 실패 (${template.templateName}):`, error); throw error; } }; const handleSendInvitation = () => { const selectedContractTemplates = selectedContracts.filter(c => c.checked); startTransition(async () => { try { let generatedPdfs: Array<{ key: string buffer: number[] fileName: string }> = [] const generatedPdfsMap = new Map() // 선택된 템플릿이 있는 경우에만 PDF 생성 if (selectedContractTemplates.length > 0) { setIsGeneratingPdfs(true) setPdfGenerationProgress(0) let generatedCount = 0; for (const vendor of selectedVendors) { // 사전견적에서 이미 기본계약을 보낸 벤더인지 확인 const hasExistingContract = existingContracts.some((ec: any) => ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId ); if (hasExistingContract) { console.log(`벤더 ${vendor.vendorName}는 사전견적에서 이미 기본계약을 받았으므로 건너뜁니다.`); generatedCount++; setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100); continue; } for (const contract of selectedContractTemplates) { setCurrentGeneratingContract(`${vendor.vendorName} - ${contract.templateName}`); const templateDetails = availableTemplates.find(t => t.id === contract.templateId); if (templateDetails) { const pdfData = await generateBasicContractPdf(templateDetails, vendor.vendorId); // sendBiddingBasicContracts와 동일한 키 형식 사용 let contractType = ''; if (contract.templateName.includes('비밀')) { contractType = 'NDA'; } else if (contract.templateName.includes('General GTC')) { contractType = 'General_GTC'; } else if (contract.templateName.includes('기술') && !contract.templateName.includes('기술자료')) { contractType = 'Project_GTC'; } else if (contract.templateName.includes('기술자료')) { contractType = '기술자료'; } const key = `${vendor.vendorId}_${contractType}_${contract.templateName}`; generatedPdfsMap.set(key, pdfData); } } generatedCount++; setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100); } setIsGeneratingPdfs(false); const pdfsArray = Array.from(generatedPdfsMap.entries()).map(([key, data]) => ({ key, buffer: data.buffer, fileName: data.fileName, })); generatedPdfs = pdfsArray; } const vendorData = selectedVendors.map(vendor => { const hasExistingContract = existingContracts.some((ec: any) => ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId ); return { vendorId: vendor.vendorId, vendorName: vendor.vendorName, vendorCode: vendor.vendorCode, vendorCountry: vendor.vendorCountry, selectedMainEmail: vendor.contactEmail || '', additionalEmails: [], contractRequirements: { ndaYn: vendor.ndaYn || false, generalGtcYn: vendor.generalGtcYn || false, projectGtcYn: vendor.projectGtcYn || false, agreementYn: vendor.agreementYn || false }, biddingCompanyId: vendor.biddingCompanyId, biddingId: vendor.biddingId, hasExistingContracts: hasExistingContract }; }); await onSend({ vendors: vendorData, generatedPdfs: generatedPdfs, message: additionalMessage }); } catch (error) { console.error('본입찰 초대 실패:', error); toast({ title: '오류', description: '본입찰 초대 중 오류가 발생했습니다.', variant: 'destructive', }); setIsGeneratingPdfs(false); } }) } const selectedContractCount = selectedContracts.filter(c => c.checked).length; return ( 본입찰 초대 {biddingTitle} - 선정된 {selectedVendors.length}개 업체에 본입찰 초대와 기본계약서를 발송합니다.
{/* 기존 계약 정보 */} {vendorsWithExistingContracts.length > 0 && ( 기존 계약 정보 사전견적에서 이미 기본계약을 받은 업체가 있습니다. 해당 업체들은 계약서 재생성을 건너뜁니다. (본입찰 초대는 정상 진행됩니다) )} {/* 대상 업체 정보 */} 초대 대상 업체 ({selectedVendors.length}개) {selectedVendors.length === 0 ? (
초대 가능한 업체가 없습니다.
) : (
{/* 계약서가 생성될 업체들 */} {vendorsWithoutExistingContracts.length > 0 && (

계약서 생성 대상 ({vendorsWithoutExistingContracts.length}개)

{vendorsWithoutExistingContracts.map((vendor) => (
{vendor.vendorName} {vendor.vendorCode}
))}
)} {/* 기존 계약이 있는 업체들 */} {vendorsWithExistingContracts.length > 0 && (

기존 계약 존재 (계약서 재생성 건너뜀) ({vendorsWithExistingContracts.length}개)

{vendorsWithExistingContracts.map((vendor) => (
{vendor.vendorName} {vendor.vendorCode} 계약 존재 (재생성 건너뜀) 본입찰 초대
))}
)}
)}
{/* 기본계약서 선택 */} 기본계약 선택 (선택사항) {/* 템플릿 로딩 */} {isLoadingTemplates ? (

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

) : (
{availableTemplates.length === 0 ? (

사용 가능한 기본계약서 템플릿이 없습니다.

) : ( <>
0 && selectedContracts.every(c => c.checked)} onCheckedChange={toggleAllContractSelection} />
{selectedContractCount}개 선택됨
{selectedContracts.map((contract) => (
toggleContractSelection(contract.templateId)} >
toggleContractSelection(contract.templateId)} />

{contract.contractType}

))}
)} {/* 선택된 템플릿 요약 */} {selectedContractCount > 0 && (
선택된 기본계약서 ({selectedContractCount}개)
    {selectedContracts.filter(c => c.checked).map((contract) => (
  • {contract.templateName}
  • ))}
)}
)}
{/* 추가 메시지 */}