From 0d68dbcba27ce49c15f30126f7a5dfce974847a3 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 19 Nov 2025 09:24:58 +0000 Subject: (최겸) 구매 입찰 발주비율 취소기능 추가 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/actions.ts | 6 +- lib/bidding/detail/service.ts | 34 +++++++++ .../table/bidding-detail-vendor-edit-dialog.tsx | 25 ++++-- .../detail/table/bidding-detail-vendor-table.tsx | 24 +++--- .../bidding-detail-vendor-toolbar-actions.tsx | 89 ++++++++++++++++++++-- 5 files changed, 152 insertions(+), 26 deletions(-) (limited to 'lib/bidding') diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index c4c543d9..d0017413 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -613,16 +613,16 @@ export async function cancelDisposalAction( } } - // 3. 입찰 상태를 입찰생성으로 변경 + // 3. 입찰 상태를 입찰평가중으로 변경 await tx .update(biddings) .set({ - status: 'bidding_generated', + status: 'evaluation_of_bidding', updatedAt: new Date(), updatedBy: userName, }) .where(eq(biddings.id, biddingId)) - + return { success: true, message: '유찰 취소가 완료되었습니다.' diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index b5a3cce8..6ab9270e 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -610,6 +610,40 @@ export async function updateBiddingDetailVendor( } } +// 발주비율 취소 (발주비율을 null로 리셋하고 낙찰 상태 해제) +export async function cancelAwardRatio(biddingCompanyId: number) { + try { + const result = await db.update(biddingCompanies) + .set({ + awardRatio: null, + isWinner: false, + updatedAt: new Date(), + }) + .where(eq(biddingCompanies.id, biddingCompanyId)) + .returning({ biddingId: biddingCompanies.biddingId }) + + // 캐시 무효화 + if (result.length > 0) { + const biddingId = result[0].biddingId + revalidateTag(`bidding-${biddingId}`) + revalidateTag('quotation-vendors') + revalidateTag('quotation-details') + revalidatePath(`/evcp/bid/${biddingId}`) + } + + return { + success: true, + message: '발주비율이 성공적으로 취소되었습니다.', + } + } catch (error) { + console.error('Failed to cancel award ratio:', error) + return { + success: false, + error: error instanceof Error ? error.message : '발주비율 취소에 실패했습니다.' + } + } +} + // 본입찰용 업체 추가 export async function createBiddingDetailVendor( biddingId: number, diff --git a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx index 1a1b331e..6e5481f4 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-edit-dialog.tsx @@ -24,6 +24,7 @@ interface BiddingDetailVendorEditDialogProps { onSuccess: () => void biddingAwardCount?: string // 낙찰수 정보 추가 biddingStatus?: string // 입찰 상태 정보 추가 + allVendors?: QuotationVendor[] // 전체 벤더 목록 추가 } export function BiddingDetailVendorEditDialog({ @@ -32,7 +33,8 @@ export function BiddingDetailVendorEditDialog({ onOpenChange, onSuccess, biddingAwardCount, - biddingStatus + biddingStatus, + allVendors = [] }: BiddingDetailVendorEditDialogProps) { const { toast } = useToast() const [isPending, startTransition] = useTransition() @@ -42,6 +44,14 @@ export function BiddingDetailVendorEditDialog({ awardRatio: 0, }) + // 단수낙찰의 경우 이미 100%인 벤더가 있는지 확인 + const hasWinnerWith100Percent = React.useMemo(() => { + if (biddingAwardCount === 'single') { + return allVendors.some(v => v.awardRatio === 100 && v.id !== vendor?.id) + } + return false + }, [allVendors, biddingAwardCount, vendor?.id]) + // vendor가 변경되면 폼 데이터 업데이트 React.useEffect(() => { if (vendor) { @@ -135,7 +145,7 @@ export function BiddingDetailVendorEditDialog({ value={formData.awardRatio} onChange={(e) => setFormData({ ...formData, awardRatio: Number(e.target.value) })} placeholder="발주비율을 입력하세요" - disabled={vendor?.isBiddingParticipated !== true || biddingAwardCount === 'single' || biddingStatus === 'vendor_selected'} + disabled={vendor?.isBiddingParticipated !== true || biddingAwardCount === 'single' || biddingStatus === 'vendor_selected' || hasWinnerWith100Percent} /> {vendor?.isBiddingParticipated !== true && (

