From 8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 12 Nov 2025 10:42:36 +0000
Subject: (최겸) 구매 일반계약, 입찰 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/bidding/failure/biddings-failure-table.tsx | 266 ++++++++++++++++++++++---
 1 file changed, 233 insertions(+), 33 deletions(-)

(limited to 'lib/bidding/failure/biddings-failure-table.tsx')

diff --git a/lib/bidding/failure/biddings-failure-table.tsx b/lib/bidding/failure/biddings-failure-table.tsx
index 901648d2..c80021ea 100644
--- a/lib/bidding/failure/biddings-failure-table.tsx
+++ b/lib/bidding/failure/biddings-failure-table.tsx
@@ -18,7 +18,12 @@ import {
     biddingStatusLabels,
     contractTypeLabels,
 } from "@/db/schema"
-import { SpecificationMeetingDialog, PrDocumentsDialog } from "../list/bidding-detail-dialogs"
+import { BiddingsClosureDialog } from "./biddings-closure-dialog"
+import { Button } from "@/components/ui/button"
+import { FileX, RefreshCw, Undo2 } from "lucide-react"
+import { bidClosureAction, cancelDisposalAction } from "@/lib/bidding/actions"
+import { increaseRoundOrRebid } from "@/lib/bidding/service"
+import { useToast } from "@/hooks/use-toast"
 
 type BiddingFailureItem = {
   id: number
@@ -30,7 +35,7 @@ type BiddingFailureItem = {
   prNumber: string | null
 
   // 가격 정보
-  targetPrice: number | null
+  targetPrice: string | number | null
   currency: string | null
 
   // 일정 정보
@@ -47,6 +52,15 @@ type BiddingFailureItem = {
   disposalUpdatedAt: Date | null // 폐찰수정일
   disposalUpdatedBy: string | null // 폐찰수정자
 
+  // 폐찰 정보
+  closureReason: string | null // 폐찰사유
+  closureDocuments: {
+    id: number
+    fileName: string
+    originalFileName: string
+    filePath: string
+  }[] // 폐찰 첨부파일들
+
   // 기타 정보
   createdBy: string | null
   createdAt: Date | null
@@ -69,9 +83,9 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
     const { data, pageCount } = biddingsResult
 
     const [isCompact, setIsCompact] = React.useState<boolean>(false)
-    const [specMeetingDialogOpen, setSpecMeetingDialogOpen] = React.useState(false)
-    const [prDocumentsDialogOpen, setPrDocumentsDialogOpen] = React.useState(false)
+    const [biddingClosureDialogOpen, setBiddingClosureDialogOpen] = React.useState(false)
     const [selectedBidding, setSelectedBidding] = React.useState<BiddingFailureItem | null>(null)
+    const { toast } = useToast()
 
     const [rowAction, setRowAction] = React.useState<DataTableRowAction<BiddingFailureItem> | null>(null)
 
@@ -89,17 +103,18 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
             setSelectedBidding(rowAction.row.original)
 
             switch (rowAction.type) {
-                case "view":
-                    // 상세 페이지로 이동
-                    router.push(`/evcp/bid/${rowAction.row.original.id}`)
+                case "rebid":
+                    // 재입찰
+                    handleRebid(rowAction.row.original)
                     break
-                case "history":
-                    // 이력보기 (추후 구현)
-                    console.log('이력보기:', rowAction.row.original)
+                case "closure":
+                    // 폐찰
+                    setSelectedBidding(rowAction.row.original)
+                    setBiddingClosureDialogOpen(true)
                     break
-                case "rebid":
-                    // 재입찰 (추후 구현)
-                    console.log('재입찰:', rowAction.row.original)
+                case "cancelDisposal":
+                    // 유찰취소
+                    handleCancelDisposal(rowAction.row.original)
                     break
                 default:
                     break
@@ -163,6 +178,8 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
         filterFields,
         enablePinning: true,
         enableAdvancedFilter: true,
+        enableRowSelection: true,
+        singleRowSelection: true,
         initialState: {
             sorting: [{ id: "disposalDate", desc: true }], // 유찰일 기준 최신순
             columnPinning: { right: ["actions"] },
@@ -176,17 +193,85 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
         setIsCompact(compact)
     }, [])
 
-    const handleSpecMeetingDialogClose = React.useCallback(() => {
-        setSpecMeetingDialogOpen(false)
+    const handleBiddingClosureDialogClose = React.useCallback(() => {
+        setBiddingClosureDialogOpen(false)
         setRowAction(null)
         setSelectedBidding(null)
     }, [])
 
-    const handlePrDocumentsDialogClose = React.useCallback(() => {
-        setPrDocumentsDialogOpen(false)
-        setRowAction(null)
-        setSelectedBidding(null)
-    }, [])
+    const handleRebid = React.useCallback(async (bidding: BiddingFailureItem) => {
+        if (!session?.user?.id) {
+            toast({
+                title: "오류",
+                description: "사용자 정보를 찾을 수 없습니다.",
+                variant: "destructive",
+            })
+            return
+        }
+
+        try {
+            const result = await increaseRoundOrRebid(bidding.id, session.user.id, 'rebidding')
+
+            if (result.success) {
+                toast({
+                    title: "성공",
+                    description: result.message,
+                })
+                // 페이지 새로고침
+                router.refresh()
+            } else {
+                toast({
+                    title: "오류",
+                    description: result.error || "재입찰 중 오류가 발생했습니다.",
+                    variant: "destructive",
+                })
+            }
+        } catch (error) {
+            console.error('재입찰 실패:', error)
+            toast({
+                title: "오류",
+                description: "재입찰 중 오류가 발생했습니다.",
+                variant: "destructive",
+            })
+        }
+    }, [session?.user?.id, toast, router])
+
+    const handleCancelDisposal = React.useCallback(async (bidding: BiddingFailureItem) => {
+        if (!session?.user?.id) {
+            toast({
+                title: "오류",
+                description: "사용자 정보를 찾을 수 없습니다.",
+                variant: "destructive",
+            })
+            return
+        }
+
+        try {
+            const result = await cancelDisposalAction(bidding.id, session.user.id)
+
+            if (result.success) {
+                toast({
+                    title: "성공",
+                    description: result.message,
+                })
+                // 페이지 새로고침
+                router.refresh()
+            } else {
+                toast({
+                    title: "오류",
+                    description: result.error || "유찰취소 중 오류가 발생했습니다.",
+                    variant: "destructive",
+                })
+            }
+        } catch (error) {
+            console.error('유찰취소 실패:', error)
+            toast({
+                title: "오류",
+                description: "유찰취소 중 오류가 발생했습니다.",
+                variant: "destructive",
+            })
+        }
+    }, [session?.user?.id, toast, router])
 
     return (
         <>
@@ -202,22 +287,137 @@ export function BiddingsFailureTable({ promises }: BiddingsFailureTableProps) {
                     compactStorageKey="biddingsFailureTableCompact"
                     onCompactChange={handleCompactChange}
                 >
+                    {/* Toolbar 액션 버튼들 */}
+                    <div className="flex items-center gap-2">
+                        <Button
+                            variant="outline"
+                            size="sm"
+                            onClick={() => {
+                                const selectedRows = table.getFilteredSelectedRowModel().rows
+                                if (selectedRows.length === 0) {
+                                    toast({
+                                        title: "선택 필요",
+                                        description: "폐찰할 입찰을 선택해주세요.",
+                                        variant: "destructive",
+                                    })
+                                    return
+                                }
+                                if (selectedRows.length > 1) {
+                                    toast({
+                                        title: "하나만 선택",
+                                        description: "폐찰은 한 개의 입찰만 선택해주세요.",
+                                        variant: "destructive",
+                                    })
+                                    return
+                                }
+                                const bidding = selectedRows[0].original
+                                if (bidding.status !== 'bidding_disposal') {
+                                    toast({
+                                        title: "유찰 상태만 가능",
+                                        description: "유찰 상태인 입찰만 폐찰할 수 있습니다.",
+                                        variant: "destructive",
+                                    })
+                                    return
+                                }
+                                setSelectedBidding(bidding)
+                                setBiddingClosureDialogOpen(true)
+                            }}
+                            disabled={table.getFilteredSelectedRowModel().rows.length !== 1 ||
+                                     (table.getFilteredSelectedRowModel().rows.length === 1 &&
+                                      table.getFilteredSelectedRowModel().rows[0].original.status === 'bid_closure')}
+                        >
+                            <FileX className="mr-2 h-4 w-4" />
+                            폐찰
+                        </Button>
+
+                        <Button
+                            variant="outline"
+                            size="sm"
+                            onClick={() => {
+                                const selectedRows = table.getFilteredSelectedRowModel().rows
+                                if (selectedRows.length === 0) {
+                                    toast({
+                                        title: "선택 필요",
+                                        description: "재입찰할 입찰을 선택해주세요.",
+                                        variant: "destructive",
+                                    })
+                                    return
+                                }
+                                if (selectedRows.length > 1) {
+                                    toast({
+                                        title: "하나만 선택",
+                                        description: "재입찰은 한 개의 입찰만 선택해주세요.",
+                                        variant: "destructive",
+                                    })
+                                    return
+                                }
+                                const bidding = selectedRows[0].original
+                                handleRebid(bidding)
+                            }}
+                            disabled={table.getFilteredSelectedRowModel().rows.length !== 1 ||
+                                     (table.getFilteredSelectedRowModel().rows.length === 1 &&
+                                      table.getFilteredSelectedRowModel().rows[0].original.status === 'bid_closure')}
+                        >
+                            <RefreshCw className="mr-2 h-4 w-4" />
+                            재입찰
+                        </Button>
+
+                        <Button
+                            variant="outline"
+                            size="sm"
+                            onClick={() => {
+                                const selectedRows = table.getFilteredSelectedRowModel().rows
+                                if (selectedRows.length === 0) {
+                                    toast({
+                                        title: "선택 필요",
+                                        description: "유찰취소할 입찰을 선택해주세요.",
+                                        variant: "destructive",
+                                    })
+                                    return
+                                }
+                                if (selectedRows.length > 1) {
+                                    toast({
+                                        title: "하나만 선택",
+                                        description: "유찰취소는 한 개의 입찰만 선택해주세요.",
+                                        variant: "destructive",
+                                    })
+                                    return
+                                }
+                                const bidding = selectedRows[0].original
+                                if (bidding.status !== 'bidding_disposal' && bidding.status !== 'bid_closure') {
+                                    toast({
+                                        title: "유찰/폐찰 상태만 가능",
+                                        description: "유찰 또는 폐찰 상태인 입찰만 취소할 수 있습니다.",
+                                        variant: "destructive",
+                                    })
+                                    return
+                                }
+                                handleCancelDisposal(bidding)
+                            }}
+                            disabled={table.getFilteredSelectedRowModel().rows.length !== 1 ||
+                                     (table.getFilteredSelectedRowModel().rows.length === 1 &&
+                                      table.getFilteredSelectedRowModel().rows[0].original.status === 'bid_closure')}
+                        >
+                            <Undo2 className="mr-2 h-4 w-4" />
+                            유찰취소
+                        </Button>
+                    </div>
                 </DataTableAdvancedToolbar>
             </DataTable>
 
-            {/* 사양설명회 다이얼로그 */}
-            <SpecificationMeetingDialog
-                open={specMeetingDialogOpen}
-                onOpenChange={handleSpecMeetingDialogClose}
-                bidding={selectedBidding}
-            />
-
-            {/* PR 문서 다이얼로그 */}
-            <PrDocumentsDialog
-                open={prDocumentsDialogOpen}
-                onOpenChange={handlePrDocumentsDialogClose}
-                bidding={selectedBidding}
-            />
+            {/* 폐찰 다이얼로그 */}
+            {selectedBidding && session?.user?.id && (
+                <BidClosureDialog
+                    open={biddingClosureDialogOpen}
+                    onOpenChange={handleBiddingClosureDialogClose}
+                    bidding={selectedBidding}
+                    userId={session.user.id}
+                    onSuccess={() => {
+                        router.refresh()
+                        handleBiddingClosureDialogClose()
+                    }}
+                />
+            )}
         </>
     )
 }
-- 
cgit v1.2.3

