From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 7 Nov 2025 08:39:04 +0000
Subject: (최겸) 입찰/견적 수정사항
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../detail/general-contract-items-table.tsx        | 602 +++++++++++++++++++++
 1 file changed, 602 insertions(+)
 create mode 100644 lib/general-contracts_old/detail/general-contract-items-table.tsx

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

diff --git a/lib/general-contracts_old/detail/general-contract-items-table.tsx b/lib/general-contracts_old/detail/general-contract-items-table.tsx
new file mode 100644
index 00000000..1b9a1a06
--- /dev/null
+++ b/lib/general-contracts_old/detail/general-contract-items-table.tsx
@@ -0,0 +1,602 @@
+'use client'
+
+import * as React from 'react'
+import { Card, CardContent, CardHeader } from '@/components/ui/card'
+import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Button } from '@/components/ui/button'
+import { Checkbox } from '@/components/ui/checkbox'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from '@/components/ui/table'
+import {
+  Package,
+  Plus,
+  Trash2,
+} from 'lucide-react'
+import { toast } from 'sonner'
+import { updateContractItems, getContractItems } from '../service'
+import { Save, LoaderIcon } from 'lucide-react'
+
+interface ContractItem {
+  id?: number
+  itemCode: string
+  itemInfo: string
+  specification: string
+  quantity: number
+  quantityUnit: string
+  totalWeight: number
+  weightUnit: string
+  contractDeliveryDate: string
+  contractUnitPrice: number
+  contractAmount: number
+  contractCurrency: string
+  isSelected?: boolean
+  [key: string]: unknown
+}
+
+interface ContractItemsTableProps {
+  contractId: number
+  items: ContractItem[]
+  onItemsChange: (items: ContractItem[]) => void
+  onTotalAmountChange: (total: number) => void
+  availableBudget?: number
+  readOnly?: boolean
+}
+
+// 통화 목록
+const CURRENCIES = ["USD", "EUR", "KRW", "JPY", "CNY"];
+
+// 수량 단위 목록
+const QUANTITY_UNITS = ["KG", "TON", "EA", "M", "M2", "M3", "L", "ML", "G", "SET", "PCS"];
+
+// 중량 단위 목록
+const WEIGHT_UNITS = ["KG", "TON", "G", "LB", "OZ"];
+
+export function ContractItemsTable({
+  contractId,
+  items,
+  onItemsChange,
+  onTotalAmountChange,
+  availableBudget = 0,
+  readOnly = false
+}: ContractItemsTableProps) {
+  const [localItems, setLocalItems] = React.useState<ContractItem[]>(items)
+  const [isSaving, setIsSaving] = React.useState(false)
+  const [isLoading, setIsLoading] = React.useState(false)
+  const [isEnabled, setIsEnabled] = React.useState(true)
+
+  // 초기 데이터 로드
+  React.useEffect(() => {
+    const loadItems = async () => {
+      try {
+        setIsLoading(true)
+        const fetchedItems = await getContractItems(contractId)
+        const formattedItems = fetchedItems.map(item => ({
+          id: item.id,
+          itemCode: item.itemCode || '',
+          itemInfo: item.itemInfo || '',
+          specification: item.specification || '',
+          quantity: Number(item.quantity) || 0,
+          quantityUnit: item.quantityUnit || 'EA',
+          totalWeight: Number(item.totalWeight) || 0,
+          weightUnit: item.weightUnit || 'KG',
+          contractDeliveryDate: item.contractDeliveryDate || '',
+          contractUnitPrice: Number(item.contractUnitPrice) || 0,
+          contractAmount: Number(item.contractAmount) || 0,
+          contractCurrency: item.contractCurrency || 'KRW',
+          isSelected: false
+        })) as ContractItem[]
+        setLocalItems(formattedItems as ContractItem[])
+        onItemsChange(formattedItems as ContractItem[])
+      } catch (error) {
+        console.error('Error loading contract items:', error)
+        // 기본 빈 배열로 설정
+        setLocalItems([])
+        onItemsChange([])
+      } finally {
+        setIsLoading(false)
+      }
+    }
+
+    loadItems()
+  }, [contractId, onItemsChange])
+
+  // 로컬 상태와 부모 상태 동기화 (초기 로드 후에는 부모 상태 우선)
+  React.useEffect(() => {
+    if (items.length > 0) {
+      setLocalItems(items)
+    }
+  }, [items])
+
+  const handleSaveItems = async () => {
+    try {
+      setIsSaving(true)
+      
+      // validation 체크
+      const errors: string[] = []
+      for (let index = 0; index < localItems.length; index++) {
+        const item = localItems[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}번째 품목의 수량`)
+        if (!item.contractUnitPrice || item.contractUnitPrice <= 0) errors.push(`${index + 1}번째 품목의 단가`)
+        if (!item.contractDeliveryDate) errors.push(`${index + 1}번째 품목의 납기일`)
+      }
+      
+      if (errors.length > 0) {
+        toast.error(`다음 항목을 입력해주세요: ${errors.join(', ')}`)
+        return
+      }
+      
+      await updateContractItems(contractId, localItems as any)
+      toast.success('품목정보가 저장되었습니다.')
+    } catch (error) {
+      console.error('Error saving contract items:', error)
+      toast.error('품목정보 저장 중 오류가 발생했습니다.')
+    } finally {
+      setIsSaving(false)
+    }
+  }
+
+  // 총 금액 계산
+  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 amountDifference = availableBudget - totalAmount
+  const budgetRatio = availableBudget > 0 ? (totalAmount / availableBudget) * 100 : 0
+
+  // 부모 컴포넌트에 총 금액 전달
+  React.useEffect(() => {
+    onTotalAmountChange(totalAmount)
+  }, [totalAmount, onTotalAmountChange])
+
+  // 아이템 업데이트
+  const updateItem = (index: number, field: keyof ContractItem, value: string | number | boolean | undefined) => {
+    const updatedItems = [...localItems]
+    updatedItems[index] = { ...updatedItems[index], [field]: value }
+    
+    // 단가나 수량이 변경되면 금액 자동 계산
+    if (field === 'contractUnitPrice' || field === 'quantity') {
+      const item = updatedItems[index]
+      updatedItems[index].contractAmount = item.contractUnitPrice * item.quantity
+    }
+    
+    setLocalItems(updatedItems)
+    onItemsChange(updatedItems)
+  }
+
+  // 행 추가
+  const addRow = () => {
+    const newItem: ContractItem = {
+      itemCode: '',
+      itemInfo: '',
+      specification: '',
+      quantity: 0,
+      quantityUnit: 'EA', // 기본 수량 단위
+      totalWeight: 0,
+      weightUnit: 'KG', // 기본 중량 단위
+      contractDeliveryDate: '',
+      contractUnitPrice: 0,
+      contractAmount: 0,
+      contractCurrency: 'KRW', // 기본 통화
+      isSelected: false
+    }
+    const updatedItems = [...localItems, newItem]
+    setLocalItems(updatedItems)
+    onItemsChange(updatedItems)
+  }
+
+  // 선택된 행 삭제
+  const deleteSelectedRows = () => {
+    const selectedIndices = localItems
+      .map((item, index) => item.isSelected ? index : -1)
+      .filter(index => index !== -1)
+    
+    if (selectedIndices.length === 0) {
+      toast.error("삭제할 행을 선택해주세요.")
+      return
+    }
+
+    const updatedItems = localItems.filter((_, index) => !selectedIndices.includes(index))
+    setLocalItems(updatedItems)
+    onItemsChange(updatedItems)
+    toast.success(`${selectedIndices.length}개 행이 삭제되었습니다.`)
+  }
+
+  // 전체 선택/해제
+  const toggleSelectAll = (checked: boolean) => {
+    const updatedItems = localItems.map(item => ({ ...item, isSelected: checked }))
+    setLocalItems(updatedItems)
+    onItemsChange(updatedItems)
+  }
+
+
+  // 통화 포맷팅
+  const formatCurrency = (amount: number, currency: string = 'KRW') => {
+    return new Intl.NumberFormat('ko-KR', {
+      style: 'currency',
+      currency: currency,
+    }).format(amount)
+  }
+
+  const allSelected = localItems.length > 0 && localItems.every(item => item.isSelected)
+  const someSelected = localItems.some(item => item.isSelected)
+
+  if (isLoading) {
+    return (
+      <Accordion type="single" collapsible className="w-full">
+        <AccordionItem value="items">
+          <AccordionTrigger className="hover:no-underline">
+            <div className="flex items-center gap-2">
+              <Package className="w-5 h-5" />
+              <span>품목 정보</span>
+              <span className="text-sm text-gray-500">(로딩 중...)</span>
+            </div>
+          </AccordionTrigger>
+          <AccordionContent>
+            <div className="flex items-center justify-center py-8">
+              <LoaderIcon className="w-6 h-6 animate-spin mr-2" />
+              <span>품목 정보를 불러오는 중...</span>
+            </div>
+          </AccordionContent>
+        </AccordionItem>
+      </Accordion>
+    )
+  }
+
+  return (
+    <Accordion type="single" collapsible className="w-full">
+      <AccordionItem value="items">
+        <AccordionTrigger className="hover:no-underline">
+          <div className="flex items-center gap-3 w-full">
+            <Package className="w-5 h-5" />
+            <span className="font-medium">품목 정보</span>
+            <span className="text-sm text-gray-500">({localItems.length}개 품목)</span>
+          </div>
+        </AccordionTrigger>
+        <AccordionContent>
+          <Card>
+            <CardHeader>
+              {/* 체크박스 */}
+              <div className="flex items-center gap-2 mb-4">
+                <Checkbox 
+                  checked={isEnabled}
+                  onCheckedChange={(checked) => setIsEnabled(checked as boolean)}
+                  disabled={readOnly}
+                />
+                <span className="text-sm font-medium">품목 정보 활성화</span>
+              </div>
+              
+              <div className="flex items-center justify-between">
+                <div className="flex items-center gap-2">
+                  <span className="text-sm text-gray-600">총 금액: {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}</span>
+                  <span className="text-sm text-gray-600">총 수량: {totalQuantity.toLocaleString()}</span>
+                </div>
+                {!readOnly && (
+                  <div className="flex items-center gap-2">
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      onClick={addRow}
+                      disabled={!isEnabled}
+                      className="flex items-center gap-2"
+                    >
+                      <Plus className="w-4 h-4" />
+                      행 추가
+                    </Button>
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      onClick={deleteSelectedRows}
+                      disabled={!isEnabled}
+                      className="flex items-center gap-2 text-red-600 hover:text-red-700"
+                    >
+                      <Trash2 className="w-4 h-4" />
+                      행 삭제
+                    </Button>
+                    <Button
+                      onClick={handleSaveItems}
+                      disabled={isSaving || !isEnabled}
+                      className="flex items-center gap-2"
+                    >
+                      {isSaving ? (
+                        <LoaderIcon className="w-4 h-4 animate-spin" />
+                      ) : (
+                        <Save className="w-4 h-4" />
+                      )}
+                      품목정보 저장
+                    </Button>
+                  </div>
+                )}
+        </div>
+        
+        {/* 요약 정보 */}
+        <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
+          <div className="space-y-1">
+            <Label className="text-sm font-medium">총 계약금액</Label>
+            <div className="text-lg font-bold text-primary">
+              {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}
+            </div>
+          </div>
+          <div className="space-y-1">
+            <Label className="text-sm font-medium">가용예산</Label>
+            <div className="text-lg font-bold">
+              {formatCurrency(availableBudget, localItems[0]?.contractCurrency || 'KRW')}
+            </div>
+          </div>
+          <div className="space-y-1">
+            <Label className="text-sm font-medium">가용예산 比 (금액차)</Label>
+            <div className={`text-lg font-bold ${amountDifference >= 0 ? 'text-green-600' : 'text-red-600'}`}>
+              {formatCurrency(amountDifference, localItems[0]?.contractCurrency || 'KRW')}
+            </div>
+          </div>
+          <div className="space-y-1">
+            <Label className="text-sm font-medium">가용예산 比 (비율)</Label>
+            <div className={`text-lg font-bold ${budgetRatio <= 100 ? 'text-green-600' : 'text-red-600'}`}>
+              {budgetRatio.toFixed(1)}%
+            </div>
+          </div>
+        </div>
+      </CardHeader>
+      
+      <CardContent>
+        <div className="overflow-x-auto">
+          <Table>
+            <TableHeader>
+              <TableRow className="border-b-2">
+                <TableHead className="w-12 px-2">
+                  {!readOnly && (
+                    <Checkbox
+                      checked={allSelected}
+                      ref={(el) => {
+                        if (el) (el as HTMLInputElement & { indeterminate?: boolean }).indeterminate = someSelected && !allSelected
+                      }}
+                      onCheckedChange={toggleSelectAll}
+                      disabled={!isEnabled}
+                    />
+                  )}
+                </TableHead>
+                <TableHead className="px-3 py-3 font-semibold">품목코드 (PKG No.)</TableHead>
+                <TableHead className="px-3 py-3 font-semibold">Item 정보 (자재그룹 / 자재코드)</TableHead>
+                <TableHead className="px-3 py-3 font-semibold">규격</TableHead>
+                <TableHead className="px-3 py-3 font-semibold text-right">수량</TableHead>
+                <TableHead className="px-3 py-3 font-semibold">수량단위</TableHead>
+                <TableHead className="px-3 py-3 font-semibold text-right">총 중량</TableHead>
+                <TableHead className="px-3 py-3 font-semibold">중량단위</TableHead>
+                <TableHead className="px-3 py-3 font-semibold">계약납기일</TableHead>
+                <TableHead className="px-3 py-3 font-semibold text-right">계약단가</TableHead>
+                <TableHead className="px-3 py-3 font-semibold text-right">계약금액</TableHead>
+                <TableHead className="px-3 py-3 font-semibold">계약통화</TableHead>
+              </TableRow>
+            </TableHeader>
+            <TableBody>
+              {localItems.map((item, index) => (
+                <TableRow key={index} className="hover:bg-muted/30 transition-colors">
+                  <TableCell className="px-2">
+                    {!readOnly && (
+                      <Checkbox
+                        checked={item.isSelected || false}
+                        onCheckedChange={(checked) =>
+                          updateItem(index, 'isSelected', checked)
+                        }
+                        disabled={!isEnabled}
+                      />
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <span className="text-sm">{item.itemCode || '-'}</span>
+                    ) : (
+                      <Input
+                        value={item.itemCode}
+                        onChange={(e) => updateItem(index, 'itemCode', e.target.value)}
+                        placeholder="품목코드"
+                        className="h-8 text-sm"
+                        disabled={!isEnabled}
+                      />
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <span className="text-sm">{item.itemInfo || '-'}</span>
+                    ) : (
+                      <Input
+                        value={item.itemInfo}
+                        onChange={(e) => updateItem(index, 'itemInfo', e.target.value)}
+                        placeholder="Item 정보"
+                        className="h-8 text-sm"
+                        disabled={!isEnabled}
+                      />
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <span className="text-sm">{item.specification || '-'}</span>
+                    ) : (
+                      <Input
+                        value={item.specification}
+                        onChange={(e) => updateItem(index, 'specification', e.target.value)}
+                        placeholder="규격"
+                        className="h-8 text-sm"
+                        disabled={!isEnabled}
+                      />
+                    )}
+                  </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)}
+                        className="h-8 text-sm text-right"
+                        placeholder="0"
+                        disabled={!isEnabled}
+                      />
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <span className="text-sm">{item.quantityUnit || '-'}</span>
+                    ) : (
+                      <Select
+                        value={item.quantityUnit}
+                        onValueChange={(value) => updateItem(index, 'quantityUnit', value)}
+                        disabled={!isEnabled}
+                      >
+                        <SelectTrigger className="h-8 text-sm w-20">
+                          <SelectValue />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {QUANTITY_UNITS.map((unit) => (
+                            <SelectItem key={unit} value={unit}>
+                              {unit}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <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)}
+                        className="h-8 text-sm text-right"
+                        placeholder="0"
+                        disabled={!isEnabled}
+                      />
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <span className="text-sm">{item.weightUnit || '-'}</span>
+                    ) : (
+                      <Select
+                        value={item.weightUnit}
+                        onValueChange={(value) => updateItem(index, 'weightUnit', value)}
+                        disabled={!isEnabled}
+                      >
+                        <SelectTrigger className="h-8 text-sm w-20">
+                          <SelectValue />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {WEIGHT_UNITS.map((unit) => (
+                            <SelectItem key={unit} value={unit}>
+                              {unit}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <span className="text-sm">{item.contractDeliveryDate || '-'}</span>
+                    ) : (
+                      <Input
+                        type="date"
+                        value={item.contractDeliveryDate}
+                        onChange={(e) => updateItem(index, 'contractDeliveryDate', e.target.value)}
+                        className="h-8 text-sm"
+                        disabled={!isEnabled}
+                      />
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <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)}
+                        className="h-8 text-sm text-right"
+                        placeholder="0"
+                        disabled={!isEnabled}
+                      />
+                    )}
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    <div className="font-semibold text-primary text-right text-sm">
+                      {formatCurrency(item.contractAmount)}
+                    </div>
+                  </TableCell>
+                  <TableCell className="px-3 py-3">
+                    {readOnly ? (
+                      <span className="text-sm">{item.contractCurrency || '-'}</span>
+                    ) : (
+                      <Select
+                        value={item.contractCurrency}
+                        onValueChange={(value) => updateItem(index, 'contractCurrency', value)}
+                        disabled={!isEnabled}
+                      >
+                        <SelectTrigger className="h-8 text-sm w-20">
+                          <SelectValue />
+                        </SelectTrigger>
+                        <SelectContent>
+                          {CURRENCIES.map((currency) => (
+                            <SelectItem key={currency} value={currency}>
+                              {currency}
+                            </SelectItem>
+                          ))}
+                        </SelectContent>
+                      </Select>
+                    )}
+                  </TableCell>
+                </TableRow>
+              ))}
+            </TableBody>
+          </Table>
+        </div>
+
+        {/* 합계 정보 */}
+        {localItems.length > 0 && (
+          <div className="mt-6 flex justify-end">
+            <Card className="w-80 bg-gradient-to-r from-primary/5 to-primary/10 border-primary/20">
+              <CardContent className="p-6">
+                <div className="space-y-4">
+                  <div className="flex items-center justify-between">
+                    <span className="text-sm font-medium text-muted-foreground">총 수량</span>
+                    <span className="text-lg font-semibold">
+                      {totalQuantity.toLocaleString()} {localItems[0]?.quantityUnit || 'KG'}
+                    </span>
+                  </div>
+                  <div className="flex items-center justify-between">
+                    <span className="text-sm font-medium text-muted-foreground">총 단가</span>
+                    <span className="text-lg font-semibold">
+                      {formatCurrency(totalUnitPrice, localItems[0]?.contractCurrency || 'KRW')}
+                    </span>
+                  </div>
+                  <div className="border-t pt-4">
+                    <div className="flex items-center justify-between">
+                      <span className="text-xl font-bold text-primary">합계 금액</span>
+                      <span className="text-2xl font-bold text-primary">
+                        {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}
+                      </span>
+                    </div>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          </div>
+        )}
+            </CardContent>
+          </Card>
+        </AccordionContent>
+      </AccordionItem>
+    </Accordion>
+  )
+}
-- 
cgit v1.2.3

