'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(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 (
품목 정보 (로딩 중...)
품목 정보를 불러오는 중...
) } return (
품목 정보 ({localItems.length}개 품목)
{/* 체크박스 */}
setIsEnabled(checked as boolean)} disabled={readOnly} /> 품목 정보 활성화
총 금액: {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')} 총 수량: {totalQuantity.toLocaleString()}
{!readOnly && (
)}
{/* 요약 정보 */}
{formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}
{formatCurrency(availableBudget, localItems[0]?.contractCurrency || 'KRW')}
= 0 ? 'text-green-600' : 'text-red-600'}`}> {formatCurrency(amountDifference, localItems[0]?.contractCurrency || 'KRW')}
{budgetRatio.toFixed(1)}%
{!readOnly && ( { if (el) (el as HTMLInputElement & { indeterminate?: boolean }).indeterminate = someSelected && !allSelected }} onCheckedChange={toggleSelectAll} disabled={!isEnabled} /> )} 품목코드 (PKG No.) Item 정보 (자재그룹 / 자재코드) 규격 수량 수량단위 총 중량 중량단위 계약납기일 계약단가 계약금액 계약통화 {localItems.map((item, index) => ( {!readOnly && ( updateItem(index, 'isSelected', checked) } disabled={!isEnabled} /> )} {readOnly ? ( {item.itemCode || '-'} ) : ( updateItem(index, 'itemCode', e.target.value)} placeholder="품목코드" className="h-8 text-sm" disabled={!isEnabled} /> )} {readOnly ? ( {item.itemInfo || '-'} ) : ( updateItem(index, 'itemInfo', e.target.value)} placeholder="Item 정보" className="h-8 text-sm" disabled={!isEnabled} /> )} {readOnly ? ( {item.specification || '-'} ) : ( updateItem(index, 'specification', e.target.value)} placeholder="규격" className="h-8 text-sm" disabled={!isEnabled} /> )} {readOnly ? ( {item.quantity.toLocaleString()} ) : ( updateItem(index, 'quantity', parseFloat(e.target.value) || 0)} className="h-8 text-sm text-right" placeholder="0" disabled={!isEnabled} /> )} {readOnly ? ( {item.quantityUnit || '-'} ) : ( )} {readOnly ? ( {item.totalWeight.toLocaleString()} ) : ( updateItem(index, 'totalWeight', parseFloat(e.target.value) || 0)} className="h-8 text-sm text-right" placeholder="0" disabled={!isEnabled} /> )} {readOnly ? ( {item.weightUnit || '-'} ) : ( )} {readOnly ? ( {item.contractDeliveryDate || '-'} ) : ( updateItem(index, 'contractDeliveryDate', e.target.value)} className="h-8 text-sm" disabled={!isEnabled} /> )} {readOnly ? ( {item.contractUnitPrice.toLocaleString()} ) : ( updateItem(index, 'contractUnitPrice', parseFloat(e.target.value) || 0)} className="h-8 text-sm text-right" placeholder="0" disabled={!isEnabled} /> )}
{formatCurrency(item.contractAmount)}
{readOnly ? ( {item.contractCurrency || '-'} ) : ( )}
))}
{/* 합계 정보 */} {localItems.length > 0 && (
총 수량 {totalQuantity.toLocaleString()} {localItems[0]?.quantityUnit || 'KG'}
총 단가 {formatCurrency(totalUnitPrice, localItems[0]?.contractCurrency || 'KRW')}
합계 금액 {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}
)}
) }