From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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

---
 .../detail/table/bidding-invitation-dialog.tsx     | 718 +++++++++++++++------
 1 file changed, 530 insertions(+), 188 deletions(-)

(limited to 'lib/bidding/detail/table/bidding-invitation-dialog.tsx')

diff --git a/lib/bidding/detail/table/bidding-invitation-dialog.tsx b/lib/bidding/detail/table/bidding-invitation-dialog.tsx
index cd79850a..ffb1fcb3 100644
--- a/lib/bidding/detail/table/bidding-invitation-dialog.tsx
+++ b/lib/bidding/detail/table/bidding-invitation-dialog.tsx
@@ -14,7 +14,6 @@ import {
   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'
@@ -22,24 +21,45 @@ import { cn } from '@/lib/utils'
 import {
   Mail,
   Building2,
-  Calendar,
   FileText,
   CheckCircle,
   Info,
   RefreshCw,
+  X,
+  ChevronDown,
   Plus,
-  X
+  UserPlus,
+  Users
 } from 'lucide-react'
-import { sendBiddingBasicContracts, getSelectedVendorsForBidding, getExistingBasicContractsForBidding } from '../../pre-quote/service'
+import { getExistingBasicContractsForBidding } from '../../pre-quote/service'
 import { getActiveContractTemplates } from '../../service'
+import { getVendorContacts } from '@/lib/vendors/service'
 import { useToast } from '@/hooks/use-toast'
 import { useTransition } from 'react'
+import { SelectTrigger } from '@/components/ui/select'
+import { SelectValue } from '@/components/ui/select'
+import { SelectContent } from '@/components/ui/select'
+import { SelectItem } from '@/components/ui/select'
+import { Select } from '@/components/ui/select'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import { Separator } from '@/components/ui/separator'
+
+
+interface VendorContact {
+  id: number
+  contactName: string
+  contactEmail: string
+  contactPhone?: string | null
+  contactPosition?: string | null
+  contactDepartment?: string | null
+}
 
 interface VendorContractRequirement {
   vendorId: number
   vendorName: string
   vendorCode?: string
   vendorCountry?: string
+  vendorEmail?: string // 벤더의 기본 이메일 (vendors.email)
   contactPerson?: string
   contactEmail?: string
   ndaYn?: boolean
@@ -50,6 +70,20 @@ interface VendorContractRequirement {
   biddingId: number
 }
 
+interface CustomEmail {
+  id: string
+  email: string
+  name?: string
+}
+
+interface VendorWithContactInfo extends VendorContractRequirement {
+  contacts: VendorContact[]
+  selectedMainEmail: string
+  additionalEmails: string[]
+  customEmails: CustomEmail[]
+  hasExistingContracts: boolean
+}
+
 interface BasicContractTemplate {
   id: number
   templateName: string
@@ -74,25 +108,8 @@ interface BiddingInvitationDialogProps {
   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
-    }>
+    vendors: VendorWithContactInfo[]
     generatedPdfs: Array<{
       key: string
       buffer: number[]
@@ -108,82 +125,206 @@ export function BiddingInvitationDialog({
   vendors,
   biddingId,
   biddingTitle,
-  projectName,
   onSend,
 }: BiddingInvitationDialogProps) {
   const { toast } = useToast()
   const [isPending, startTransition] = useTransition()
 
   // 기본계약 관련 상태
-  const [existingContracts, setExistingContracts] = React.useState<any[]>([])
+  const [, setExistingContractsList] = React.useState<Array<{ vendorId: number; biddingCompanyId: number }>>([])
   const [isGeneratingPdfs, setIsGeneratingPdfs] = React.useState(false)
   const [pdfGenerationProgress, setPdfGenerationProgress] = React.useState(0)
   const [currentGeneratingContract, setCurrentGeneratingContract] = React.useState('')
 
+  // 벤더 정보 상태 (담당자 선택 기능 포함)
+  const [vendorData, setVendorData] = React.useState<VendorWithContactInfo[]>([])
+
   // 기본계약서 템플릿 관련 상태
-  const [availableTemplates, setAvailableTemplates] = React.useState<any[]>([])
+  const [availableTemplates, setAvailableTemplates] = React.useState<BasicContractTemplate[]>([])
   const [selectedContracts, setSelectedContracts] = React.useState<SelectedContract[]>([])
   const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false)
   const [additionalMessage, setAdditionalMessage] = React.useState('')
 
+  // 커스텀 이메일 관련 상태
+  const [showCustomEmailForm, setShowCustomEmailForm] = React.useState<Record<number, boolean>>({})
+  const [customEmailInputs, setCustomEmailInputs] = React.useState<Record<number, { email: string; name: string }>>({})
+  const [customEmailCounter, setCustomEmailCounter] = React.useState(0)
+
+  // 벤더 정보 업데이트 함수
+  const updateVendor = React.useCallback((vendorId: number, updates: Partial<VendorWithContactInfo>) => {
+    setVendorData(prev => prev.map(vendor =>
+      vendor.vendorId === vendorId ? { ...vendor, ...updates } : vendor
+    ))
+  }, [])
+
+  // CC 이메일 토글
+  const toggleAdditionalEmail = React.useCallback((vendorId: number, email: string) => {
+    setVendorData(prev => prev.map(vendor => {
+      if (vendor.vendorId === vendorId) {
+        const additionalEmails = vendor.additionalEmails.includes(email)
+          ? vendor.additionalEmails.filter(e => e !== email)
+          : [...vendor.additionalEmails, email]
+        return { ...vendor, additionalEmails }
+      }
+      return vendor
+    }))
+  }, [])
+
+  // 커스텀 이메일 추가
+  const addCustomEmail = React.useCallback((vendorId: number) => {
+    const input = customEmailInputs[vendorId]
+    if (!input?.email) return
+
+    setVendorData(prev => prev.map(vendor => {
+      if (vendor.vendorId === vendorId) {
+        const newCustomEmail: CustomEmail = {
+          id: `custom-${customEmailCounter}`,
+          email: input.email,
+          name: input.name || input.email
+        }
+        return {
+          ...vendor,
+          customEmails: [...vendor.customEmails, newCustomEmail]
+        }
+      }
+      return vendor
+    }))
+
+    setCustomEmailInputs(prev => ({
+      ...prev,
+      [vendorId]: { email: '', name: '' }
+    }))
+    setCustomEmailCounter(prev => prev + 1)
+  }, [customEmailInputs, customEmailCounter])
+
+  // 커스텀 이메일 제거
+  const removeCustomEmail = React.useCallback((vendorId: number, customEmailId: string) => {
+    setVendorData(prev => prev.map(vendor => {
+      if (vendor.vendorId === vendorId) {
+        return {
+          ...vendor,
+          customEmails: vendor.customEmails.filter(ce => ce.id !== customEmailId),
+          additionalEmails: vendor.additionalEmails.filter(email =>
+            !vendor.customEmails.find(ce => ce.id === customEmailId)?.email || email !== vendor.customEmails.find(ce => ce.id === customEmailId)?.email
+          )
+        }
+      }
+      return vendor
+    }))
+  }, [])
+
+  // 총 수신자 수 계산
+  const totalRecipientCount = React.useMemo(() => {
+    return vendorData.reduce((sum, vendor) => {
+      return sum + 1 + vendor.additionalEmails.length // 주 수신자 1명 + CC
+    }, 0)
+  }, [vendorData])
+
   // 선택된 업체들 (사전견적에서 선정된 업체들만)
   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]
+    vendorData.filter(vendor => vendor.hasExistingContracts),
+    [vendorData]
   )
 
-  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) {
+    if (open && selectedVendors.length > 0) {
       const fetchInitialData = async () => {
         setIsLoadingTemplates(true);
         try {
-          const [contractsResult, templatesData] = await Promise.all([
-            getSelectedVendorsForBidding(biddingId),
+          const [existingContractsResult, templatesData] = await Promise.all([
+            getExistingBasicContractsForBidding(biddingId),
             getActiveContractTemplates(),
           ]);
 
-          // 기존 계약 조회 (사전견적에서 보낸 기본계약 확인) - 서버 액션 사용
-          const existingContracts = await getExistingBasicContractsForBidding(biddingId);
-          setExistingContracts(existingContracts.success ? existingContracts.contracts || [] : []);
+          // 기존 계약 조회
+          const contracts = existingContractsResult.success ? existingContractsResult.contracts || [] : [];
+          const typedContracts = contracts.map(c => ({
+            vendorId: c.vendorId || 0,
+            biddingCompanyId: c.biddingCompanyId || 0
+          }));
+          setExistingContractsList(typedContracts);
 
           // 템플릿 로드 (4개 타입만 필터링)
-          // 4개 템플릿 타입만 필터링: 비밀, General, Project, 기술자료
           const allowedTemplateNames = ['비밀', 'General GTC', '기술', '기술자료'];
           const rawTemplates = templatesData.templates || [];
-          const filteredTemplates = rawTemplates.filter((template: any) =>
+          const filteredTemplates = rawTemplates.filter((template: BasicContractTemplate) =>
             allowedTemplateNames.some(allowedName =>
               template.templateName.includes(allowedName) ||
               allowedName.includes(template.templateName)
             )
           );
-          setAvailableTemplates(filteredTemplates as any);
-          const initialSelected = filteredTemplates.map((template: any) => ({
+          setAvailableTemplates(filteredTemplates);
+          const initialSelected = filteredTemplates.map((template: BasicContractTemplate) => ({
             templateId: template.id,
             templateName: template.templateName,
             contractType: template.templateName,
             checked: false
           }));
           setSelectedContracts(initialSelected);
+
+          // 벤더 담당자 정보 병렬로 가져오기
+          const vendorContactsPromises = selectedVendors.map(vendor =>
+            getVendorContacts({
+              page: 1,
+              perPage: 100,
+              flags: [],
+              sort: [],
+              filters: [],
+              joinOperator: 'and',
+              search: '',
+              contactName: '',
+              contactPosition: '',
+              contactEmail: '',
+              contactPhone: ''
+            }, vendor.vendorId)
+              .then(result => ({
+                vendorId: vendor.vendorId,
+                contacts: (result.data || []).map(contact => ({
+                  id: contact.id,
+                  contactName: contact.contactName,
+                  contactEmail: contact.contactEmail,
+                  contactPhone: contact.contactPhone,
+                  contactPosition: contact.contactPosition,
+                  contactDepartment: contact.contactDepartment
+                }))
+              }))
+              .catch(() => ({
+                vendorId: vendor.vendorId,
+                contacts: []
+              }))
+          );
+
+          const vendorContactsResults = await Promise.all(vendorContactsPromises);
+          const vendorContactsMap = new Map(vendorContactsResults.map(result => [result.vendorId, result.contacts]));
+
+          // vendorData 초기화 (담당자 정보 포함)
+          const initialVendorData: VendorWithContactInfo[] = selectedVendors.map(vendor => {
+            const hasExistingContract = typedContracts.some((ec) =>
+              ec.vendorId === vendor.vendorId && ec.biddingCompanyId === vendor.biddingCompanyId
+            );
+            const vendorContacts = vendorContactsMap.get(vendor.vendorId) || [];
+
+            // 주 수신자 기본값: 벤더의 기본 이메일 (vendorEmail)
+            const defaultEmail = vendor.vendorEmail || (vendorContacts.length > 0 ? vendorContacts[0].contactEmail : '');
+            console.log(defaultEmail, "defaultEmail");
+            return {
+              ...vendor,
+              contacts: vendorContacts,
+              selectedMainEmail: defaultEmail,
+              additionalEmails: [],
+              customEmails: [],
+              hasExistingContracts: hasExistingContract
+            };
+          });
+
+          setVendorData(initialVendorData);
         } catch (error) {
           console.error('초기 데이터 로드 실패:', error);
           toast({
@@ -193,13 +334,14 @@ export function BiddingInvitationDialog({
           });
           setAvailableTemplates([]);
           setSelectedContracts([]);
+          setVendorData([]);
         } finally {
           setIsLoadingTemplates(false);
         }
       }
       fetchInitialData();
     }
-  }, [open, biddingId, toast]);
+  }, [open, biddingId, selectedVendors, toast]);
 
   const handleOpenChange = (open: boolean) => {
     onOpenChange(open)
@@ -209,6 +351,7 @@ export function BiddingInvitationDialog({
       setIsGeneratingPdfs(false)
       setPdfGenerationProgress(0)
       setCurrentGeneratingContract('')
+      setVendorData([])
     }
   }
 
@@ -245,32 +388,32 @@ export function BiddingInvitationDialog({
           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(
           {
@@ -280,29 +423,29 @@ export function BiddingInvitationDialog({
           },
           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);
@@ -333,43 +476,39 @@ export function BiddingInvitationDialog({
           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}는 사전견적에서 이미 기본계약을 받았으므로 건너뜁니다.`);
+          for (const vendorWithContact of vendorData) {
+            // 기존 계약이 있는 경우 건너뛰기
+            if (vendorWithContact.hasExistingContracts) {
+              console.log(`벤더 ${vendorWithContact.vendorName}는 사전견적에서 이미 기본계약을 받았으므로 건너뜁니다.`);
               generatedCount++;
-              setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100);
+              setPdfGenerationProgress((generatedCount / vendorData.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 = '기술자료';
+            for (const contract of selectedContractTemplates) {
+              setCurrentGeneratingContract(`${vendorWithContact.vendorName} - ${contract.templateName}`);
+              const templateDetails = availableTemplates.find(t => t.id === contract.templateId);
+
+              if (templateDetails) {
+                const pdfData = await generateBasicContractPdf(templateDetails, vendorWithContact.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 = `${vendorWithContact.vendorId}_${contractType}_${contract.templateName}`;
+                generatedPdfsMap.set(key, pdfData);
               }
-              const key = `${vendor.vendorId}_${contractType}_${contract.templateName}`;
-              generatedPdfsMap.set(key, pdfData);
             }
+            generatedCount++;
+            setPdfGenerationProgress((generatedCount / vendorData.length) * 100);
           }
-          generatedCount++;
-          setPdfGenerationProgress((generatedCount / selectedVendors.length) * 100);
-        }
 
           setIsGeneratingPdfs(false);
 
@@ -382,30 +521,6 @@ export function BiddingInvitationDialog({
           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,
@@ -428,7 +543,7 @@ export function BiddingInvitationDialog({
 
   return (
     <Dialog open={open} onOpenChange={handleOpenChange}>
-      <DialogContent className="sm:max-w-[800px] max-h-[90vh] flex flex-col"  style={{width:900, maxWidth:900}}>
+      <DialogContent className="sm:max-w-[800px] max-h-[90vh] flex flex-col" style={{ width: 900, maxWidth: 900 }}>
         <DialogHeader>
           <DialogTitle className="flex items-center gap-2">
             <Mail className="w-5 h-5" />
@@ -453,72 +568,299 @@ export function BiddingInvitationDialog({
               </Alert>
             )}
 
-            {/* 대상 업체 정보 */}
-            <Card>
-              <CardHeader className="pb-3">
-                <CardTitle className="flex items-center gap-2 text-base">
-                  <Building2 className="h-5 w-5 text-green-600" />
-                  초대 대상 업체 ({selectedVendors.length}개)
-                </CardTitle>
-              </CardHeader>
-              <CardContent>
-                {selectedVendors.length === 0 ? (
-                  <div className="text-center py-6 text-muted-foreground">
-                    초대 가능한 업체가 없습니다.
-                  </div>
-                ) : (
-                  <div className="space-y-4">
-                    {/* 계약서가 생성될 업체들 */}
-                    {vendorsWithoutExistingContracts.length > 0 && (
-                      <div>
-                        <h4 className="text-sm font-medium text-green-700 mb-2 flex items-center gap-2">
-                          <CheckCircle className="h-4 w-4 text-green-600" />
-                          계약서 생성 대상 ({vendorsWithoutExistingContracts.length}개)
-                        </h4>
-                        <div className="space-y-2 max-h-32 overflow-y-auto">
-                          {vendorsWithoutExistingContracts.map((vendor) => (
-                            <div key={vendor.vendorId} className="flex items-center gap-2 text-sm p-2 bg-green-50 rounded border border-green-200">
-                              <CheckCircle className="h-4 w-4 text-green-600" />
-                              <span className="font-medium">{vendor.vendorName}</span>
-                              <Badge variant="outline" className="text-xs">
-                                {vendor.vendorCode}
-                              </Badge>
-                            </div>
-                          ))}
-                        </div>
-                      </div>
-                    )}
+            {/* 대상 업체 정보 - 테이블 형식 */}
+            <div className="space-y-4">
+              <div className="flex items-center justify-between">
+                <div className="flex items-center gap-2 text-sm font-medium">
+                  <Building2 className="h-4 w-4" />
+                  초대 대상 업체 ({vendorData.length})
+                </div>
+                <Badge variant="outline" className="flex items-center gap-1">
+                  <Users className="h-3 w-3" />
+                  총 {totalRecipientCount}명
+                </Badge>
+              </div>
 
-                    {/* 기존 계약이 있는 업체들 */}
-                    {vendorsWithExistingContracts.length > 0 && (
-                      <div>
-                        <h4 className="text-sm font-medium text-orange-700 mb-2 flex items-center gap-2">
-                          <X className="h-4 w-4 text-orange-600" />
-                          기존 계약 존재 (계약서 재생성 건너뜀) ({vendorsWithExistingContracts.length}개)
-                        </h4>
-                        <div className="space-y-2 max-h-32 overflow-y-auto">
-                          {vendorsWithExistingContracts.map((vendor) => (
-                            <div key={vendor.vendorId} className="flex items-center gap-2 text-sm p-2 bg-orange-50 rounded border border-orange-200">
-                              <X className="h-4 w-4 text-orange-600" />
-                              <span className="font-medium">{vendor.vendorName}</span>
-                              <Badge variant="outline" className="text-xs">
-                                {vendor.vendorCode}
-                              </Badge>
-                              <Badge variant="secondary" className="text-xs bg-orange-100 text-orange-800">
-                                계약 존재 (재생성 건너뜀)
-                              </Badge>
-                              <Badge variant="outline" className="text-xs border-green-500 text-green-700">
-                                본입찰 초대
-                              </Badge>
-                            </div>
-                          ))}
-                        </div>
-                      </div>
-                    )}
-                  </div>
-                )}
-              </CardContent>
-            </Card>
+              {vendorData.length === 0 ? (
+                <div className="text-center py-6 text-muted-foreground border rounded-lg">
+                  초대 가능한 업체가 없습니다.
+                </div>
+              ) : (
+                <div className="border rounded-lg overflow-hidden">
+                  <table className="w-full">
+                    <thead className="bg-muted/50 border-b">
+                      <tr>
+                        <th className="text-left p-2 text-xs font-medium">No.</th>
+                        <th className="text-left p-2 text-xs font-medium">업체명</th>
+                        <th className="text-left p-2 text-xs font-medium">주 수신자</th>
+                        <th className="text-left p-2 text-xs font-medium">CC</th>
+                        <th className="text-left p-2 text-xs font-medium">작업</th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      {vendorData.map((vendor, index) => {
+                        const allContacts = vendor.contacts || [];
+                        const allEmails = [
+                          // 벤더의 기본 이메일을 첫 번째로 표시
+                          ...(vendor.vendorEmail ? [{
+                            value: vendor.vendorEmail,
+                            label: `${vendor.vendorEmail}`,
+                            email: vendor.vendorEmail,
+                            type: 'vendor' as const
+                          }] : []),
+                          // 담당자 이메일들
+                          ...allContacts.map(c => ({
+                            value: c.contactEmail,
+                            label: `${c.contactName} ${c.contactPosition ? `(${c.contactPosition})` : ''}`,
+                            email: c.contactEmail,
+                            type: 'contact' as const
+                          })),
+                          // 커스텀 이메일들
+                          ...vendor.customEmails.map(c => ({
+                            value: c.email,
+                            label: c.name || c.email,
+                            email: c.email,
+                            type: 'custom' as const
+                          }))
+                        ];
+
+                        const ccEmails = allEmails.filter(e => e.value !== vendor.selectedMainEmail);
+                        const selectedMainEmailInfo = allEmails.find(e => e.value === vendor.selectedMainEmail);
+                        const isFormOpen = showCustomEmailForm[vendor.vendorId];
+
+                        return (
+                          <React.Fragment key={vendor.vendorId}>
+                            <tr className="border-b hover:bg-muted/20">
+                              <td className="p-2">
+                                <div className="flex items-center gap-1">
+                                  <div className="flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-xs font-medium">
+                                    {index + 1}
+                                  </div>
+                                </div>
+                              </td>
+                              <td className="p-2">
+                                <div className="space-y-1">
+                                  <div className="font-medium text-sm">{vendor.vendorName}</div>
+                                  <div className="flex items-center gap-1">
+                                    <Badge variant="outline" className="text-xs">
+                                      {vendor.vendorCountry || vendor.vendorCode}
+                                    </Badge>
+                                  </div>
+                                </div>
+                              </td>
+                              <td className="p-2">
+                                <Select
+                                  value={vendor.selectedMainEmail}
+                                  onValueChange={(value) => updateVendor(vendor.vendorId, { selectedMainEmail: value })}
+                                >
+                                  <SelectTrigger className="h-7 text-xs w-[200px]">
+                                    <SelectValue placeholder="선택하세요">
+                                      {selectedMainEmailInfo && (
+                                        <div className="flex items-center gap-1">
+                                          {selectedMainEmailInfo.type === 'custom' && <UserPlus className="h-3 w-3 text-green-500" />}
+                                          <span className="truncate">{selectedMainEmailInfo.label}</span>
+                                        </div>
+                                      )}
+                                    </SelectValue>
+                                  </SelectTrigger>
+                                  <SelectContent>
+                                    {allEmails.map((email) => (
+                                      <SelectItem key={email.value} value={email.value} className="text-xs">
+                                        <div className="flex items-center gap-1">
+                                          {email.type === 'custom' && <UserPlus className="h-3 w-3 text-green-500" />}
+                                          <span>{email.label}</span>
+                                        </div>
+                                      </SelectItem>
+                                    ))}
+                                  </SelectContent>
+                                </Select>
+                                {!vendor.selectedMainEmail && (
+                                  <span className="text-xs text-red-500">필수</span>
+                                )}
+                              </td>
+                              <td className="p-2">
+                                <Popover>
+                                  <PopoverTrigger asChild>
+                                    <Button variant="outline" className="h-7 text-xs">
+                                      {vendor.additionalEmails.length > 0
+                                        ? `${vendor.additionalEmails.length}명`
+                                        : "선택"
+                                      }
+                                      <ChevronDown className="ml-1 h-3 w-3" />
+                                    </Button>
+                                  </PopoverTrigger>
+                                  <PopoverContent className="w-48 p-2">
+                                    <div className="max-h-48 overflow-y-auto space-y-1">
+                                      {ccEmails.map((email) => (
+                                        <div key={email.value} className="flex items-center space-x-1 p-1">
+                                          <Checkbox
+                                            checked={vendor.additionalEmails.includes(email.value)}
+                                            onCheckedChange={() => toggleAdditionalEmail(vendor.vendorId, email.value)}
+                                            className="h-3 w-3"
+                                          />
+                                          <label className="text-xs cursor-pointer flex-1 truncate">
+                                            {email.label}
+                                          </label>
+                                        </div>
+                                      ))}
+                                    </div>
+                                  </PopoverContent>
+                                </Popover>
+                              </td>
+                              <td className="p-2">
+                                <div className="flex items-center gap-1">
+                                  <Button
+                                    variant={isFormOpen ? "default" : "ghost"}
+                                    size="sm"
+                                    className="h-6 w-6 p-0"
+                                    onClick={() => {
+                                      setShowCustomEmailForm(prev => ({
+                                        ...prev,
+                                        [vendor.vendorId]: !prev[vendor.vendorId]
+                                      }));
+                                    }}
+                                  >
+                                    {isFormOpen ? <X className="h-3 w-3" /> : <Plus className="h-3 w-3" />}
+                                  </Button>
+                                  {vendor.customEmails.length > 0 && (
+                                    <Badge variant="secondary" className="text-xs">
+                                      +{vendor.customEmails.length}
+                                    </Badge>
+                                  )}
+                                </div>
+                              </td>
+                            </tr>
+
+                            {/* 인라인 수신자 추가 폼 */}
+                            {isFormOpen && (
+                              <tr className="bg-muted/10 border-b">
+                                <td colSpan={5} className="p-4">
+                                  <div className="space-y-3">
+                                    <div className="flex items-center justify-between mb-2">
+                                      <div className="flex items-center gap-2 text-sm font-medium">
+                                        <UserPlus className="h-4 w-4" />
+                                        수신자 추가 - {vendor.vendorName}
+                                      </div>
+                                      <Button
+                                        variant="ghost"
+                                        size="sm"
+                                        className="h-6 w-6 p-0"
+                                        onClick={() => setShowCustomEmailForm(prev => ({
+                                          ...prev,
+                                          [vendor.vendorId]: false
+                                        }))}
+                                      >
+                                        <X className="h-3 w-3" />
+                                      </Button>
+                                    </div>
+
+                                    <div className="flex gap-2 items-end">
+                                      <div className="w-[150px]">
+                                        <Label className="text-xs mb-1 block">이름 (선택)</Label>
+                                        <Input
+                                          placeholder="홍길동"
+                                          className="h-8 text-sm"
+                                          value={customEmailInputs[vendor.vendorId]?.name || ''}
+                                          onChange={(e) => setCustomEmailInputs(prev => ({
+                                            ...prev,
+                                            [vendor.vendorId]: {
+                                              ...prev[vendor.vendorId],
+                                              name: e.target.value
+                                            }
+                                          }))}
+                                        />
+                                      </div>
+                                      <div className="flex-1">
+                                        <Label className="text-xs mb-1 block">이메일 <span className="text-red-500">*</span></Label>
+                                        <Input
+                                          type="email"
+                                          placeholder="example@company.com"
+                                          className="h-8 text-sm"
+                                          value={customEmailInputs[vendor.vendorId]?.email || ''}
+                                          onChange={(e) => setCustomEmailInputs(prev => ({
+                                            ...prev,
+                                            [vendor.vendorId]: {
+                                              ...prev[vendor.vendorId],
+                                              email: e.target.value
+                                            }
+                                          }))}
+                                          onKeyPress={(e) => {
+                                            if (e.key === 'Enter') {
+                                              e.preventDefault();
+                                              addCustomEmail(vendor.vendorId);
+                                            }
+                                          }}
+                                        />
+                                      </div>
+                                      <Button
+                                        size="sm"
+                                        className="h-8 px-4"
+                                        onClick={() => addCustomEmail(vendor.vendorId)}
+                                        disabled={!customEmailInputs[vendor.vendorId]?.email}
+                                      >
+                                        <Plus className="h-3 w-3 mr-1" />
+                                        추가
+                                      </Button>
+                                      <Button
+                                        variant="outline"
+                                        size="sm"
+                                        className="h-8 px-4"
+                                        onClick={() => {
+                                          setCustomEmailInputs(prev => ({
+                                            ...prev,
+                                            [vendor.vendorId]: { email: '', name: '' }
+                                          }));
+                                          setShowCustomEmailForm(prev => ({
+                                            ...prev,
+                                            [vendor.vendorId]: false
+                                          }));
+                                        }}
+                                      >
+                                        취소
+                                      </Button>
+                                    </div>
+
+                                    {/* 추가된 커스텀 이메일 목록 */}
+                                    {vendor.customEmails.length > 0 && (
+                                      <div className="mt-3 pt-3 border-t">
+                                        <div className="text-xs text-muted-foreground mb-2">추가된 수신자 목록</div>
+                                        <div className="grid grid-cols-2 xl:grid-cols-3 gap-2">
+                                          {vendor.customEmails.map((custom) => (
+                                            <div key={custom.id} className="flex items-center justify-between bg-background rounded-md p-2">
+                                              <div className="flex items-center gap-2 min-w-0">
+                                                <UserPlus className="h-3 w-3 text-green-500 flex-shrink-0" />
+                                                <div className="min-w-0">
+                                                  <div className="text-sm font-medium truncate">{custom.name}</div>
+                                                  <div className="text-xs text-muted-foreground truncate">{custom.email}</div>
+                                                </div>
+                                              </div>
+                                              <Button
+                                                variant="ghost"
+                                                size="sm"
+                                                className="h-6 w-6 p-0 flex-shrink-0"
+                                                onClick={() => removeCustomEmail(vendor.vendorId, custom.id)}
+                                              >
+                                                <X className="h-3 w-3" />
+                                              </Button>
+                                            </div>
+                                          ))}
+                                        </div>
+                                      </div>
+                                    )}
+                                  </div>
+                                </td>
+                              </tr>
+                            )}
+                          </React.Fragment>
+                        );
+                      })}
+                    </tbody>
+                  </table>
+                </div>
+              )}
+            </div>
+
+            <Separator />
 
             {/* 기본계약서 선택 */}
             <Card>
@@ -685,4 +1027,4 @@ export function BiddingInvitationDialog({
       </DialogContent>
     </Dialog>
   )
-}
+}
\ No newline at end of file
-- 
cgit v1.2.3

