From d1f1768611a73541f5d63b6735f64d194466825b Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 4 Dec 2025 12:42:23 +0000
Subject: (최겸) 구매 입찰 견적 히스토리, 응찰품목조회 table 개발
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../detail/general-contract-items-table.tsx        | 117 +++++++++++++++++----
 1 file changed, 94 insertions(+), 23 deletions(-)

(limited to 'lib/general-contracts/detail/general-contract-items-table.tsx')

diff --git a/lib/general-contracts/detail/general-contract-items-table.tsx b/lib/general-contracts/detail/general-contract-items-table.tsx
index e5fc6cf2..4f74cfbb 100644
--- a/lib/general-contracts/detail/general-contract-items-table.tsx
+++ b/lib/general-contracts/detail/general-contract-items-table.tsx
@@ -32,6 +32,7 @@ import { MaterialGroupSelectorDialogSingle } from '@/components/common/material/
 import { MaterialSearchItem } from '@/lib/material/material-group-service'
 import { ProcurementItemSelectorDialogSingle } from '@/components/common/selectors/procurement-item/procurement-item-selector-dialog-single'
 import { ProcurementSearchItem } from '@/components/common/selectors/procurement-item/procurement-item-service'
+import { cn } from '@/lib/utils'
 
 interface ContractItem {
   id?: number
@@ -43,12 +44,12 @@ interface ContractItem {
   materialGroupCode?: string
   materialGroupDescription?: string
   specification: string
-  quantity: number
+  quantity: number | string // number | string으로 변경하여 입력 중 포맷팅 지원
   quantityUnit: string
-  totalWeight: number
+  totalWeight: number | string // number | string으로 변경하여 입력 중 포맷팅 지원
   weightUnit: string
   contractDeliveryDate: string
-  contractUnitPrice: number
+  contractUnitPrice: number | string // number | string으로 변경하여 입력 중 포맷팅 지원
   contractAmount: number
   contractCurrency: string
   isSelected?: boolean
@@ -105,6 +106,34 @@ export function ContractItemsTable({
     contractUnitPrice: ''
   })
 
+  // 천단위 콤마 포맷팅 헬퍼 함수들
+  const formatNumberWithCommas = (value: string | number | null | undefined): string => {
+    if (value === null || value === undefined || value === '') return ''
+    const str = value.toString()
+    const parts = str.split('.')
+    const integerPart = parts[0].replace(/,/g, '')
+    
+    // 정수부가 비어있거나 '-' 만 있는 경우 처리
+    if (integerPart === '' || integerPart === '-') {
+        return str
+    }
+    
+    const num = parseFloat(integerPart)
+    if (isNaN(num)) return str
+    
+    const formattedInt = num.toLocaleString()
+    
+    if (parts.length > 1) {
+      return `${formattedInt}.${parts[1]}`
+    }
+    
+    return formattedInt
+  }
+
+  const parseNumberFromCommas = (value: string): string => {
+    return value.replace(/,/g, '')
+  }
+
   // 초기 데이터 로드
   React.useEffect(() => {
     const loadItems = async () => {
@@ -125,6 +154,8 @@ export function ContractItemsTable({
             }
           }
           
+          // number 타입을 string으로 변환하지 않고 일단 그대로 둠 (렌더링 시 포맷팅)
+          // 단, 입력 중 편의를 위해 string이 들어올 수 있으므로 ContractItem 타입 변경함
           return {
             id: item.id,
             projectId: item.projectId || null,
@@ -174,8 +205,17 @@ export function ContractItemsTable({
       
       // validation 체크
       const errors: string[] = []
-      for (let index = 0; index < localItems.length; index++) {
-        const item = localItems[index]
+      // 저장 시 number로 변환된 데이터 준비
+      const itemsToSave = localItems.map(item => ({
+        ...item,
+        quantity: parseFloat(item.quantity.toString().replace(/,/g, '')) || 0,
+        totalWeight: parseFloat(item.totalWeight.toString().replace(/,/g, '')) || 0,
+        contractUnitPrice: parseFloat(item.contractUnitPrice.toString().replace(/,/g, '')) || 0,
+        contractAmount: parseFloat(item.contractAmount.toString().replace(/,/g, '')) || 0,
+      }));
+
+      for (let index = 0; index < itemsToSave.length; index++) {
+        const item = itemsToSave[index]
         // if (!item.itemCode) errors.push(`${index + 1}번째 품목의 품목코드`)
         if (!item.itemInfo) errors.push(`${index + 1}번째 품목의 Item 정보`)
         if (!item.quantity || item.quantity <= 0) errors.push(`${index + 1}번째 품목의 수량`)
@@ -188,7 +228,7 @@ export function ContractItemsTable({
         return
       }
       
-      await updateContractItems(contractId, localItems as any)
+      await updateContractItems(contractId, itemsToSave as any)
       toast.success('품목정보가 저장되었습니다.')
     } catch (error) {
       console.error('Error saving contract items:', error)
@@ -199,9 +239,18 @@ export function ContractItemsTable({
   }
 
   // 총 금액 계산
-  const totalAmount = localItems.reduce((sum, item) => sum + item.contractAmount, 0)
-  const totalQuantity = localItems.reduce((sum, item) => sum + item.quantity, 0)
-  const totalUnitPrice = localItems.reduce((sum, item) => sum + item.contractUnitPrice, 0)
+  const totalAmount = localItems.reduce((sum, item) => {
+    const amount = parseFloat(item.contractAmount.toString().replace(/,/g, '')) || 0
+    return sum + amount
+  }, 0)
+  const totalQuantity = localItems.reduce((sum, item) => {
+    const quantity = parseFloat(item.quantity.toString().replace(/,/g, '')) || 0
+    return sum + quantity
+  }, 0)
+  const totalUnitPrice = localItems.reduce((sum, item) => {
+    const unitPrice = parseFloat(item.contractUnitPrice.toString().replace(/,/g, '')) || 0
+    return sum + unitPrice
+  }, 0)
   const amountDifference = availableBudget - totalAmount
   const budgetRatio = availableBudget > 0 ? (totalAmount / availableBudget) * 100 : 0
 
@@ -213,12 +262,14 @@ export function ContractItemsTable({
   // 아이템 업데이트
   const updateItem = (index: number, field: keyof ContractItem, value: string | number | boolean | undefined) => {
     const updatedItems = [...localItems]
-    updatedItems[index] = { ...updatedItems[index], [field]: value }
+    const updatedItem = { ...updatedItems[index], [field]: value }
+    updatedItems[index] = updatedItem
     
     // 단가나 수량이 변경되면 금액 자동 계산
     if (field === 'contractUnitPrice' || field === 'quantity') {
-      const item = updatedItems[index]
-      updatedItems[index].contractAmount = item.contractUnitPrice * item.quantity
+      const quantity = parseFloat(updatedItem.quantity.toString().replace(/,/g, '')) || 0
+      const unitPrice = parseFloat(updatedItem.contractUnitPrice.toString().replace(/,/g, '')) || 0
+      updatedItem.contractAmount = unitPrice * quantity
     }
     
     setLocalItems(updatedItems)
@@ -326,7 +377,8 @@ export function ContractItemsTable({
       if (batchInputData.contractUnitPrice) {
         updatedItem.contractUnitPrice = parseFloat(batchInputData.contractUnitPrice) || 0
         // 단가가 변경되면 계약금액도 재계산
-        updatedItem.contractAmount = updatedItem.contractUnitPrice * updatedItem.quantity
+        const quantity = parseFloat(updatedItem.quantity.toString().replace(/,/g, '')) || 0
+        updatedItem.contractAmount = (parseFloat(batchInputData.contractUnitPrice) || 0) * quantity
       }
       
       return updatedItem
@@ -712,14 +764,23 @@ export function ContractItemsTable({
                     )}
                   </TableCell> */}
                   <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <span className="text-sm text-right">{item.quantity.toLocaleString()}</span>
+                    ) : (
                       <Input
-                        type="number"
-                        value={item.quantity}
-                        onChange={(e) => updateItem(index, 'quantity', parseFloat(e.target.value) || 0)}
+                        type="text"
+                        value={formatNumberWithCommas(item.quantity)}
+                        onChange={(e) => {
+                          const val = parseNumberFromCommas(e.target.value)
+                          if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
+                            updateItem(index, 'quantity', val)
+                          }
+                        }}
                         className="h-8 text-sm text-right"
                         placeholder="0"
-                        disabled={!isEnabled}
+                        disabled={!isEnabled || isQuantityDisabled}
                       />
+                    )}
                   </TableCell>
                   <TableCell className="px-3 py-3">
                     {readOnly ? (
@@ -748,9 +809,14 @@ export function ContractItemsTable({
                       <span className="text-sm text-right">{item.totalWeight.toLocaleString()}</span>
                     ) : (
                       <Input
-                        type="number"
-                        value={item.totalWeight}
-                        onChange={(e) => updateItem(index, 'totalWeight', parseFloat(e.target.value) || 0)}
+                        type="text"
+                        value={formatNumberWithCommas(item.totalWeight)}
+                        onChange={(e) => {
+                          const val = parseNumberFromCommas(e.target.value)
+                          if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
+                            updateItem(index, 'totalWeight', val)
+                          }
+                        }}
                         className="h-8 text-sm text-right"
                         placeholder="0"
                         disabled={!isEnabled || isQuantityDisabled}
@@ -797,9 +863,14 @@ export function ContractItemsTable({
                       <span className="text-sm text-right">{item.contractUnitPrice.toLocaleString()}</span>
                     ) : (
                       <Input
-                        type="number"
-                        value={item.contractUnitPrice}
-                        onChange={(e) => updateItem(index, 'contractUnitPrice', parseFloat(e.target.value) || 0)}
+                        type="text"
+                        value={formatNumberWithCommas(item.contractUnitPrice)}
+                        onChange={(e) => {
+                          const val = parseNumberFromCommas(e.target.value)
+                          if (val === '' || /^-?\d*\.?\d*$/.test(val)) {
+                            updateItem(index, 'contractUnitPrice', val)
+                          }
+                        }}
                         className="h-8 text-sm text-right"
                         placeholder="0"
                         disabled={!isEnabled}
-- 
cgit v1.2.3

