summaryrefslogtreecommitdiff
path: root/components/bidding/manage/bidding-companies-editor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/bidding/manage/bidding-companies-editor.tsx')
-rw-r--r--components/bidding/manage/bidding-companies-editor.tsx803
1 files changed, 803 insertions, 0 deletions
diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx
new file mode 100644
index 00000000..1ce8b014
--- /dev/null
+++ b/components/bidding/manage/bidding-companies-editor.tsx
@@ -0,0 +1,803 @@
+'use client'
+
+import * as React from 'react'
+import { Building, User, Plus, Trash2 } from 'lucide-react'
+import { toast } from 'sonner'
+
+import { Button } from '@/components/ui/button'
+import {
+ getBiddingVendors,
+ getBiddingCompanyContacts,
+ createBiddingCompanyContact,
+ deleteBiddingCompanyContact,
+ getVendorContactsByVendorId,
+ updateBiddingCompanyPriceAdjustmentQuestion
+} from '@/lib/bidding/service'
+import { deleteBiddingCompany } from '@/lib/bidding/pre-quote/service'
+import { BiddingDetailVendorCreateDialog } from './bidding-detail-vendor-create-dialog'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Checkbox } from '@/components/ui/checkbox'
+import { Loader2 } from 'lucide-react'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+
+interface QuotationVendor {
+ id: number // biddingCompanies.id
+ companyId?: number // vendors.id (벤더 ID)
+ vendorName: string
+ vendorCode: string
+ contactPerson?: string
+ contactEmail?: string
+ contactPhone?: string
+ quotationAmount?: number
+ currency: string
+ invitationStatus: string
+ isPriceAdjustmentApplicableQuestion?: boolean
+}
+
+interface BiddingCompaniesEditorProps {
+ biddingId: number
+}
+
+interface VendorContact {
+ id: number
+ vendorId: number
+ contactName: string
+ contactPosition: string | null
+ contactDepartment: string | null
+ contactTask: string | null
+ contactEmail: string
+ contactPhone: string | null
+ isPrimary: boolean
+}
+
+interface BiddingCompanyContact {
+ id: number
+ biddingId: number
+ vendorId: number
+ contactName: string
+ contactEmail: string
+ contactNumber: string | null
+ createdAt: Date
+ updatedAt: Date
+}
+
+export function BiddingCompaniesEditor({ biddingId }: BiddingCompaniesEditorProps) {
+ const [vendors, setVendors] = React.useState<QuotationVendor[]>([])
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [addVendorDialogOpen, setAddVendorDialogOpen] = React.useState(false)
+ const [selectedVendor, setSelectedVendor] = React.useState<QuotationVendor | null>(null)
+ const [biddingCompanyContacts, setBiddingCompanyContacts] = React.useState<BiddingCompanyContact[]>([])
+ const [isLoadingContacts, setIsLoadingContacts] = React.useState(false)
+ // 각 업체별 첫 번째 담당자 정보 저장 (vendorId -> 첫 번째 담당자)
+ const [vendorFirstContacts, setVendorFirstContacts] = React.useState<Map<number, BiddingCompanyContact>>(new Map())
+
+ // 담당자 추가 다이얼로그
+ const [addContactDialogOpen, setAddContactDialogOpen] = React.useState(false)
+ const [newContact, setNewContact] = React.useState({
+ contactName: '',
+ contactEmail: '',
+ contactNumber: '',
+ })
+ const [addContactFromVendorDialogOpen, setAddContactFromVendorDialogOpen] = React.useState(false)
+ const [vendorContacts, setVendorContacts] = React.useState<VendorContact[]>([])
+ const [isLoadingVendorContacts, setIsLoadingVendorContacts] = React.useState(false)
+ const [selectedContactFromVendor, setSelectedContactFromVendor] = React.useState<VendorContact | null>(null)
+
+ // 업체 목록 다시 로딩 함수
+ const reloadVendors = React.useCallback(async () => {
+ try {
+ const result = await getBiddingVendors(biddingId)
+ if (result.success && result.data) {
+ const vendorsList = result.data.map(v => ({
+ ...v,
+ companyId: v.companyId || undefined,
+ vendorName: v.vendorName || '',
+ vendorCode: v.vendorCode || '',
+ contactPerson: v.contactPerson ?? undefined,
+ contactEmail: v.contactEmail ?? undefined,
+ contactPhone: v.contactPhone ?? undefined,
+ quotationAmount: v.quotationAmount ? parseFloat(v.quotationAmount) : undefined,
+ isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false,
+ }))
+ setVendors(vendorsList)
+
+ // 각 업체별 첫 번째 담당자 정보 로드
+ const firstContactsMap = new Map<number, BiddingCompanyContact>()
+ const contactPromises = vendorsList
+ .filter(v => v.companyId)
+ .map(async (vendor) => {
+ try {
+ const contactResult = await getBiddingCompanyContacts(biddingId, vendor.companyId!)
+ if (contactResult.success && contactResult.data && contactResult.data.length > 0) {
+ firstContactsMap.set(vendor.companyId!, contactResult.data[0])
+ }
+ } catch (error) {
+ console.error(`Failed to load contact for vendor ${vendor.companyId}:`, error)
+ }
+ })
+
+ await Promise.all(contactPromises)
+ setVendorFirstContacts(firstContactsMap)
+ }
+ } catch (error) {
+ console.error('Failed to reload vendors:', error)
+ }
+ }, [biddingId])
+
+ // 데이터 로딩
+ React.useEffect(() => {
+ const loadVendors = async () => {
+ setIsLoading(true)
+ try {
+ const result = await getBiddingVendors(biddingId)
+ if (result.success && result.data) {
+ const vendorsList = result.data.map(v => ({
+ id: v.id,
+ companyId: v.companyId || undefined,
+ vendorName: v.vendorName || '',
+ vendorCode: v.vendorCode || '',
+ contactPerson: v.contactPerson !== null ? v.contactPerson : undefined,
+ contactEmail: v.contactEmail !== null ? v.contactEmail : undefined,
+ contactPhone: v.contactPhone !== null ? v.contactPhone : undefined,
+ quotationAmount: v.quotationAmount ? parseFloat(v.quotationAmount) : undefined,
+ currency: v.currency || 'KRW',
+ invitationStatus: v.invitationStatus,
+ isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false,
+ }))
+ setVendors(vendorsList)
+
+ // 각 업체별 첫 번째 담당자 정보 로드
+ const firstContactsMap = new Map<number, BiddingCompanyContact>()
+ const contactPromises = vendorsList
+ .filter(v => v.companyId)
+ .map(async (vendor) => {
+ try {
+ const contactResult = await getBiddingCompanyContacts(biddingId, vendor.companyId!)
+ if (contactResult.success && contactResult.data && contactResult.data.length > 0) {
+ firstContactsMap.set(vendor.companyId!, contactResult.data[0])
+ }
+ } catch (error) {
+ console.error(`Failed to load contact for vendor ${vendor.companyId}:`, error)
+ }
+ })
+
+ await Promise.all(contactPromises)
+ setVendorFirstContacts(firstContactsMap)
+ } else {
+ toast.error(result.error || '업체 정보를 불러오는데 실패했습니다.')
+ setVendors([])
+ }
+ } catch (error) {
+ console.error('Failed to load vendors:', error)
+ toast.error('업체 정보를 불러오는데 실패했습니다.')
+ setVendors([])
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ loadVendors()
+ }, [biddingId])
+
+ // 업체 선택 핸들러 (단일 선택)
+ const handleVendorSelect = async (vendor: QuotationVendor) => {
+ // 이미 선택된 업체를 다시 클릭하면 선택 해제
+ if (selectedVendor?.id === vendor.id) {
+ setSelectedVendor(null)
+ setBiddingCompanyContacts([])
+ return
+ }
+
+ // 새 업체 선택
+ setSelectedVendor(vendor)
+
+ // 선택한 업체의 담당자 목록 로딩
+ if (vendor.companyId) {
+ setIsLoadingContacts(true)
+ try {
+ const result = await getBiddingCompanyContacts(biddingId, vendor.companyId)
+ if (result.success && result.data) {
+ setBiddingCompanyContacts(result.data)
+ } else {
+ toast.error(result.error || '담당자 목록을 불러오는데 실패했습니다.')
+ setBiddingCompanyContacts([])
+ }
+ } catch (error) {
+ console.error('Failed to load contacts:', error)
+ toast.error('담당자 목록을 불러오는데 실패했습니다.')
+ setBiddingCompanyContacts([])
+ } finally {
+ setIsLoadingContacts(false)
+ }
+ }
+ }
+
+ // 업체 삭제
+ const handleRemoveVendor = async (vendorId: number) => {
+ if (!confirm('정말로 이 업체를 삭제하시겠습니까?')) {
+ return
+ }
+
+ try {
+ const result = await deleteBiddingCompany(vendorId)
+ if (result.success) {
+ toast.success('업체가 삭제되었습니다.')
+ // 업체 목록 다시 로딩
+ await reloadVendors()
+ // 선택된 업체가 삭제된 경우 담당자 목록도 초기화
+ if (selectedVendor?.id === vendorId) {
+ setSelectedVendor(null)
+ setBiddingCompanyContacts([])
+ }
+ } else {
+ toast.error(result.error || '업체 삭제에 실패했습니다.')
+ }
+ } catch (error) {
+ console.error('Failed to remove vendor:', error)
+ toast.error('업체 삭제에 실패했습니다.')
+ }
+ }
+
+ // 담당자 추가 (직접 입력)
+ const handleAddContact = async () => {
+ if (!selectedVendor || !selectedVendor.companyId) {
+ toast.error('업체를 선택해주세요.')
+ return
+ }
+
+ if (!newContact.contactName || !newContact.contactEmail) {
+ toast.error('이름과 이메일은 필수입니다.')
+ return
+ }
+
+ try {
+ const result = await createBiddingCompanyContact(
+ biddingId,
+ selectedVendor.companyId,
+ {
+ contactName: newContact.contactName,
+ contactEmail: newContact.contactEmail,
+ contactNumber: newContact.contactNumber || undefined,
+ }
+ )
+
+ if (result.success) {
+ toast.success('담당자가 추가되었습니다.')
+ setAddContactDialogOpen(false)
+ setNewContact({ contactName: '', contactEmail: '', contactNumber: '' })
+
+ // 담당자 목록 새로고침
+ const contactsResult = await getBiddingCompanyContacts(biddingId, selectedVendor.companyId)
+ if (contactsResult.success && contactsResult.data) {
+ setBiddingCompanyContacts(contactsResult.data)
+ // 첫 번째 담당자 정보 업데이트
+ if (contactsResult.data.length > 0) {
+ setVendorFirstContacts(prev => {
+ const newMap = new Map(prev)
+ newMap.set(selectedVendor.companyId!, contactsResult.data[0])
+ return newMap
+ })
+ }
+ }
+ } else {
+ toast.error(result.error || '담당자 추가에 실패했습니다.')
+ }
+ } catch (error) {
+ console.error('Failed to add contact:', error)
+ toast.error('담당자 추가에 실패했습니다.')
+ }
+ }
+
+ // 담당자 추가 (벤더 목록에서 선택)
+ const handleOpenAddContactFromVendor = async () => {
+ if (!selectedVendor || !selectedVendor.companyId) {
+ toast.error('업체를 선택해주세요.')
+ return
+ }
+
+ setIsLoadingVendorContacts(true)
+ setAddContactFromVendorDialogOpen(true)
+ setSelectedContactFromVendor(null)
+
+ try {
+ const result = await getVendorContactsByVendorId(selectedVendor.companyId)
+ if (result.success && result.data) {
+ setVendorContacts(result.data)
+ } else {
+ toast.error(result.error || '벤더 담당자 목록을 불러오는데 실패했습니다.')
+ setVendorContacts([])
+ }
+ } catch (error) {
+ console.error('Failed to load vendor contacts:', error)
+ toast.error('벤더 담당자 목록을 불러오는데 실패했습니다.')
+ setVendorContacts([])
+ } finally {
+ setIsLoadingVendorContacts(false)
+ }
+ }
+
+ // 벤더 담당자 선택 후 저장
+ const handleAddContactFromVendor = async () => {
+ if (!selectedContactFromVendor || !selectedVendor || !selectedVendor.companyId) {
+ toast.error('담당자를 선택해주세요.')
+ return
+ }
+
+ try {
+ const result = await createBiddingCompanyContact(
+ biddingId,
+ selectedVendor.companyId,
+ {
+ contactName: selectedContactFromVendor.contactName,
+ contactEmail: selectedContactFromVendor.contactEmail,
+ contactNumber: selectedContactFromVendor.contactPhone || undefined,
+ }
+ )
+
+ if (result.success) {
+ toast.success('담당자가 추가되었습니다.')
+ setAddContactFromVendorDialogOpen(false)
+ setSelectedContactFromVendor(null)
+
+ // 담당자 목록 새로고침
+ const contactsResult = await getBiddingCompanyContacts(biddingId, selectedVendor.companyId)
+ if (contactsResult.success && contactsResult.data) {
+ setBiddingCompanyContacts(contactsResult.data)
+ // 첫 번째 담당자 정보 업데이트
+ if (contactsResult.data.length > 0) {
+ setVendorFirstContacts(prev => {
+ const newMap = new Map(prev)
+ newMap.set(selectedVendor.companyId!, contactsResult.data[0])
+ return newMap
+ })
+ }
+ }
+ } else {
+ toast.error(result.error || '담당자 추가에 실패했습니다.')
+ }
+ } catch (error) {
+ console.error('Failed to add contact:', error)
+ toast.error('담당자 추가에 실패했습니다.')
+ }
+ }
+
+ // 담당자 삭제
+ const handleDeleteContact = async (contactId: number) => {
+ if (!confirm('정말로 이 담당자를 삭제하시겠습니까?')) {
+ return
+ }
+
+ try {
+ const result = await deleteBiddingCompanyContact(contactId)
+ if (result.success) {
+ toast.success('담당자가 삭제되었습니다.')
+
+ // 담당자 목록 새로고침
+ if (selectedVendor && selectedVendor.companyId) {
+ const contactsResult = await getBiddingCompanyContacts(biddingId, selectedVendor.companyId)
+ if (contactsResult.success && contactsResult.data) {
+ setBiddingCompanyContacts(contactsResult.data)
+ // 첫 번째 담당자 정보 업데이트
+ if (contactsResult.data.length > 0) {
+ setVendorFirstContacts(prev => {
+ const newMap = new Map(prev)
+ newMap.set(selectedVendor.companyId!, contactsResult.data[0])
+ return newMap
+ })
+ } else {
+ // 담당자가 없으면 Map에서 제거
+ setVendorFirstContacts(prev => {
+ const newMap = new Map(prev)
+ newMap.delete(selectedVendor.companyId!)
+ return newMap
+ })
+ }
+ }
+ }
+ } else {
+ toast.error(result.error || '담당자 삭제에 실패했습니다.')
+ }
+ } catch (error) {
+ console.error('Failed to delete contact:', error)
+ toast.error('담당자 삭제에 실패했습니다.')
+ }
+ }
+
+ // 연동제 적용요건 문의 체크박스 변경
+ const handleTogglePriceAdjustmentQuestion = async (vendorId: number, checked: boolean) => {
+ try {
+ const result = await updateBiddingCompanyPriceAdjustmentQuestion(vendorId, checked)
+ if (result.success) {
+ // 로컬 상태 업데이트
+ setVendors(prev => prev.map(v =>
+ v.id === vendorId
+ ? { ...v, isPriceAdjustmentApplicableQuestion: checked }
+ : v
+ ))
+
+ // 선택된 업체 정보도 업데이트
+ if (selectedVendor?.id === vendorId) {
+ setSelectedVendor(prev => prev ? { ...prev, isPriceAdjustmentApplicableQuestion: checked } : null)
+ }
+
+ // 담당자 목록 새로고침 (첫 번째 담당자 정보 업데이트를 위해)
+ if (selectedVendor && selectedVendor.companyId) {
+ const contactsResult = await getBiddingCompanyContacts(biddingId, selectedVendor.companyId)
+ if (contactsResult.success && contactsResult.data) {
+ setBiddingCompanyContacts(contactsResult.data)
+ if (contactsResult.data.length > 0) {
+ setVendorFirstContacts(prev => {
+ const newMap = new Map(prev)
+ newMap.set(selectedVendor.companyId!, contactsResult.data[0])
+ return newMap
+ })
+ }
+ }
+ }
+ } else {
+ toast.error(result.error || '연동제 적용요건 문의 여부 업데이트에 실패했습니다.')
+ }
+ } catch (error) {
+ console.error('Failed to update price adjustment question:', error)
+ toast.error('연동제 적용요건 문의 여부 업데이트에 실패했습니다.')
+ }
+ }
+
+ if (isLoading) {
+ return (
+ <div className="flex items-center justify-center p-8">
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
+ <span className="ml-2">업체 정보를 불러오는 중...</span>
+ </div>
+ )
+ }
+
+ return (
+ <div className="space-y-6">
+ {/* 참여 업체 목록 테이블 */}
+ <Card>
+ <CardHeader className="flex flex-row items-center justify-between">
+ <div>
+ <CardTitle className="flex items-center gap-2">
+ <Building className="h-5 w-5" />
+ 참여 업체 목록
+ </CardTitle>
+ <p className="text-sm text-muted-foreground mt-1">
+ 입찰에 참여하는 업체들을 관리합니다. 업체를 선택하면 하단에 담당자 목록이 표시됩니다.
+ </p>
+ </div>
+ <Button onClick={() => setAddVendorDialogOpen(true)} className="flex items-center gap-2">
+ <Plus className="h-4 w-4" />
+ 업체 추가
+ </Button>
+ </CardHeader>
+ <CardContent>
+ {vendors.length === 0 ? (
+ <div className="text-center py-8 text-muted-foreground">
+ 참여 업체가 없습니다. 업체를 추가해주세요.
+ </div>
+ ) : (
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead className="w-[50px]">선택</TableHead>
+ <TableHead>업체명</TableHead>
+ <TableHead>업체코드</TableHead>
+ <TableHead>담당자 이름</TableHead>
+ <TableHead>담당자 이메일</TableHead>
+ <TableHead>담당자 연락처</TableHead>
+ <TableHead>상태</TableHead>
+ <TableHead className="w-[180px]">연동제 적용요건 문의</TableHead>
+ <TableHead className="w-[100px]">작업</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {vendors.map((vendor) => (
+ <TableRow
+ key={vendor.id}
+ className={`cursor-pointer hover:bg-muted/50 ${selectedVendor?.id === vendor.id ? 'bg-muted/50' : ''}`}
+ onClick={() => handleVendorSelect(vendor)}
+ >
+ <TableCell onClick={(e) => e.stopPropagation()}>
+ <Checkbox
+ checked={selectedVendor?.id === vendor.id}
+ onCheckedChange={() => handleVendorSelect(vendor)}
+ />
+ </TableCell>
+ <TableCell className="font-medium">{vendor.vendorName}</TableCell>
+ <TableCell>{vendor.vendorCode}</TableCell>
+ <TableCell>
+ {vendor.companyId && vendorFirstContacts.has(vendor.companyId)
+ ? vendorFirstContacts.get(vendor.companyId)!.contactName
+ : '-'}
+ </TableCell>
+ <TableCell>
+ {vendor.companyId && vendorFirstContacts.has(vendor.companyId)
+ ? vendorFirstContacts.get(vendor.companyId)!.contactEmail
+ : '-'}
+ </TableCell>
+ <TableCell>
+ {vendor.companyId && vendorFirstContacts.has(vendor.companyId)
+ ? vendorFirstContacts.get(vendor.companyId)!.contactNumber || '-'
+ : '-'}
+ </TableCell>
+
+ <TableCell>
+ <span className="px-2 py-1 rounded-full text-xs bg-blue-100 text-blue-800">
+ {vendor.invitationStatus}
+ </span>
+ </TableCell>
+ <TableCell>
+ <div className="flex items-center gap-2">
+ <Checkbox
+ checked={vendor.isPriceAdjustmentApplicableQuestion || false}
+ onCheckedChange={(checked) =>
+ handleTogglePriceAdjustmentQuestion(vendor.id, checked as boolean)
+ }
+ />
+ <span className="text-sm text-muted-foreground">
+ {vendor.isPriceAdjustmentApplicableQuestion ? '예' : '아니오'}
+ </span>
+ </div>
+ </TableCell>
+ <TableCell>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => handleRemoveVendor(vendor.id)}
+ className="text-red-600 hover:text-red-800"
+ >
+ <Trash2 className="h-4 w-4" />
+ </Button>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ )}
+ </CardContent>
+ </Card>
+
+ {/* 선택한 업체의 담당자 목록 테이블 */}
+ {selectedVendor && (
+ <Card>
+ <CardHeader className="flex flex-row items-center justify-between">
+ <div>
+ <CardTitle className="flex items-center gap-2">
+ <User className="h-5 w-5" />
+ {selectedVendor.vendorName} 담당자 목록
+ </CardTitle>
+ <p className="text-sm text-muted-foreground mt-1">
+ 선택한 업체의 선정된 담당자를 관리합니다.
+ </p>
+ </div>
+ <div className="flex gap-2">
+ <Button
+ variant="outline"
+ onClick={handleOpenAddContactFromVendor}
+ className="flex items-center gap-2"
+ >
+ <User className="h-4 w-4" />
+ 업체 담당자 추가
+ </Button>
+ <Button
+ onClick={() => setAddContactDialogOpen(true)}
+ className="flex items-center gap-2"
+ >
+ <Plus className="h-4 w-4" />
+ 담당자 수기 입력
+ </Button>
+ </div>
+ </CardHeader>
+ <CardContent>
+ {isLoadingContacts ? (
+ <div className="flex items-center justify-center py-8">
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+ <span className="text-sm text-muted-foreground">담당자 목록을 불러오는 중...</span>
+ </div>
+ ) : biddingCompanyContacts.length === 0 ? (
+ <div className="text-center py-8 text-muted-foreground">
+ 등록된 담당자가 없습니다. 담당자를 추가해주세요.
+ </div>
+ ) : (
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>이름</TableHead>
+ <TableHead>이메일</TableHead>
+ <TableHead>전화번호</TableHead>
+ <TableHead className="w-[100px]">작업</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {biddingCompanyContacts.map((biddingCompanyContact) => (
+ <TableRow key={biddingCompanyContact.id}>
+ <TableCell className="font-medium">{biddingCompanyContact.contactName}</TableCell>
+ <TableCell>{biddingCompanyContact.contactEmail}</TableCell>
+ <TableCell>{biddingCompanyContact.contactNumber || '-'}</TableCell>
+ <TableCell>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => handleDeleteContact(biddingCompanyContact.id)}
+ className="text-red-600 hover:text-red-800"
+ >
+ <Trash2 className="h-4 w-4" />
+ </Button>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ )}
+ </CardContent>
+ </Card>
+ )}
+
+ {/* 업체 추가 다이얼로그 */}
+ <BiddingDetailVendorCreateDialog
+ biddingId={biddingId}
+ open={addVendorDialogOpen}
+ onOpenChange={setAddVendorDialogOpen}
+ onSuccess={reloadVendors}
+ />
+
+ {/* 담당자 추가 다이얼로그 (직접 입력) */}
+ <Dialog open={addContactDialogOpen} onOpenChange={setAddContactDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>담당자 추가</DialogTitle>
+ <DialogDescription>
+ 새로운 담당자 정보를 입력하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4 py-4">
+ <div className="space-y-2">
+ <Label htmlFor="contactName">이름 *</Label>
+ <Input
+ id="contactName"
+ value={newContact.contactName}
+ onChange={(e) => setNewContact(prev => ({ ...prev, contactName: e.target.value }))}
+ placeholder="담당자 이름"
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="contactEmail">이메일 *</Label>
+ <Input
+ id="contactEmail"
+ type="email"
+ value={newContact.contactEmail}
+ onChange={(e) => setNewContact(prev => ({ ...prev, contactEmail: e.target.value }))}
+ placeholder="example@email.com"
+ />
+ </div>
+ <div className="space-y-2">
+ <Label htmlFor="contactNumber">전화번호</Label>
+ <Input
+ id="contactNumber"
+ value={newContact.contactNumber}
+ onChange={(e) => setNewContact(prev => ({ ...prev, contactNumber: e.target.value }))}
+ placeholder="010-1234-5678"
+ />
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => {
+ setAddContactDialogOpen(false)
+ setNewContact({ contactName: '', contactEmail: '', contactNumber: '' })
+ }}
+ >
+ 취소
+ </Button>
+ <Button onClick={handleAddContact}>
+ 추가
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+
+ {/* 벤더 담당자에서 추가 다이얼로그 */}
+ <Dialog open={addContactFromVendorDialogOpen} onOpenChange={setAddContactFromVendorDialogOpen}>
+ <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>
+ {selectedVendor ? `${selectedVendor.vendorName} 벤더 담당자에서 선택` : '벤더 담당자 선택'}
+ </DialogTitle>
+ <DialogDescription>
+ 벤더에 등록된 담당자 목록에서 선택하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="space-y-4 py-4">
+ {isLoadingVendorContacts ? (
+ <div className="flex items-center justify-center py-8">
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
+ <span className="text-sm text-muted-foreground">담당자 목록을 불러오는 중...</span>
+ </div>
+ ) : vendorContacts.length === 0 ? (
+ <div className="text-center py-8 text-muted-foreground">
+ 등록된 담당자가 없습니다.
+ </div>
+ ) : (
+ <div className="space-y-2">
+ {vendorContacts.map((contact) => (
+ <div
+ key={contact.id}
+ className={`flex items-center justify-between p-4 border rounded-lg cursor-pointer hover:bg-muted/50 transition-colors ${
+ selectedContactFromVendor?.id === contact.id ? 'bg-primary/10 border-primary' : ''
+ }`}
+ onClick={() => setSelectedContactFromVendor(contact)}
+ >
+ <div className="flex items-center gap-3 flex-1">
+ <Checkbox
+ checked={selectedContactFromVendor?.id === contact.id}
+ onCheckedChange={() => setSelectedContactFromVendor(contact)}
+ className="shrink-0"
+ />
+ <div className="flex-1 min-w-0">
+ <div className="flex items-center gap-2">
+ <span className="font-medium">{contact.contactName}</span>
+ {contact.isPrimary && (
+ <span className="px-2 py-0.5 rounded-full text-xs bg-primary/10 text-primary">
+ 주담당자
+ </span>
+ )}
+ </div>
+ {contact.contactPosition && (
+ <p className="text-sm text-muted-foreground">{contact.contactPosition}</p>
+ )}
+ <div className="flex items-center gap-4 mt-1 text-sm text-muted-foreground">
+ <span>{contact.contactEmail}</span>
+ {contact.contactPhone && <span>{contact.contactPhone}</span>}
+ </div>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => {
+ setAddContactFromVendorDialogOpen(false)
+ setSelectedContactFromVendor(null)
+ }}
+ >
+ 취소
+ </Button>
+ <Button
+ onClick={handleAddContactFromVendor}
+ disabled={!selectedContactFromVendor || isLoadingVendorContacts}
+ >
+ 추가
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </div>
+ )
+} \ No newline at end of file