summaryrefslogtreecommitdiff
path: root/lib/bidding/pre-quote
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-25 03:15:45 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-25 03:15:45 +0000
commit450234437267cdd9cdf196d5d37657708062bef5 (patch)
tree2ec0ef193a71e949ccda776e040cc02ceff88ce0 /lib/bidding/pre-quote
parent146dd77da407438023d6fe6f18c0ebb8b6915765 (diff)
(최겸) 구매 기준정보, 로그인 용어, 입찰 내정가 산정 로직 수정
Diffstat (limited to 'lib/bidding/pre-quote')
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-vendor-create-dialog.tsx302
1 files changed, 204 insertions, 98 deletions
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>