summaryrefslogtreecommitdiff
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
parent146dd77da407438023d6fe6f18c0ebb8b6915765 (diff)
(최겸) 구매 기준정보, 로그인 용어, 입찰 내정가 산정 로직 수정
-rw-r--r--components/login/login-form.tsx38
-rw-r--r--components/login/partner-auth-form.tsx2
-rw-r--r--config/language.ts4
-rw-r--r--lib/bidding/detail/service.ts22
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-create-dialog.tsx298
-rw-r--r--lib/bidding/detail/table/bidding-detail-vendor-toolbar-actions.tsx4
-rw-r--r--lib/bidding/pre-quote/table/bidding-pre-quote-vendor-create-dialog.tsx302
-rw-r--r--lib/vendor-basic-info/basic-info-client.tsx55
-rw-r--r--lib/vendor-basic-info/types.ts1
-rw-r--r--lib/vendors/service.ts1
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,