diff options
| -rw-r--r-- | components/login/login-form.tsx | 38 | ||||
| -rw-r--r-- | components/login/partner-auth-form.tsx | 2 | ||||
| -rw-r--r-- | config/language.ts | 4 | ||||
| -rw-r--r-- | lib/bidding/detail/service.ts | 22 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx | 298 | ||||
| -rw-r--r-- | lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx | 4 | ||||
| -rw-r--r-- | lib/bidding/pre-quote/table/bidding-pre-quote-vendor-create-dialog.tsx | 302 | ||||
| -rw-r--r-- | lib/vendor-basic-info/basic-info-client.tsx | 55 | ||||
| -rw-r--r-- | lib/vendor-basic-info/types.ts | 1 | ||||
| -rw-r--r-- | lib/vendors/service.ts | 1 |
10 files changed, 494 insertions, 233 deletions
diff --git a/components/login/login-form.tsx b/components/login/login-form.tsx index 67d9f8ac..8e9509c8 100644 --- a/components/login/login-form.tsx +++ b/components/login/login-form.tsx @@ -66,6 +66,13 @@ export function LoginForm() { message: undefined, }); + // // 영문 페이지에서 S-Gips 로그인 비활성화 시 기본 로그인 방법 설정 + // useEffect(() => { + // if (lng === 'en' && loginMethod === 'sgips') { + // setLoginMethod('username'); + // } + // }, [lng, loginMethod]); + // 이미 로그인된 사용자 리다이렉트 처리 useEffect(() => { if (status === 'authenticated' && session?.user) { @@ -510,18 +517,21 @@ export function LoginForm() { > {t('generalLogin')} </button> - <button - type="button" - onClick={() => setLoginMethod('sgips')} - className={cn( - "flex-1 rounded-md px-3 py-2 text-sm font-medium transition-all", - loginMethod === 'sgips' - ? "bg-background text-foreground shadow-sm" - : "text-muted-foreground hover:text-foreground" - )} - > - {t('sgipsLogin')} - </button> + {/* S-Gips 로그인은 영문 페이지에서 비활성화 0925 구매 요청사항*/} + {lng !== 'en' && ( + <button + type="button" + onClick={() => setLoginMethod('sgips')} + className={cn( + "flex-1 rounded-md px-3 py-2 text-sm font-medium transition-all", + loginMethod === 'sgips' + ? "bg-background text-foreground shadow-sm" + : "text-muted-foreground hover:text-foreground" + )} + > + {t('sgipsLogin')} + </button> + )} </div> {/* Username Login Form */} @@ -562,8 +572,8 @@ export function LoginForm() { </form> )} - {/* S-Gips Login Form */} - {loginMethod === 'sgips' && ( + {/* S-Gips Login Form - 영문 페이지에서 비활성화 0925 구매 요청사항*/} + {loginMethod === 'sgips' && lng !== 'en' && ( <form onSubmit={handleSgipsLogin} className="grid gap-4"> <div className="grid gap-2"> <Input diff --git a/components/login/partner-auth-form.tsx b/components/login/partner-auth-form.tsx index 10efaec5..56b1533d 100644 --- a/components/login/partner-auth-form.tsx +++ b/components/login/partner-auth-form.tsx @@ -62,8 +62,6 @@ export function CompanyAuthForm({ className, ...props }: React.HTMLAttributes<HT const currentLanguageText = i18n.language === "ko" ? t("languages.korean") - : i18n.language === "ja" - ? t("languages.japanese") : t("languages.english") // 로그인 페이지로 이동 diff --git a/config/language.ts b/config/language.ts index c4095801..e2561d58 100644 --- a/config/language.ts +++ b/config/language.ts @@ -1,13 +1,13 @@ export const languages = [ { value: "ko", labelKey: "languages.korean" }, { value: "en", labelKey: "languages.english" }, - { value: "ja", labelKey: "languages.japanese" }, + // { value: "ja", labelKey: "languages.japanese" }, ] export const LOCALE_MAP: Record<string, string> = { en: "en-US", ko: "ko-KR", - ja: "ja-JP", + // ja: "ja-JP", // 필요하면 더 추가... }
\ No newline at end of file diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index 645ebeac..404bc3cd 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -475,6 +475,7 @@ export async function calculateAndUpdateTargetPrice( biddingId: number ) { try { + // 입찰 정보 조회 const bidding = await getBiddingById(biddingId) if (!bidding) { @@ -512,17 +513,16 @@ export async function calculateAndUpdateTargetPrice( const updateResult = await updateTargetPrice(biddingId, targetPrice, criteria) if (updateResult.success) { - // 내정가 산정 후 입찰 상태를 set_target_price로 변경 (received_quotation 상태에서만) - await db - .update(biddings) - .set({ - status: 'set_target_price', - updatedAt: new Date() - }) - .where(and( - eq(biddings.id, biddingId), - eq(biddings.status, 'received_quotation') - )) + // // 내정가 산정 후 입찰 상태를 set_target_price로 변경 (received_quotation 상태에서만) + // await db + // .update(biddings) + // .set({ + // status: 'set_target_price', + // updatedAt: new Date() + // }) + // .where(and( + // eq(biddings.id, biddingId) + // )) // 캐시 무효화 revalidateTag(`bidding-${biddingId}`) diff --git a/lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx b/lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx index f35957bc..c1471a69 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx @@ -27,18 +27,21 @@ import { CommandGroup, CommandInput, CommandItem, + CommandList, } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover' -import { Check, ChevronsUpDown, Search } from 'lucide-react' +import { Check, ChevronsUpDown, Search, Loader2, X, Plus } from 'lucide-react' import { cn } from '@/lib/utils' import { createBiddingDetailVendor } from '@/lib/bidding/detail/service' import { searchVendorsForBidding } from '@/lib/bidding/service' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' +import { Badge } from '@/components/ui/badge' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' interface BiddingDetailVendorCreateDialogProps { biddingId: number @@ -64,45 +67,67 @@ export function BiddingDetailVendorCreateDialog({ const [isPending, startTransition] = useTransition() // Vendor 검색 상태 - const [vendors, setVendors] = React.useState<Vendor[]>([]) - const [selectedVendor, setSelectedVendor] = React.useState<Vendor | null>(null) - const [vendorSearchOpen, setVendorSearchOpen] = React.useState(false) - const [vendorSearchValue, setVendorSearchValue] = React.useState('') + const [vendorList, setVendorList] = React.useState<Vendor[]>([]) + const [selectedVendors, setSelectedVendors] = React.useState<Vendor[]>([]) + const [vendorOpen, setVendorOpen] = React.useState(false) // 폼 상태 (간소화 - 필수 항목만) const [formData, setFormData] = React.useState({ awardRatio: 100, // 기본 100% }) - // Vendor 검색 + // 벤더 로드 + const loadVendors = React.useCallback(async () => { + try { + const result = await searchVendorsForBidding('', biddingId, 50) // 빈 검색어로 모든 벤더 로드 + setVendorList(result || []) + } catch (error) { + console.error('Failed to load vendors:', error) + toast({ + title: '오류', + description: '벤더 목록을 불러오는데 실패했습니다.', + variant: 'destructive', + }) + setVendorList([]) + } + }, [biddingId]) + React.useEffect(() => { - const search = async () => { - if (vendorSearchValue.trim().length < 2) { - setVendors([]) - return - } + if (open) { + loadVendors() + } + }, [open, loadVendors]) - try { - const result = await searchVendorsForBidding(vendorSearchValue.trim(), biddingId, 10) - setVendors(result) - } catch (error) { - console.error('Vendor search failed:', error) - setVendors([]) - } + // 초기화 + React.useEffect(() => { + if (!open) { + setSelectedVendors([]) + setFormData({ + awardRatio: 100, // 기본 100% + }) } + }, [open]) - const debounceTimer = setTimeout(search, 300) - return () => clearTimeout(debounceTimer) - }, [vendorSearchValue]) + // 벤더 추가 + const handleAddVendor = (vendor: Vendor) => { + if (!selectedVendors.find(v => v.id === vendor.id)) { + setSelectedVendors([...selectedVendors, vendor]) + } + setVendorOpen(false) + } - const handleVendorSelect = (vendor: Vendor) => { - setSelectedVendor(vendor) - setVendorSearchValue(`${vendor.vendorName} (${vendor.vendorCode})`) - setVendorSearchOpen(false) + // 벤더 제거 + const handleRemoveVendor = (vendorId: number) => { + setSelectedVendors(selectedVendors.filter(v => v.id !== vendorId)) + } + + // 이미 선택된 벤더인지 확인 + const isVendorSelected = (vendorId: number) => { + return selectedVendors.some(v => v.id === vendorId) } const handleCreate = () => { - if (!selectedVendor) { + if (selectedVendors.length === 0) { toast({ title: '오류', description: '업체를 선택해주세요.', @@ -111,25 +136,41 @@ export function BiddingDetailVendorCreateDialog({ return } - startTransition(async () => { - const response = await createBiddingDetailVendor( - biddingId, - selectedVendor.id - ) + let successCount = 0 + let errorMessages: string[] = [] + + for (const vendor of selectedVendors) { + try { + const response = await createBiddingDetailVendor( + biddingId, + vendor.id + ) + + if (response.success) { + successCount++ + } else { + errorMessages.push(`${vendor.vendorName}: ${response.error}`) + } + } catch (error) { + errorMessages.push(`${vendor.vendorName}: 처리 중 오류가 발생했습니다.`) + } + } - if (response.success) { + if (successCount > 0) { toast({ title: '성공', - description: response.message, + description: `${successCount}개의 업체가 성공적으로 추가되었습니다.${errorMessages.length > 0 ? ` ${errorMessages.length}개는 실패했습니다.` : ''}`, }) onOpenChange(false) resetForm() onSuccess() - } else { + } + + if (errorMessages.length > 0 && successCount === 0) { toast({ title: '오류', - description: response.error, + description: `업체 추가에 실패했습니다: ${errorMessages.join(', ')}`, variant: 'destructive', }) } @@ -137,8 +178,7 @@ export function BiddingDetailVendorCreateDialog({ } const resetForm = () => { - setSelectedVendor(null) - setVendorSearchValue('') + setSelectedVendors([]) setFormData({ awardRatio: 100, // 기본 100% }) @@ -146,74 +186,140 @@ export function BiddingDetailVendorCreateDialog({ return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="sm:max-w-[600px]"> - <DialogHeader> + <DialogContent className="max-w-4xl max-h-[90vh] p-0 flex flex-col"> + {/* 헤더 */} + <DialogHeader className="p-6 pb-0"> <DialogTitle>협력업체 추가</DialogTitle> <DialogDescription> - 검색해서 업체를 선택하고 견적 정보를 입력해주세요. + 입찰에 참여할 업체를 선택하세요. 여러 개 선택 가능합니다. </DialogDescription> </DialogHeader> - <div className="grid gap-4 py-4"> - {/* Vendor 검색 */} - <div className="space-y-2"> - <Label htmlFor="vendor-search">업체 검색</Label> - <Popover open={vendorSearchOpen} onOpenChange={setVendorSearchOpen}> - <PopoverTrigger asChild> - <Button - variant="outline" - role="combobox" - aria-expanded={vendorSearchOpen} - className="w-full justify-between" - > - {selectedVendor - ? `${selectedVendor.vendorName} (${selectedVendor.vendorCode})` - : "업체를 검색해서 선택하세요..."} - <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-full p-0"> - <Command> - <CommandInput - placeholder="업체명 또는 코드를 입력하세요..." - value={vendorSearchValue} - onValueChange={setVendorSearchValue} - /> - <CommandEmpty> - {vendorSearchValue.length < 2 - ? "최소 2자 이상 입력해주세요" - : "검색 결과가 없습니다"} - </CommandEmpty> - <CommandGroup className="max-h-64 overflow-auto"> - {vendors.map((vendor) => ( - <CommandItem - key={vendor.id} - value={`${vendor.vendorName} ${vendor.vendorCode}`} - onSelect={() => handleVendorSelect(vendor)} + + {/* 메인 컨텐츠 */} + <div className="flex-1 px-6 py-4 overflow-y-auto"> + <div className="space-y-6"> + {/* 업체 선택 카드 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">업체 선택</CardTitle> + <CardDescription> + 입찰에 참여할 협력업체를 선택하세요. + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-4"> + {/* 업체 추가 버튼 */} + <Popover open={vendorOpen} onOpenChange={setVendorOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={vendorOpen} + className="w-full justify-between" + disabled={vendorList.length === 0} > - <Check - className={cn( - "mr-2 h-4 w-4", - selectedVendor?.id === vendor.id ? "opacity-100" : "opacity-0" - )} - /> - <div className="flex flex-col"> - <span className="font-medium">{vendor.vendorName}</span> - <span className="text-sm text-muted-foreground">{vendor.vendorCode}</span> - </div> - </CommandItem> - ))} - </CommandGroup> - </Command> - </PopoverContent> - </Popover> + <span className="flex items-center gap-2"> + <Plus className="h-4 w-4" /> + 업체 선택하기 + </span> + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[500px] p-0" align="start"> + <Command> + <CommandInput placeholder="업체명 또는 코드로 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {vendorList + .filter(vendor => !isVendorSelected(vendor.id)) + .map((vendor) => ( + <CommandItem + key={vendor.id} + value={`${vendor.vendorCode} ${vendor.vendorName}`} + onSelect={() => handleAddVendor(vendor)} + > + <div className="flex items-center gap-2 w-full"> + <Badge variant="outline" className="shrink-0"> + {vendor.vendorCode} + </Badge> + <span className="truncate">{vendor.vendorName}</span> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + + {/* 선택된 업체 목록 */} + {selectedVendors.length > 0 && ( + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <h4 className="text-sm font-medium">선택된 업체 ({selectedVendors.length}개)</h4> + </div> + <div className="space-y-2"> + {selectedVendors.map((vendor, index) => ( + <div + key={vendor.id} + className="flex items-center justify-between p-3 rounded-lg bg-secondary/50" + > + <div className="flex items-center gap-3"> + <span className="text-sm text-muted-foreground"> + {index + 1}. + </span> + <Badge variant="outline"> + {vendor.vendorCode} + </Badge> + <span className="text-sm font-medium"> + {vendor.vendorName} + </span> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveVendor(vendor.id)} + className="h-8 w-8 p-0" + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </div> + )} + + {selectedVendors.length === 0 && ( + <div className="text-center py-8 text-muted-foreground"> + <p className="text-sm">아직 선택된 업체가 없습니다.</p> + <p className="text-xs mt-1">위 버튼을 클릭하여 업체를 추가하세요.</p> + </div> + )} + </div> + </CardContent> + </Card> </div> </div> - <DialogFooter> - <Button variant="outline" onClick={() => onOpenChange(false)}> + + {/* 푸터 */} + <DialogFooter className="p-6 pt-0 border-t"> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isPending} + > 취소 </Button> - <Button onClick={handleCreate} disabled={isPending || !selectedVendor}> - 추가 + <Button + onClick={handleCreate} + disabled={isPending || selectedVendors.length === 0} + > + {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {selectedVendors.length > 0 + ? `${selectedVendors.length}개 업체 추가` + : '업체 추가' + } </Button> </DialogFooter> </DialogContent> diff --git a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx index 5d1bfde7..4655ed9f 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx @@ -63,10 +63,10 @@ export function BiddingDetailVendorToolbarActions({ } const handleRegister = () => { - if (bidding.status !== 'set_target_price') { + if (!bidding.targetPrice) { toast({ title: '오류', - description: '내정가 산정이 완료되어야 입찰 등록을 할 수 있습니다.', + description: '내정가가 산정되어야 입찰 초대를 할 수 있습니다.', variant: 'destructive', }) return diff --git a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-create-dialog.tsx b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-create-dialog.tsx index bc233e77..9ca7deb6 100644 --- a/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-create-dialog.tsx +++ b/lib/bidding/pre-quote/table/bidding-pre-quote-vendor-create-dialog.tsx @@ -17,18 +17,24 @@ import { CommandGroup, CommandInput, CommandItem, + CommandList, } from '@/components/ui/command' import { Popover, PopoverContent, - PopoverTrigger, + PopoverTrigger } from '@/components/ui/popover' -import { Check, ChevronsUpDown } from 'lucide-react' +import { Check, ChevronsUpDown, Loader2, X, Plus, Search } from 'lucide-react' import { cn } from '@/lib/utils' import { createBiddingCompany } from '@/lib/bidding/pre-quote/service' import { searchVendorsForBidding } from '@/lib/bidding/service' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' +import { Badge } from '@/components/ui/badge' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Alert, AlertDescription } from '@/components/ui/alert' +import { Info } from 'lucide-react' interface BiddingPreQuoteVendorCreateDialogProps { biddingId: number @@ -54,41 +60,60 @@ export function BiddingPreQuoteVendorCreateDialog({ const [isPending, startTransition] = useTransition() // Vendor 검색 상태 - const [vendors, setVendors] = React.useState<Vendor[]>([]) - const [selectedVendor, setSelectedVendor] = React.useState<Vendor | null>(null) - const [vendorSearchOpen, setVendorSearchOpen] = React.useState(false) - const [vendorSearchValue, setVendorSearchValue] = React.useState('') + const [vendorList, setVendorList] = React.useState<Vendor[]>([]) + const [selectedVendors, setSelectedVendors] = React.useState<Vendor[]>([]) + const [vendorOpen, setVendorOpen] = React.useState(false) - // Vendor 검색 + // 벤더 로드 + const loadVendors = React.useCallback(async () => { + try { + const result = await searchVendorsForBidding('', biddingId, 50) // 빈 검색어로 모든 벤더 로드 + setVendorList(result || []) + } catch (error) { + console.error('Failed to load vendors:', error) + toast({ + title: '오류', + description: '벤더 목록을 불러오는데 실패했습니다.', + variant: 'destructive', + }) + setVendorList([]) + } + }, [biddingId]) + React.useEffect(() => { - const search = async () => { - if (vendorSearchValue.trim().length < 2) { - setVendors([]) - return - } + if (open) { + loadVendors() + } + }, [open, loadVendors]) - try { - const result = await searchVendorsForBidding(vendorSearchValue.trim(), biddingId, 10) - setVendors(result) - } catch (error) { - console.error('Vendor search failed:', error) - setVendors([]) - } + // 초기화 + React.useEffect(() => { + if (!open) { + setSelectedVendors([]) } + }, [open]) + + // 벤더 추가 + const handleAddVendor = (vendor: Vendor) => { + if (!selectedVendors.find(v => v.id === vendor.id)) { + setSelectedVendors([...selectedVendors, vendor]) + } + setVendorOpen(false) + } - const debounceTimer = setTimeout(search, 300) - return () => clearTimeout(debounceTimer) - }, [vendorSearchValue]) + // 벤더 제거 + const handleRemoveVendor = (vendorId: number) => { + setSelectedVendors(selectedVendors.filter(v => v.id !== vendorId)) + } - const handleVendorSelect = (vendor: Vendor) => { - setSelectedVendor(vendor) - setVendorSearchValue(`${vendor.vendorName} (${vendor.vendorCode})`) - setVendorSearchOpen(false) + // 이미 선택된 벤더인지 확인 + const isVendorSelected = (vendorId: number) => { + return selectedVendors.some(v => v.id === vendorId) } const handleCreate = () => { - if (!selectedVendor) { + if (selectedVendors.length === 0) { toast({ title: '오류', description: '업체를 선택해주세요.', @@ -98,23 +123,40 @@ export function BiddingPreQuoteVendorCreateDialog({ } startTransition(async () => { - const response = await createBiddingCompany({ - biddingId, - companyId: selectedVendor.id, - }) - console.log(response) - if (response.success) { + let successCount = 0 + let errorMessages: string[] = [] + + for (const vendor of selectedVendors) { + try { + const response = await createBiddingCompany({ + biddingId, + companyId: vendor.id, + }) + + if (response.success) { + successCount++ + } else { + errorMessages.push(`${vendor.vendorName}: ${response.error}`) + } + } catch (error) { + errorMessages.push(`${vendor.vendorName}: 처리 중 오류가 발생했습니다.`) + } + } + + if (successCount > 0) { toast({ title: '성공', - description: response.message, + description: `${successCount}개의 업체가 성공적으로 추가되었습니다.${errorMessages.length > 0 ? ` ${errorMessages.length}개는 실패했습니다.` : ''}`, }) onOpenChange(false) resetForm() onSuccess() - } else { + } + + if (errorMessages.length > 0 && successCount === 0) { toast({ title: '오류', - description: response.error, + description: `업체 추가에 실패했습니다: ${errorMessages.join(', ')}`, variant: 'destructive', }) } @@ -122,81 +164,145 @@ export function BiddingPreQuoteVendorCreateDialog({ } const resetForm = () => { - setSelectedVendor(null) - setVendorSearchValue('') + setSelectedVendors([]) } return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="sm:max-w-[600px]"> - <DialogHeader> + <DialogContent className="max-w-4xl max-h-[90vh] p-0 flex flex-col"> + {/* 헤더 */} + <DialogHeader className="p-6 pb-0"> <DialogTitle>사전견적 업체 추가</DialogTitle> <DialogDescription> - 검색해서 업체를 선택해주세요. + 견적 요청을 보낼 업체를 선택하세요. 여러 개 선택 가능합니다. </DialogDescription> </DialogHeader> - <div className="grid gap-4 py-4"> - {/* Vendor 검색 */} - <div className="space-y-2"> - <Label htmlFor="vendor-search">업체 검색</Label> - <Popover open={vendorSearchOpen} onOpenChange={setVendorSearchOpen}> - <PopoverTrigger asChild> - <Button - variant="outline" - role="combobox" - aria-expanded={vendorSearchOpen} - className="w-full justify-between" - > - {selectedVendor - ? `${selectedVendor.vendorName} (${selectedVendor.vendorCode})` - : "업체를 검색해서 선택하세요..."} - <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> - </Button> - </PopoverTrigger> - <PopoverContent className="w-full p-0"> - <Command> - <CommandInput - placeholder="업체명 또는 코드를 입력하세요..." - value={vendorSearchValue} - onValueChange={setVendorSearchValue} - /> - <CommandEmpty> - {vendorSearchValue.length < 2 - ? "최소 2자 이상 입력해주세요" - : "검색 결과가 없습니다"} - </CommandEmpty> - <CommandGroup className="max-h-64 overflow-auto"> - {vendors.map((vendor) => ( - <CommandItem - key={vendor.id} - value={`${vendor.vendorName} ${vendor.vendorCode}`} - onSelect={() => handleVendorSelect(vendor)} + + {/* 메인 컨텐츠 */} + <div className="flex-1 px-6 py-4 overflow-y-auto"> + <div className="space-y-6"> + {/* 업체 선택 카드 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">업체 선택</CardTitle> + <CardDescription> + 사전견적을 발송할 업체를 선택하세요. + </CardDescription> + </CardHeader> + <CardContent> + <div className="space-y-4"> + {/* 업체 추가 버튼 */} + <Popover open={vendorOpen} onOpenChange={setVendorOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={vendorOpen} + className="w-full justify-between" + disabled={vendorList.length === 0} > - <Check - className={cn( - "mr-2 h-4 w-4", - selectedVendor?.id === vendor.id ? "opacity-100" : "opacity-0" - )} - /> - <div className="flex flex-col"> - <span className="font-medium">{vendor.vendorName}</span> - <span className="text-sm text-muted-foreground">{vendor.vendorCode}</span> - </div> - </CommandItem> - ))} - </CommandGroup> - </Command> - </PopoverContent> - </Popover> - </div> + <span className="flex items-center gap-2"> + <Plus className="h-4 w-4" /> + 업체 선택하기 + </span> + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[500px] p-0" align="start"> + <Command> + <CommandInput placeholder="업체명 또는 코드로 검색..." /> + <CommandList> + <CommandEmpty>검색 결과가 없습니다.</CommandEmpty> + <CommandGroup> + {vendorList + .filter(vendor => !isVendorSelected(vendor.id)) + .map((vendor) => ( + <CommandItem + key={vendor.id} + value={`${vendor.vendorCode} ${vendor.vendorName}`} + onSelect={() => handleAddVendor(vendor)} + > + <div className="flex items-center gap-2 w-full"> + <Badge variant="outline" className="shrink-0"> + {vendor.vendorCode} + </Badge> + <span className="truncate">{vendor.vendorName}</span> + </div> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + {/* 선택된 업체 목록 */} + {selectedVendors.length > 0 && ( + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <h4 className="text-sm font-medium">선택된 업체 ({selectedVendors.length}개)</h4> + </div> + <div className="space-y-2"> + {selectedVendors.map((vendor, index) => ( + <div + key={vendor.id} + className="flex items-center justify-between p-3 rounded-lg bg-secondary/50" + > + <div className="flex items-center gap-3"> + <span className="text-sm text-muted-foreground"> + {index + 1}. + </span> + <Badge variant="outline"> + {vendor.vendorCode} + </Badge> + <span className="text-sm font-medium"> + {vendor.vendorName} + </span> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => handleRemoveVendor(vendor.id)} + className="h-8 w-8 p-0" + > + <X className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </div> + )} + + {selectedVendors.length === 0 && ( + <div className="text-center py-8 text-muted-foreground"> + <p className="text-sm">아직 선택된 업체가 없습니다.</p> + <p className="text-xs mt-1">위 버튼을 클릭하여 업체를 추가하세요.</p> + </div> + )} + </div> + </CardContent> + </Card> + </div> </div> - <DialogFooter> - <Button variant="outline" onClick={() => onOpenChange(false)}> + + {/* 푸터 */} + <DialogFooter className="p-6 pt-0 border-t"> + <Button + variant="outline" + onClick={() => onOpenChange(false)} + disabled={isPending} + > 취소 </Button> - <Button onClick={handleCreate} disabled={isPending || !selectedVendor}> - 추가 + <Button + onClick={handleCreate} + disabled={isPending || selectedVendors.length === 0} + > + {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} + {selectedVendors.length > 0 + ? `${selectedVendors.length}개 업체 추가` + : '업체 추가' + } </Button> </DialogFooter> </DialogContent> diff --git a/lib/vendor-basic-info/basic-info-client.tsx b/lib/vendor-basic-info/basic-info-client.tsx index 0ef9f940..39763e0a 100644 --- a/lib/vendor-basic-info/basic-info-client.tsx +++ b/lib/vendor-basic-info/basic-info-client.tsx @@ -30,6 +30,10 @@ import { getVendorAttachmentsByType, getVendorPeriodicGrade, getVendorTypeInfo } import { useCreditIntegration } from "./use-credit-integration"; // downloadFile은 동적으로 import import { SalesInfoTable } from "./sales-info-table"; +import { vendors } from "@/db/schema/vendors"; + +// StatusType 정의 +type StatusType = (typeof vendors.status.enumValues)[number]; interface BasicInfoClientProps { initialData: VendorData | null; @@ -351,6 +355,26 @@ export default function BasicInfoClient({ const [pqSubmissionData, setPqSubmissionData] = useState<any[]>([]); const [additionalInfo, setAdditionalInfo] = useState<any>(null); const [businessContacts, setBusinessContacts] = useState<any[]>([]); + + // status 값에 따른 업체분류 결정 함수 + const getVendorClassification = (status: StatusType): string => { + const classificationMap: Record<StatusType, string> = { + "PENDING_REVIEW": "발굴업체", // 업체발굴 + "REJECTED": "발굴업체", // 가입거절 + "APPROVED": "잠재업체", // 가입승인 + "IN_PQ": "잠재업체", // PQ요청 + "PQ_SUBMITTED": "잠재업체", // PQ제출 + "PQ_FAILED": "잠재업체", // 실사실패 + "PQ_APPROVED": "잠재업체", // 실사통과 + "IN_REVIEW": "잠재업체", // 정규등록검토 + "READY_TO_SEND": "잠재업체", // 정규등록검토 + "ACTIVE": "정규업체", // 정규등록 + "INACTIVE": "중지업체", // 비활성화 + "BLACKLISTED": "중지업체", // 거래금지 + }; + + return classificationMap[status] || "미분류"; + }; const [formData, setFormData] = useState<VendorFormData>({ vendorName: initialData?.vendorName || "", representativeName: initialData?.representativeName || "", @@ -426,7 +450,12 @@ export default function BasicInfoClient({ try { const result = await fetchVendorRegistrationStatus(parseInt(vendorId)); if (!result.success || !result.data) { - toast.info("기본계약 정보가 없습니다."); + // 정규업체 등록 관련 레코드가 없는 경우 + if (result.noRegistration) { + toast.info("정규업체 등록 진행 정보가 없습니다."); + } else { + toast.info("기본계약 정보가 없습니다."); + } return; } @@ -812,11 +841,21 @@ export default function BasicInfoClient({ /> <InfoItem title="업체유형" - value={formData.businessType} - isEditable={true} - editMode={editMode} - fieldKey="businessType" - onChange={(value) => updateField("businessType", value)} + value={getVendorClassification(initialData?.status as StatusType) || ""} + type="readonly" + /> + <InfoItem + title="성조회 여부" + value={(() => { + const memberVal = initialData?.isAssociationMember as string | null; + switch (memberVal) { + case "Y": return "가입"; + case "N": return "미가입"; + case "E": return "해당없음"; + default: return "정보없음"; + } + })()} + type="readonly" /> <InfoItem title="소개자료" @@ -902,7 +941,7 @@ export default function BasicInfoClient({ <div className="space-y-2"> <InfoItem title="업체분류" - value={vendorTypeInfo?.vendorTypeName || ""} + value={getVendorClassification(initialData?.status as StatusType) || ""} type="readonly" /> <InfoItem @@ -941,7 +980,7 @@ export default function BasicInfoClient({ <WideInfoSection title="첨부파일" content={ - <div className="grid grid-cols-5 gap-4 min-w-0 overflow-x-auto"> + <div className="flex flex-wrap justify-between gap-4 min-w-0 overflow-x-auto"> {/* 사업자등록증 */} <div className="text-center min-w-0"> <div className="text-sm font-medium mb-2 break-words">사업자등록증</div> diff --git a/lib/vendor-basic-info/types.ts b/lib/vendor-basic-info/types.ts index ead3a44c..58b61957 100644 --- a/lib/vendor-basic-info/types.ts +++ b/lib/vendor-basic-info/types.ts @@ -127,6 +127,7 @@ export interface VendorData { email: string; website: string; status: string; + isAssociationMember: string | null; representativeName: string; representativeBirth: string; representativeEmail: string; diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts index 596a52a0..f4ba815c 100644 --- a/lib/vendors/service.ts +++ b/lib/vendors/service.ts @@ -2643,6 +2643,7 @@ export async function getVendorBasicInfo(vendorId: number) { email: vendor.email, website: vendor.website, status: vendor.status, + isAssociationMember: vendor.isAssociationMember, representativeName: vendor.representativeName, representativeBirth: vendor.representativeBirth, representativeEmail: vendor.representativeEmail, |
