'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 { BiddingCompany } from './bidding-pre-quote-vendor-columns' import { sendPreQuoteInvitations, sendBiddingBasicContracts, getExistingBasicContractsForBidding } from '../service' import { getActiveContractTemplates } from '../../service' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' import { Mail, Building2, Calendar, FileText, CheckCircle, Info, RefreshCw } from 'lucide-react' import { Progress } from '@/components/ui/progress' import { Separator } from '@/components/ui/separator' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { cn } from '@/lib/utils' interface BiddingPreQuoteInvitationDialogProps { open: boolean onOpenChange: (open: boolean) => void companies: BiddingCompany[] biddingId: number biddingTitle: string projectName?: string onSuccess: () => void } 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 // templateName을 contractType으로 사용 checked: boolean } // 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; } }; export function BiddingPreQuoteInvitationDialog({ open, onOpenChange, companies, biddingId, biddingTitle, projectName, onSuccess }: BiddingPreQuoteInvitationDialogProps) { const { toast } = useToast() const [isPending, startTransition] = useTransition() const [selectedCompanyIds, setSelectedCompanyIds] = React.useState([]) const [preQuoteDeadline, setPreQuoteDeadline] = React.useState('') const [additionalMessage, setAdditionalMessage] = React.useState('') // 기본계약 관련 상태 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) // 초대 가능한 업체들 (pending 상태인 업체들) const invitableCompanies = React.useMemo(() => companies.filter(company => company.invitationStatus === 'pending' && company.companyName ), [companies]) // 다이얼로그가 열릴 때 기존 계약 조회 및 템플릿 로드 React.useEffect(() => { if (open) { const fetchInitialData = async () => { setIsLoadingTemplates(true); try { const [contractsResult, templatesData] = await Promise.all([ getExistingBasicContractsForBidding(biddingId), getActiveContractTemplates() ]); // 기존 계약 조회 - 서버 액션 사용 const existingContractsResult = await getExistingBasicContractsForBidding(biddingId); setExistingContracts(existingContractsResult.success ? existingContractsResult.contracts || [] : []); // 템플릿 로드 (4개 타입만 필터링) // 4개 템플릿 타입만 필터링: 비밀, General, Project, 기술자료 const allowedTemplateNames = ['비밀', 'General GTC', '기술', '기술자료']; const filteredTemplates = (templatesData.templates || []).filter((template: any) => allowedTemplateNames.some(allowedName => template.templateName.includes(allowedName) || allowedName.includes(template.templateName) ) ); setAvailableTemplates(filteredTemplates as BasicContractTemplate[]); 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', }); setExistingContracts([]); setAvailableTemplates([]); setSelectedContracts([]); } finally { setIsLoadingTemplates(false); } } fetchInitialData(); } }, [open, biddingId, toast]); const handleSelectAll = (checked: boolean | 'indeterminate') => { if (checked) { // 기존 계약이 없는 업체만 선택 const availableCompanies = invitableCompanies.filter(company => !existingContracts.some(ec => ec.vendorId === company.companyId) ) setSelectedCompanyIds(availableCompanies.map(company => company.id)) } else { setSelectedCompanyIds([]) } } const handleSelectCompany = (companyId: number, checked: boolean) => { const company = invitableCompanies.find(c => c.id === companyId) const hasExistingContract = company ? existingContracts.some(ec => ec.vendorId === company.companyId) : false if (hasExistingContract) { toast({ title: '선택 불가', description: '이미 기본계약서를 받은 업체는 다시 선택할 수 없습니다.', variant: 'default', }) return } if (checked) { setSelectedCompanyIds(prev => [...prev, companyId]) } else { setSelectedCompanyIds(prev => prev.filter(id => id !== companyId)) } } // 기본계약서 선택 토글 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 })) ) } const handleSendInvitations = () => { if (selectedCompanyIds.length === 0) { toast({ title: '알림', description: '초대를 발송할 업체를 선택해주세요.', variant: 'default', }) return } const selectedContractTemplates = selectedContracts.filter(c => c.checked); const companiesForContracts = invitableCompanies.filter(company => selectedCompanyIds.includes(company.id)); const vendorsToGenerateContracts = companiesForContracts.filter(company => !existingContracts.some(ec => ec.vendorId === company.companyId && ec.biddingCompanyId === company.id ) ); startTransition(async () => { try { // 1. 사전견적 초대 발송 const invitationResponse = await sendPreQuoteInvitations( selectedCompanyIds, preQuoteDeadline || undefined ) if (!invitationResponse.success) { toast({ title: '초대 발송 실패', description: invitationResponse.error, variant: 'destructive', }) return } // 2. 기본계약 발송 (선택된 템플릿과 업체가 있는 경우) let contractResponse: Awaited> | null = null if (selectedContractTemplates.length > 0 && selectedCompanyIds.length > 0) { setIsGeneratingPdfs(true) setPdfGenerationProgress(0) const generatedPdfsMap = new Map() let generatedCount = 0; for (const vendor of vendorsToGenerateContracts) { for (const contract of selectedContractTemplates) { setCurrentGeneratingContract(`${vendor.companyName} - ${contract.templateName}`); const templateDetails = availableTemplates.find(t => t.id === contract.templateId); if (templateDetails) { const pdfData = await generateBasicContractPdf(templateDetails, vendor.companyId); // 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.companyId}_${contractType}_${contract.templateName}`; generatedPdfsMap.set(key, pdfData); } } generatedCount++; setPdfGenerationProgress((generatedCount / vendorsToGenerateContracts.length) * 100); } setIsGeneratingPdfs(false); const vendorData = companiesForContracts.map(company => { // 선택된 템플릿에 따라 contractRequirements 동적으로 설정 const contractRequirements = { ndaYn: selectedContractTemplates.some(c => c.templateName.includes('비밀')), generalGtcYn: selectedContractTemplates.some(c => c.templateName.includes('General GTC')), projectGtcYn: selectedContractTemplates.some(c => c.templateName.includes('기술') && !c.templateName.includes('기술자료')), agreementYn: selectedContractTemplates.some(c => c.templateName.includes('기술자료')) }; return { vendorId: company.companyId, vendorName: company.companyName || '', vendorCode: company.companyCode, vendorCountry: '대한민국', selectedMainEmail: company.contactEmail || '', contactPerson: company.contactPerson, contactEmail: company.contactEmail, biddingCompanyId: company.id, biddingId: biddingId, hasExistingContracts: existingContracts.some(ec => ec.vendorId === company.companyId && ec.biddingCompanyId === company.id ), contractRequirements, additionalEmails: [], customEmails: [] }; }); const pdfsArray = Array.from(generatedPdfsMap.entries()).map(([key, data]) => ({ key, buffer: data.buffer, fileName: data.fileName, })); console.log("Calling sendBiddingBasicContracts with biddingId:", biddingId); console.log("vendorData:", vendorData.map(v => ({ vendorId: v.vendorId, biddingCompanyId: v.biddingCompanyId, biddingId: v.biddingId }))); contractResponse = await sendBiddingBasicContracts( biddingId, vendorData, pdfsArray, additionalMessage ); } let successMessage = '사전견적 초대가 성공적으로 발송되었습니다.'; if (contractResponse && contractResponse.success) { successMessage += `\n${contractResponse.message}`; } toast({ title: '성공', description: successMessage, }) // 상태 초기화 setSelectedCompanyIds([]); setPreQuoteDeadline(''); setAdditionalMessage(''); setExistingContracts([]); setIsGeneratingPdfs(false); setPdfGenerationProgress(0); setCurrentGeneratingContract(''); setSelectedContracts(prev => prev.map(c => ({ ...c, checked: false }))); onOpenChange(false); onSuccess(); } catch (error) { console.error('발송 실패:', error); toast({ title: '오류', description: '발송 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.', variant: 'destructive', }); setIsGeneratingPdfs(false); } }) } const handleOpenChange = (open: boolean) => { onOpenChange(open) if (!open) { setSelectedCompanyIds([]) setPreQuoteDeadline('') setAdditionalMessage('') setExistingContracts([]) setIsGeneratingPdfs(false) setPdfGenerationProgress(0) setCurrentGeneratingContract('') setSelectedContracts([]) } } const selectedContractCount = selectedContracts.filter(c => c.checked).length; const selectedCompanyCount = selectedCompanyIds.length; const companiesToReceiveContracts = invitableCompanies.filter(company => selectedCompanyIds.includes(company.id)); // 기존 계약이 없는 업체들만 계산 const availableCompanies = invitableCompanies.filter(company => !existingContracts.some(ec => ec.vendorId === company.companyId) ); const selectedAvailableCompanyCount = selectedCompanyIds.filter(id => availableCompanies.some(company => company.id === id) ).length; // 선택된 업체들 중 기존 계약이 있는 업체들 const selectedCompaniesWithExistingContracts = invitableCompanies.filter(company => selectedCompanyIds.includes(company.id) && existingContracts.some(ec => ec.vendorId === company.companyId) ); return ( 사전견적 초대 및 기본계약 발송 선택한 업체들에게 사전견적 요청과 기본계약서를 발송합니다.
{/* 견적 마감일 설정 */}
setPreQuoteDeadline(e.target.value)} className="w-full" />
{/* 기존 계약 정보 알림 */} {existingContracts.length > 0 && ( 기존 계약 정보 이미 기본계약을 받은 업체가 있습니다. 해당 업체들은 초대 대상에서 제외되며, 계약서 재생성도 건너뜁니다. )} {/* 업체 선택 섹션 */} 초대 대상 업체 {invitableCompanies.length === 0 ? (
초대 가능한 업체가 없습니다.
) : ( <>
0} onCheckedChange={handleSelectAll} />
{selectedCompanyCount}개 선택됨
{invitableCompanies.map((company) => { const hasExistingContract = existingContracts.some(ec => ec.vendorId === company.companyId); return (
handleSelectCompany(company.id, !!checked)} />
{company.companyName} {company.companyCode} {hasExistingContract && ( 계약 체결됨 )}
{hasExistingContract && (

이미 기본계약서를 받은 업체입니다. 선택에서 제외됩니다.

)}
) })}
)}
{/* 선택된 업체 중 기존 계약이 있는 경우 경고 */} {selectedCompaniesWithExistingContracts.length > 0 && ( 선택한 업체 중 제외될 업체 선택한 {selectedCompaniesWithExistingContracts.length}개 업체가 이미 기본계약서를 받았습니다. 이 업체들은 초대 발송 및 계약서 생성에서 제외됩니다.
실제 발송 대상: {selectedCompanyCount - selectedCompaniesWithExistingContracts.length}개 업체
)} {/* 기본계약서 선택 섹션 */} 기본계약서 선택 (선택된 업체에만 발송) {isLoadingTemplates ? (

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

) : (
{selectedCompanyCount === 0 && ( 알림 기본계약서를 발송할 업체를 먼저 선택해주세요. )} {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}
  • ))}
)}
)}
{/* 추가 메시지 */}