@@ -152,15 +162,20 @@ export function BiddingDetailVendorEditDialog({ 낙찰이 완료되어 발주비율을 수정할 수 없습니다.

)} + {hasWinnerWith100Percent && ( +

+ 단수 낙찰의 경우 이미 100% 발주비율이 설정된 업체가 있어 다른 업체의 발주비율을 수정할 수 없습니다. +

+ )} - diff --git a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx index cfdab9c6..1fa116ab 100644 --- a/lib/bidding/detail/table/bidding-detail-vendor-table.tsx +++ b/lib/bidding/detail/table/bidding-detail-vendor-table.tsx @@ -66,16 +66,16 @@ const advancedFilterFields: DataTableAdvancedFilterField[] = [ label: '견적금액', type: 'number', }, - { - id: 'status', - label: '상태', - type: 'multi-select', - options: [ - { label: '제출완료', value: 'submitted' }, - { label: '선정완료', value: 'selected' }, - { label: '미제출', value: 'pending' }, - ], - }, + { + id: 'invitationStatus', + label: '상태', + type: 'multi-select', + options: [ + { label: '제출완료', value: 'bidding_submitted' }, + { label: '선정완료', value: 'bidding_accepted' }, + { label: '미제출', value: 'pending' }, + ], + }, ] export function BiddingDetailVendorTableContent({ @@ -201,6 +201,7 @@ export function BiddingDetailVendorTableContent({ userId={userId} onOpenAwardDialog={() => setIsAwardDialogOpen(true)} onSuccess={onRefresh} + winnerVendor={vendors.find(v => v.awardRatio === 100)} /> @@ -210,8 +211,9 @@ export function BiddingDetailVendorTableContent({ open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen} onSuccess={onRefresh} - biddingAwardCount={bidding.awardCount} + biddingAwardCount={bidding.awardCount || undefined} biddingStatus={bidding.status} + allVendors={vendors} /> void onSuccess: () => void + winnerVendor?: QuotationVendor | null // 100% 낙찰된 벤더 } export function BiddingDetailVendorToolbarActions({ @@ -28,7 +30,8 @@ export function BiddingDetailVendorToolbarActions({ bidding, userId, onOpenAwardDialog, - onSuccess + onSuccess, + winnerVendor }: BiddingDetailVendorToolbarActionsProps) { const router = useRouter() const { toast } = useToast() @@ -39,6 +42,7 @@ export function BiddingDetailVendorToolbarActions({ const [isBiddingInvitationDialogOpen, setIsBiddingInvitationDialogOpen] = React.useState(false) const [selectedVendors, setSelectedVendors] = React.useState([]) const [isRoundIncreaseDialogOpen, setIsRoundIncreaseDialogOpen] = React.useState(false) + const [isCancelAwardDialogOpen, setIsCancelAwardDialogOpen] = React.useState(false) // 본입찰 초대 다이얼로그가 열릴 때 선정된 업체들 조회 React.useEffect(() => { @@ -178,26 +182,51 @@ export function BiddingDetailVendorToolbarActions({ }) } + const handleCancelAward = () => { + if (!winnerVendor) return + + startTransition(async () => { + const result = await cancelAwardRatio(winnerVendor.id) + + if (result.success) { + toast({ + title: "성공", + description: result.message, + }) + setIsCancelAwardDialogOpen(false) + onSuccess() + } else { + toast({ + title: "오류", + description: result.error || "발주비율 취소 중 오류가 발생했습니다.", + variant: 'destructive', + }) + } + }) + } + const handleRoundIncreaseWithNavigation = () => { startTransition(async () => { const result = await increaseRoundOrRebid(bidding.id, userId, 'round_increase') if (result.success) { + const successResult = result as { success: true; message: string; biddingId: number; biddingNumber: string } toast({ title: "성공", - description: result.message, + description: successResult.message, }) // 새로 생성된 입찰의 상세 페이지로 이동 - if (result.biddingId) { - router.push(`/evcp/bid/${result.biddingId}`) + if (successResult.biddingId) { + router.push(`/evcp/bid/${successResult.biddingId}/info`) } else { - router.push(`/evcp/bid`) + router.push(`/evcp/bid/${biddingId}/info`) } onSuccess() } else { + const errorResult = result as { success: false; error: string } toast({ title: "오류", - description: result.error || "차수증가 중 오류가 발생했습니다.", + description: errorResult.error || "차수증가 중 오류가 발생했습니다.", variant: 'destructive', }) } @@ -244,6 +273,19 @@ export function BiddingDetailVendorToolbarActions({ )} + + {/* 발주비율 취소: 100% 낙찰된 벤더가 있는 경우 */} + {winnerVendor && ( + + )} {/* 구분선 */} {(bidding.status === 'bidding_generated' || bidding.status === 'bidding_disposal') && ( @@ -307,6 +349,39 @@ export function BiddingDetailVendorToolbarActions({ + {/* 발주비율 취소 확인 다이얼로그 */} + + + + 발주비율 취소 확인 + + {winnerVendor && ( + <> + {winnerVendor.vendorName} 업체의 발주비율(100%)을 취소하시겠습니까? +
+ 취소 후 다른 업체의 발주비율을 설정할 수 있습니다. + + )} +
+
+ + + + +
+
+ ) } -- cgit v1.2.3