diff options
Diffstat (limited to 'lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx')
| -rw-r--r-- | lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx | 770 |
1 files changed, 0 insertions, 770 deletions
diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx deleted file mode 100644 index 3205df08..00000000 --- a/lib/bidding/pre-quote/table/bidding-pre-quote-invitation-dialog.tsx +++ /dev/null @@ -1,770 +0,0 @@ -'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<number[]>([]) - const [preQuoteDeadline, setPreQuoteDeadline] = React.useState('') - const [additionalMessage, setAdditionalMessage] = React.useState('') - - // 기본계약 관련 상태 - const [existingContracts, setExistingContracts] = React.useState<any[]>([]) - const [isGeneratingPdfs, setIsGeneratingPdfs] = React.useState(false) - const [pdfGenerationProgress, setPdfGenerationProgress] = React.useState(0) - const [currentGeneratingContract, setCurrentGeneratingContract] = React.useState('') - - // 기본계약서 템플릿 관련 상태 - const [availableTemplates, setAvailableTemplates] = React.useState<BasicContractTemplate[]>([]) - const [selectedContracts, setSelectedContracts] = React.useState<SelectedContract[]>([]) - 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<ReturnType<typeof sendBiddingBasicContracts>> | null = null - if (selectedContractTemplates.length > 0 && selectedCompanyIds.length > 0) { - setIsGeneratingPdfs(true) - setPdfGenerationProgress(0) - - const generatedPdfsMap = new Map<string, { buffer: number[], fileName: string }>() - - 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 ( - <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogContent className="sm:max-w-[800px] max-h-[90vh] flex flex-col"> - <DialogHeader> - <DialogTitle className="flex items-center gap-2"> - <Mail className="w-5 h-5" /> - 사전견적 초대 및 기본계약 발송 - </DialogTitle> - <DialogDescription> - 선택한 업체들에게 사전견적 요청과 기본계약서를 발송합니다. - </DialogDescription> - </DialogHeader> - - <div className="flex-1 overflow-y-auto px-1" style={{ maxHeight: 'calc(70vh - 200px)' }}> - <div className="space-y-6 pr-4"> - {/* 견적 마감일 설정 */} - <div className="mb-6 p-4 border rounded-lg bg-muted/30"> - <Label htmlFor="preQuoteDeadline" className="text-sm font-medium mb-2 flex items-center gap-2"> - <Calendar className="w-4 h-4" /> - 견적 마감일 - </Label> - <Input - id="preQuoteDeadline" - type="datetime-local" - value={preQuoteDeadline} - onChange={(e) => setPreQuoteDeadline(e.target.value)} - className="w-full" - /> - </div> - - {/* 기존 계약 정보 알림 */} - {existingContracts.length > 0 && ( - <Alert className="border-orange-500 bg-orange-50"> - <Info className="h-4 w-4 text-orange-600" /> - <AlertTitle className="text-orange-800">기존 계약 정보</AlertTitle> - <AlertDescription className="text-orange-700"> - 이미 기본계약을 받은 업체가 있습니다. - 해당 업체들은 초대 대상에서 제외되며, 계약서 재생성도 건너뜁니다. - </AlertDescription> - </Alert> - )} - - {/* 업체 선택 섹션 */} - <Card className="border-2 border-dashed"> - <CardHeader className="pb-3"> - <CardTitle className="flex items-center gap-2 text-base"> - <Building2 className="h-5 w-5 text-green-600" /> - 초대 대상 업체 - </CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - {invitableCompanies.length === 0 ? ( - <div className="text-center py-8 text-muted-foreground"> - 초대 가능한 업체가 없습니다. - </div> - ) : ( - <> - <div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"> - <div className="flex items-center gap-2"> - <Checkbox - id="select-all-companies" - checked={selectedAvailableCompanyCount === availableCompanies.length && availableCompanies.length > 0} - onCheckedChange={handleSelectAll} - /> - <Label htmlFor="select-all-companies" className="font-medium"> - 전체 선택 ({availableCompanies.length}개 업체) - </Label> - </div> - <Badge variant="outline"> - {selectedCompanyCount}개 선택됨 - </Badge> - </div> - - <div className="space-y-3 max-h-80 overflow-y-auto"> - {invitableCompanies.map((company) => { - const hasExistingContract = existingContracts.some(ec => ec.vendorId === company.companyId); - return ( - <div key={company.id} className={cn("flex items-center space-x-3 p-3 border rounded-lg transition-colors", - selectedCompanyIds.includes(company.id) && !hasExistingContract && "border-green-500 bg-green-50", - hasExistingContract && "border-orange-500 bg-orange-50 opacity-75" - )}> - <Checkbox - id={`company-${company.id}`} - checked={selectedCompanyIds.includes(company.id)} - disabled={hasExistingContract} - onCheckedChange={(checked) => handleSelectCompany(company.id, !!checked)} - /> - <div className="flex-1"> - <div className="flex items-center gap-2"> - <span className={cn("font-medium", hasExistingContract && "text-muted-foreground")}> - {company.companyName} - </span> - <Badge variant="outline" className="text-xs"> - {company.companyCode} - </Badge> - {hasExistingContract && ( - <Badge variant="secondary" className="text-xs"> - <CheckCircle className="h-3 w-3 mr-1" /> - 계약 체결됨 - </Badge> - )} - </div> - {hasExistingContract && ( - <p className="text-xs text-orange-600 mt-1"> - 이미 기본계약서를 받은 업체입니다. 선택에서 제외됩니다. - </p> - )} - </div> - </div> - ) - })} - </div> - </> - )} - </CardContent> - </Card> - - {/* 선택된 업체 중 기존 계약이 있는 경우 경고 */} - {selectedCompaniesWithExistingContracts.length > 0 && ( - <Alert className="border-red-500 bg-red-50"> - <Info className="h-4 w-4 text-red-600" /> - <AlertTitle className="text-red-800">선택한 업체 중 제외될 업체</AlertTitle> - <AlertDescription className="text-red-700"> - 선택한 {selectedCompaniesWithExistingContracts.length}개 업체가 이미 기본계약서를 받았습니다. - 이 업체들은 초대 발송 및 계약서 생성에서 제외됩니다. - <br /> - <strong>실제 발송 대상: {selectedCompanyCount - selectedCompaniesWithExistingContracts.length}개 업체</strong> - </AlertDescription> - </Alert> - )} - - {/* 기본계약서 선택 섹션 */} - <Separator /> - <Card className="border-2 border-dashed"> - <CardHeader className="pb-3"> - <CardTitle className="flex items-center gap-2 text-base"> - <FileText className="h-5 w-5 text-blue-600" /> - 기본계약서 선택 (선택된 업체에만 발송) - </CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - {isLoadingTemplates ? ( - <div className="text-center py-6"> - <RefreshCw className="h-6 w-6 animate-spin mx-auto mb-2 text-blue-600" /> - <p className="text-sm text-muted-foreground">기본계약서 템플릿을 불러오는 중...</p> - </div> - ) : ( - <div className="space-y-4"> - {selectedCompanyCount === 0 && ( - <Alert className="border-red-500 bg-red-50"> - <Info className="h-4 w-4 text-red-600" /> - <AlertTitle className="text-red-800">알림</AlertTitle> - <AlertDescription className="text-red-700"> - 기본계약서를 발송할 업체를 먼저 선택해주세요. - </AlertDescription> - </Alert> - )} - {availableTemplates.length === 0 ? ( - <div className="text-center py-8 text-muted-foreground"> - <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" /> - <p>사용 가능한 기본계약서 템플릿이 없습니다.</p> - </div> - ) : ( - <> - <div className="flex items-center justify-between p-3 bg-muted/50 rounded-lg"> - <div className="flex items-center gap-2"> - <Checkbox - id="select-all-contracts" - checked={selectedContracts.length > 0 && selectedContracts.every(c => c.checked)} - onCheckedChange={toggleAllContractSelection} - /> - <Label htmlFor="select-all-contracts" className="font-medium"> - 전체 선택 ({availableTemplates.length}개 템플릿) - </Label> - </div> - <Badge variant="outline"> - {selectedContractCount}개 선택됨 - </Badge> - </div> - <div className="grid gap-3 max-h-60 overflow-y-auto"> - {selectedContracts.map((contract) => ( - <div - key={contract.templateId} - className={cn( - "flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors cursor-pointer", - contract.checked && "border-blue-500 bg-blue-50" - )} - onClick={() => toggleContractSelection(contract.templateId)} - > - <div className="flex items-center gap-3"> - <Checkbox - id={`contract-${contract.templateId}`} - checked={contract.checked} - onCheckedChange={() => toggleContractSelection(contract.templateId)} - /> - <div className="flex-1"> - <Label - htmlFor={`contract-${contract.templateId}`} - className="font-medium cursor-pointer" - > - {contract.templateName} - </Label> - <p className="text-xs text-muted-foreground mt-1"> - {contract.contractType} - </p> - </div> - </div> - </div> - ))} - </div> - </> - )} - {selectedContractCount > 0 && ( - <div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-lg"> - <div className="flex items-center gap-2 mb-2"> - <CheckCircle className="h-4 w-4 text-green-600" /> - <span className="font-medium text-green-900 text-sm"> - 선택된 기본계약서 ({selectedContractCount}개) - </span> - </div> - <ul className="space-y-1 text-xs text-green-800 list-disc list-inside"> - {selectedContracts.filter(c => c.checked).map((contract) => ( - <li key={contract.templateId}> - {contract.templateName} - </li> - ))} - </ul> - </div> - )} - </div> - )} - </CardContent> - </Card> - - {/* 추가 메시지 */} - <div className="space-y-2"> - <Label htmlFor="contractMessage" className="text-sm font-medium"> - 계약서 추가 메시지 (선택사항) - </Label> - <textarea - id="contractMessage" - className="w-full min-h-[60px] p-3 text-sm border rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-primary" - placeholder="기본계약서와 함께 보낼 추가 메시지를 입력하세요..." - value={additionalMessage} - onChange={(e) => setAdditionalMessage(e.target.value)} - /> - </div> - - {/* PDF 생성 진행 상황 */} - {isGeneratingPdfs && ( - <Alert className="border-blue-500 bg-blue-50"> - <div className="space-y-3"> - <div className="flex items-center gap-2"> - <RefreshCw className="h-4 w-4 animate-spin text-blue-600" /> - <AlertTitle className="text-blue-800">기본계약서 생성 중</AlertTitle> - </div> - <AlertDescription> - <div className="space-y-2"> - <p className="text-sm text-blue-700">{currentGeneratingContract}</p> - <Progress value={pdfGenerationProgress} className="h-2" /> - <p className="text-xs text-blue-600"> - {Math.round(pdfGenerationProgress)}% 완료 - </p> - </div> - </AlertDescription> - </div> - </Alert> - )} - </div> - </div> - - <DialogFooter className="flex-col sm:flex-row-reverse sm:justify-between items-center px-4 pt-4"> - <div className="flex gap-2 w-full sm:w-auto"> - <Button variant="outline" onClick={() => handleOpenChange(false)} className="w-full sm:w-auto"> - 취소 - </Button> - <Button - onClick={handleSendInvitations} - disabled={isPending || selectedCompanyCount === 0 || isGeneratingPdfs} - className="w-full sm:w-auto" - > - {isPending ? ( - <> - <RefreshCw className="w-4 h-4 mr-2 animate-spin" /> - 발송 중... - </> - ) : ( - <> - <Mail className="w-4 h-4 mr-2" /> - 초대 발송 및 계약서 생성 - </> - )} - </Button> - </div> - {/* {(selectedCompanyCount > 0 || selectedContractCount > 0) && ( - <div className="mt-4 sm:mt-0 text-sm text-muted-foreground"> - {selectedCompanyCount > 0 && ( - <p> - <strong>{selectedCompanyCount}개 업체</strong>에 초대를 발송합니다. - </p> - )} - {selectedContractCount > 0 && selectedCompanyCount > 0 && ( - <p> - 이 중 <strong>{companiesToReceiveContracts.length}개 업체</strong>에 <strong>{selectedContractCount}개</strong>의 기본계약서를 발송합니다. - </p> - )} - </div> - )} */} - </DialogFooter> - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file |
