From a8674e6b91fb4d356c311fad0251878de154da53 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 24 Nov 2025 11:16:32 +0000
Subject: (최겸) 구매 입찰 수정(폐찰, 낙찰 결재 기능 추가 등)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/bidding/manage/bidding-items-editor.tsx | 271 ++++++++++++++++++---
 1 file changed, 240 insertions(+), 31 deletions(-)

(limited to 'components/bidding/manage/bidding-items-editor.tsx')

diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx
index f0287ae4..208cf040 100644
--- a/components/bidding/manage/bidding-items-editor.tsx
+++ b/components/bidding/manage/bidding-items-editor.tsx
@@ -441,8 +441,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
       materialGroupInfo: null,
       materialNumber: null,
       materialInfo: null,
-      priceUnit: null,
-      purchaseUnit: '1',
+      priceUnit: 1,
+      purchaseUnit: 'EA',
       materialWeight: null,
       wbsCode: null,
       wbsName: null,
@@ -495,8 +495,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
       prev.map((item) => {
         if (item.id === id) {
           const updatedItem = { ...item, ...updates }
-          // 내정단가, 수량, 중량, 구매단위가 변경되면 내정금액 재계산
-          if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.purchaseUnit) {
+          // 내정단가, 수량, 중량, 가격단위가 변경되면 내정금액 재계산
+          if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.priceUnit) {
             updatedItem.targetAmount = calculateTargetAmount(updatedItem)
           }
           return updatedItem
@@ -533,20 +533,66 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
 
   const calculateTargetAmount = (item: PRItemInfo): string => {
     const unitPrice = parseFloat(item.targetUnitPrice?.replace(/,/g, '') || '0') || 0
-    const purchaseUnit = parseFloat(item.purchaseUnit || '1') || 1
+    const priceUnit = parseFloat(item.priceUnit || '1') || 1
     let amount = 0
 
     if (quantityWeightMode === 'quantity') {
       const quantity = parseFloat(item.quantity || '0') || 0
-      amount = (quantity / purchaseUnit) * unitPrice
+      amount = (quantity / priceUnit) * unitPrice
     } else {
       const weight = parseFloat(item.totalWeight || '0') || 0
-      amount = (weight / purchaseUnit) * unitPrice
+      amount = (weight / priceUnit) * unitPrice
     }
 
     return Math.floor(amount).toString()
   }
 
+  // 합계 계산 함수들
+  const calculateTotals = () => {
+    let quantityTotal = 0
+    let weightTotal = 0
+    let targetAmountTotal = 0
+    let budgetAmountTotal = 0
+    let actualAmountTotal = 0
+
+    items.forEach((item) => {
+      // 수량 합계
+      if (item.quantity) {
+        quantityTotal += parseFloat(item.quantity) || 0
+      }
+
+      // 중량 합계
+      if (item.totalWeight) {
+        weightTotal += parseFloat(item.totalWeight) || 0
+      }
+
+      // 내정금액 합계
+      if (item.targetAmount) {
+        targetAmountTotal += parseFloat(item.targetAmount.replace(/,/g, '')) || 0
+      }
+
+      // 예산금액 합계
+      if (item.budgetAmount) {
+        budgetAmountTotal += parseFloat(item.budgetAmount.replace(/,/g, '')) || 0
+      }
+
+      // 실적금액 합계
+      if (item.actualAmount) {
+        actualAmountTotal += parseFloat(item.actualAmount.replace(/,/g, '')) || 0
+      }
+    })
+
+    return {
+      quantityTotal,
+      weightTotal,
+      targetAmountTotal,
+      budgetAmountTotal,
+      actualAmountTotal,
+    }
+  }
+
+  const totals = calculateTotals()
+
   if (isLoading) {
     return (
       <div className="flex items-center justify-center p-8">
@@ -572,14 +618,16 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                 </th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">프로젝트코드</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">프로젝트명</th>
-                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">PR 번호</th>
+                
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">자재그룹코드 <span className="text-red-500">*</span></th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">자재그룹명 <span className="text-red-500">*</span></th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">자재코드</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">자재명</th>
-                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">수량</th>
-                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">단위</th>
+                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">수량(중량) <span className="text-red-500">*</span></th>
+                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">단위 <span className="text-red-500">*</span></th>
+                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">가격단위</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">구매단위</th>
+                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[100px]">자재순중량</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">내정단가</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">내정금액</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">내정통화</th>
@@ -587,19 +635,120 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">예산통화</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">실적금액</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">실적통화</th>
+                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">납품요청일 <span className="text-red-500">*</span></th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">WBS코드</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">WBS명</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">코스트센터코드</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">코스트센터명</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">GL계정코드</th>
                 <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">GL계정명</th>
-                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">납품요청일 <span className="text-red-500">*</span></th>
+                <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">PR 번호</th>
                 <th className="sticky right-0 z-10 bg-muted/50 border-l px-3 py-3 text-center text-xs font-medium min-w-[100px]">
                   액션
                 </th>
               </tr>
             </thead>
             <tbody>
+              {/* 합계 행 */}
+              <tr className="bg-blue-50 border-y-2 border-blue-200 font-semibold">
+                <td className="sticky left-0 z-10 bg-blue-50 border-r px-2 py-3 text-center">
+                  <span className="text-xs">합계</span>
+                </td>
+                <td className="sticky left-[50px] z-10 bg-blue-50 border-r px-3 py-3 text-center">
+                  <span className="text-xs">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs">
+                    {quantityWeightMode === 'quantity'
+                      ? `${formatNumberWithCommas(totals.quantityTotal.toString())}`
+                      : `${formatNumberWithCommas(totals.weightTotal.toString())}`
+                    }
+                  </span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                <span className="text-xs">
+                    {quantityWeightMode === 'quantity'
+                      ? `${items[0]?.quantityUnit || 'EA'}`
+                      : `${items[0]?.weightUnit || 'KG'}`
+                    }
+                  </span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs">{formatNumberWithCommas(totals.targetAmountTotal.toString())}</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs">{formatNumberWithCommas(totals.budgetAmountTotal.toString())}</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs">{formatNumberWithCommas(totals.actualAmountTotal.toString())}</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="border-r px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+                <td className="sticky right-0 z-10 bg-blue-50 border-l px-3 py-3 text-center">
+                  <span className="text-xs text-muted-foreground">-</span>
+                </td>
+              </tr>
               {items.map((item, index) => (
                 <tr key={item.id} className="border-t hover:bg-muted/30">
                   <td className="sticky left-0 z-10 bg-background border-r px-2 py-2 text-center">
@@ -617,10 +766,17 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                     <ProjectSelector
                       selectedProjectId={item.projectId || null}
                       onProjectSelect={(project) => {
-                        updatePRItem(item.id, {
-                          projectId: project.id,
-                          projectInfo: project.projectName
-                        })
+                        if (project) {
+                          updatePRItem(item.id, {
+                            projectId: project.id,
+                            projectInfo: project.projectName
+                          })
+                        } else {
+                          updatePRItem(item.id, {
+                            projectId: null,
+                            projectInfo: null
+                          })
+                        }
                       }}
                       placeholder="프로젝트 선택"
                     />
@@ -633,14 +789,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                       className="h-8 text-xs bg-muted/50"
                     />
                   </td>
-                  <td className="border-r px-3 py-2">
-                    <Input
-                      placeholder="PR 번호"
-                      value={item.prNumber || ''}
-                      readOnly
-                      className="h-8 text-xs bg-muted/50"
-                    />
-                  </td>
+
                   <td className="border-r px-3 py-2">
                     {biddingType !== 'equipment' ? (
                       <ProcurementItemSelectorDialogSingle
@@ -745,6 +894,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                         value={item.quantity || ''}
                         onChange={(e) => updatePRItem(item.id, { quantity: e.target.value })}
                         className="h-8 text-xs"
+                        required
                       />
                     ) : (
                       <Input
@@ -754,6 +904,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                         value={item.totalWeight || ''}
                         onChange={(e) => updatePRItem(item.id, { totalWeight: e.target.value })}
                         className="h-8 text-xs"
+                        required
                       />
                     )}
                   </td>
@@ -762,6 +913,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                       <Select
                         value={item.quantityUnit || 'EA'}
                         onValueChange={(value) => updatePRItem(item.id, { quantityUnit: value })}
+                        required
                       >
                         <SelectTrigger className="h-8 text-xs">
                           <SelectValue />
@@ -779,6 +931,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                       <Select
                         value={item.weightUnit || 'KG'}
                         onValueChange={(value) => updatePRItem(item.id, { weightUnit: value })}
+                        required
                       >
                         <SelectTrigger className="h-8 text-xs">
                           <SelectValue />
@@ -797,9 +950,42 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                       type="number"
                       min="1"
                       step="1"
-                      placeholder="구매단위"
-                      value={item.purchaseUnit || ''}
-                      onChange={(e) => updatePRItem(item.id, { purchaseUnit: e.target.value })}
+                      placeholder="가격단위"
+                      value={item.priceUnit || ''}
+                      onChange={(e) => updatePRItem(item.id, { priceUnit: e.target.value })}
+                      className="h-8 text-xs"
+                    />
+                  </td>
+                  <td className="border-r px-3 py-2">
+                    <Select
+                      value={item.purchaseUnit || 'EA'}
+                      onValueChange={(value) => updatePRItem(item.id, { purchaseUnit: value })}
+                    >
+                      <SelectTrigger className="h-8 text-xs">
+                        <SelectValue />
+                      </SelectTrigger>
+                      <SelectContent>
+                        <SelectItem value="EA">EA</SelectItem>
+                        <SelectItem value="SET">SET</SelectItem>
+                        <SelectItem value="LOT">LOT</SelectItem>
+                        <SelectItem value="M">M</SelectItem>
+                        <SelectItem value="M2">M²</SelectItem>
+                        <SelectItem value="M3">M³</SelectItem>
+                        <SelectItem value="KG">KG</SelectItem>
+                        <SelectItem value="TON">TON</SelectItem>
+                        <SelectItem value="G">G</SelectItem>
+                        <SelectItem value="LB">LB</SelectItem>
+                      </SelectContent>
+                    </Select>
+                  </td>
+                  <td className="border-r px-3 py-2">
+                    <Input
+                      type="number"
+                      min="0"
+                      step="0.01"
+                      placeholder="자재순중량"
+                      value={item.materialWeight || ''}
+                      onChange={(e) => updatePRItem(item.id, { materialWeight: e.target.value })}
                       className="h-8 text-xs"
                     />
                   </td>
@@ -887,10 +1073,24 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                       </SelectContent>
                     </Select>
                   </td>
+                  <td className="border-r px-3 py-2">
+                    <Input
+                      type="date"
+                      value={item.requestedDeliveryDate || ''}
+                      onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })}
+                      className="h-8 text-xs"
+                      required
+                    />
+                  </td>
                   <td className="border-r px-3 py-2">
                     <Button
                       variant="outline"
                       onClick={() => {
+                        // 재클릭 시 기존 데이터 클리어
+                        updatePRItem(item.id, {
+                          wbsCode: null,
+                          wbsName: null
+                        })
                         setSelectedItemForWbs(item.id)
                         setWbsCodeDialogOpen(true)
                       }}
@@ -941,6 +1141,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                     <Button
                       variant="outline"
                       onClick={() => {
+                        // 재클릭 시 기존 데이터 클리어
+                        updatePRItem(item.id, {
+                          costCenterCode: null,
+                          costCenterName: null
+                        })
                         setSelectedItemForCostCenter(item.id)
                         setCostCenterDialogOpen(true)
                       }}
@@ -992,6 +1197,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                     <Button
                       variant="outline"
                       onClick={() => {
+                        // 재클릭 시 기존 데이터 클리어
+                        updatePRItem(item.id, {
+                          glAccountCode: null,
+                          glAccountName: null
+                        })
                         setSelectedItemForGlAccount(item.id)
                         setGlAccountDialogOpen(true)
                       }}
@@ -1039,11 +1249,10 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems
                   </td>
                   <td className="border-r px-3 py-2">
                     <Input
-                      type="date"
-                      value={item.requestedDeliveryDate || ''}
-                      onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })}
-                      className="h-8 text-xs"
-                      required
+                      placeholder="PR 번호"
+                      value={item.prNumber || ''}
+                      readOnly
+                      className="h-8 text-xs bg-muted/50"
                     />
                   </td>
                   <td className="sticky right-0 z-10 bg-background border-l px-3 py-2">
-- 
cgit v1.2.3

