'use client' import * as React from 'react' import { Package, Plus, Trash2, Save, RefreshCw, FileText } from 'lucide-react' import { getPRItemsForBidding } from '@/lib/bidding/detail/service' import { updatePrItem } from '@/lib/bidding/detail/service' import { toast } from 'sonner' import { useSession } from 'next-auth/react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Checkbox } from '@/components/ui/checkbox' import { ProjectSelector } from '@/components/bidding/ProjectSelectorBid' import { MaterialGroupSelectorDialogSingle } from '@/components/common/material/material-group-selector-dialog-single' import { MaterialSelectorDialogSingle } from '@/components/common/selectors/material/material-selector-dialog-single' import { WbsCodeSingleSelector } from '@/components/common/selectors/wbs-code/wbs-code-single-selector' import { CostCenterSingleSelector } from '@/components/common/selectors/cost-center/cost-center-single-selector' import { GlAccountSingleSelector } from '@/components/common/selectors/gl-account/gl-account-single-selector' // PR 아이템 정보 타입 (create-bidding-dialog와 동일) interface PRItemInfo { id: number // 실제 DB ID prNumber?: string | null projectId?: number | null projectInfo?: string | null shi?: string | null quantity?: string | null quantityUnit?: string | null totalWeight?: string | null weightUnit?: string | null materialDescription?: string | null hasSpecDocument?: boolean requestedDeliveryDate?: string | null isRepresentative?: boolean // 대표 아이템 여부 // 가격 정보 annualUnitPrice?: string | null currency?: string | null // 자재 그룹 정보 (필수) materialGroupNumber?: string | null materialGroupInfo?: string | null // 자재 정보 materialNumber?: string | null materialInfo?: string | null // 단위 정보 priceUnit?: string | null purchaseUnit?: string | null materialWeight?: string | null // WBS 정보 wbsCode?: string | null wbsName?: string | null // Cost Center 정보 costCenterCode?: string | null costCenterName?: string | null // GL Account 정보 glAccountCode?: string | null glAccountName?: string | null // 내정 정보 targetUnitPrice?: string | null targetAmount?: string | null targetCurrency?: string | null // 예산 정보 budgetAmount?: string | null budgetCurrency?: string | null // 실적 정보 actualAmount?: string | null actualCurrency?: string | null } interface BiddingItemsEditorProps { biddingId: number readonly?: boolean } import { removeBiddingItem, addPRItemForBidding, getBiddingById, getBiddingConditions } from '@/lib/bidding/service' import { CreatePreQuoteRfqDialog } from './create-pre-quote-rfq-dialog' import { ProcurementItemSelectorDialogSingle } from '@/components/common/selectors/procurement-item/procurement-item-selector-dialog-single' import { Textarea } from '@/components/ui/textarea' import { Label } from '@/components/ui/label' export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItemsEditorProps) { const { data: session } = useSession() const [items, setItems] = React.useState([]) const [isLoading, setIsLoading] = React.useState(false) const [isSubmitting, setIsSubmitting] = React.useState(false) const [quantityWeightMode, setQuantityWeightMode] = React.useState<'quantity' | 'weight'>('quantity') const [costCenterDialogOpen, setCostCenterDialogOpen] = React.useState(false) const [selectedItemForCostCenter, setSelectedItemForCostCenter] = React.useState(null) const [glAccountDialogOpen, setGlAccountDialogOpen] = React.useState(false) const [selectedItemForGlAccount, setSelectedItemForGlAccount] = React.useState(null) const [wbsCodeDialogOpen, setWbsCodeDialogOpen] = React.useState(false) const [selectedItemForWbs, setSelectedItemForWbs] = React.useState(null) const [tempIdCounter, setTempIdCounter] = React.useState(0) // 임시 ID 카운터 const [deletedItemIds, setDeletedItemIds] = React.useState>(new Set()) // 삭제된 아이템 ID 추적 const [preQuoteDialogOpen, setPreQuoteDialogOpen] = React.useState(false) const [targetPriceCalculationCriteria, setTargetPriceCalculationCriteria] = React.useState('') const [biddingPicUserId, setBiddingPicUserId] = React.useState(null) const [biddingType, setBiddingType] = React.useState(null) const [biddingConditions, setBiddingConditions] = React.useState<{ paymentTerms?: string | null taxConditions?: string | null incoterms?: string | null incotermsOption?: string | null contractDeliveryDate?: string | null shippingPort?: string | null destinationPort?: string | null isPriceAdjustmentApplicable?: boolean | null sparePartOptions?: string | null } | null>(null) // 초기 데이터 로딩 - 기존 품목이 있으면 자동으로 로드 React.useEffect(() => { const loadItems = async () => { if (!biddingId) return setIsLoading(true) try { const prItems = await getPRItemsForBidding(biddingId) if (prItems && prItems.length > 0) { const formattedItems: PRItemInfo[] = prItems.map((item) => ({ id: item.id, prNumber: item.prNumber || null, projectId: item.projectId || null, projectInfo: item.projectInfo || null, shi: item.shi || null, quantity: item.quantity ? item.quantity.toString() : null, quantityUnit: item.quantityUnit || null, totalWeight: item.totalWeight ? item.totalWeight.toString() : null, weightUnit: item.weightUnit || null, materialDescription: item.itemInfo || null, hasSpecDocument: item.hasSpecDocument || false, requestedDeliveryDate: item.requestedDeliveryDate ? new Date(item.requestedDeliveryDate).toISOString().split('T')[0] : null, isRepresentative: false, // 첫 번째 아이템을 대표로 설정할 수 있음 annualUnitPrice: item.annualUnitPrice ? item.annualUnitPrice.toString() : null, currency: item.currency || 'KRW', materialGroupNumber: item.materialGroupNumber || null, materialGroupInfo: item.materialGroupInfo || null, materialNumber: item.materialNumber || null, materialInfo: item.materialInfo || null, priceUnit: item.priceUnit || null, purchaseUnit: item.purchaseUnit || null, materialWeight: item.materialWeight ? item.materialWeight.toString() : null, wbsCode: item.wbsCode || null, wbsName: item.wbsName || null, costCenterCode: item.costCenterCode || null, costCenterName: item.costCenterName || null, glAccountCode: item.glAccountCode || null, glAccountName: item.glAccountName || null, targetUnitPrice: item.targetUnitPrice ? item.targetUnitPrice.toString() : null, targetAmount: item.targetAmount ? item.targetAmount.toString() : null, targetCurrency: item.targetCurrency || 'KRW', budgetAmount: item.budgetAmount ? item.budgetAmount.toString() : null, budgetCurrency: item.budgetCurrency || 'KRW', actualAmount: item.actualAmount ? item.actualAmount.toString() : null, actualCurrency: item.actualCurrency || 'KRW', })) // 첫 번째 아이템을 대표로 설정 if (formattedItems.length > 0) { formattedItems[0].isRepresentative = true } setItems(formattedItems) setDeletedItemIds(new Set()) // 삭제 목록 초기화 // 기존 품목 로드 성공 알림 (조용히 표시, 선택적) console.log(`기존 품목 ${formattedItems.length}개를 불러왔습니다.`) } else { // 품목이 없을 때는 빈 배열로 초기화 setItems([]) setDeletedItemIds(new Set()) } } catch (error) { console.error('Failed to load items:', error) toast.error('품목 정보를 불러오는데 실패했습니다.') // 에러 발생 시에도 빈 배열로 초기화하여 UI가 깨지지 않도록 setItems([]) setDeletedItemIds(new Set()) } finally { setIsLoading(false) } } loadItems() }, [biddingId]) // 입찰 정보 및 조건 로드 (사전견적 다이얼로그용) React.useEffect(() => { const loadBiddingInfo = async () => { if (!biddingId) return try { const [bidding, conditions] = await Promise.all([ getBiddingById(biddingId), getBiddingConditions(biddingId) ]) if (bidding) { console.log('📋 bidding:', bidding.biddingType) setBiddingPicUserId(bidding.bidPicId || null) setBiddingType(bidding.biddingType || null) setTargetPriceCalculationCriteria(bidding.targetPriceCalculationCriteria || '') } if (conditions) { setBiddingConditions(conditions) } } catch (error) { console.error('Failed to load bidding info:', error) } } loadBiddingInfo() }, [biddingId]) const handleSave = async () => { setIsSubmitting(true) try { const userId = session?.user?.id?.toString() || '1' let hasError = false // 필수값 검증 for (let i = 0; i < items.length; i++) { const item = items[i]; // 필수값: 자재그룹코드, 자재그룹명 if (!item.materialGroupNumber || !item.materialGroupInfo) { toast.error(`${i + 1}번 품목의 자재그룹 정보를 입력해주세요.`); setIsSubmitting(false); return; } // 필수값: 수량 또는 중량 if (quantityWeightMode === 'quantity') { if (!item.quantity || parseFloat(item.quantity) <= 0) { toast.error(`${i + 1}번 품목의 수량을 입력해주세요.`); setIsSubmitting(false); return; } if (!item.quantityUnit) { toast.error(`${i + 1}번 품목의 수량 단위를 선택해주세요.`); setIsSubmitting(false); return; } } else { if (!item.totalWeight || parseFloat(item.totalWeight) <= 0) { toast.error(`${i + 1}번 품목의 중량을 입력해주세요.`); setIsSubmitting(false); return; } if (!item.weightUnit) { toast.error(`${i + 1}번 품목의 중량 단위를 선택해주세요.`); setIsSubmitting(false); return; } } // 필수값: 납품요청일 if (!item.requestedDeliveryDate) { toast.error(`${i + 1}번 품목의 납품요청일을 입력해주세요.`); setIsSubmitting(false); return; } // 필수값: 내정단가 (사용자 요청) if (!item.targetUnitPrice || parseFloat(item.targetUnitPrice.replace(/,/g, '')) <= 0) { toast.error(`${i + 1}번 품목의 내정단가를 입력해주세요.`); setIsSubmitting(false); return; } } // 모든 아이템을 upsert 처리 (id가 있으면 update, 없으면 insert) for (const item of items) { const targetAmount = calculateTargetAmount(item) let result if (item.id > 0) { // 기존 아이템 업데이트 result = await updatePrItem(item.id, { projectId: item.projectId || null, projectInfo: item.projectInfo || null, shi: item.shi || null, materialGroupNumber: item.materialGroupNumber || null, materialGroupInfo: item.materialGroupInfo || null, materialNumber: item.materialNumber || null, materialInfo: item.materialInfo || null, quantity: item.quantity ? parseFloat(item.quantity) : null, quantityUnit: item.quantityUnit || null, totalWeight: item.totalWeight ? parseFloat(item.totalWeight) : null, weightUnit: item.weightUnit || null, priceUnit: item.priceUnit || null, purchaseUnit: item.purchaseUnit || null, materialWeight: item.materialWeight ? parseFloat(item.materialWeight) : null, wbsCode: item.wbsCode || null, wbsName: item.wbsName || null, costCenterCode: item.costCenterCode || null, costCenterName: item.costCenterName || null, glAccountCode: item.glAccountCode || null, glAccountName: item.glAccountName || null, targetUnitPrice: item.targetUnitPrice ? parseFloat(item.targetUnitPrice.replace(/,/g, '')) : null, targetAmount: targetAmount ? parseFloat(targetAmount) : null, targetCurrency: item.targetCurrency || 'KRW', budgetAmount: item.budgetAmount ? parseFloat(item.budgetAmount.replace(/,/g, '')) : null, budgetCurrency: item.budgetCurrency || 'KRW', actualAmount: item.actualAmount ? parseFloat(item.actualAmount.replace(/,/g, '')) : null, actualCurrency: item.actualCurrency || 'KRW', requestedDeliveryDate: item.requestedDeliveryDate ? new Date(item.requestedDeliveryDate) : null, currency: item.currency || 'KRW', annualUnitPrice: item.annualUnitPrice ? parseFloat(item.annualUnitPrice) : null, prNumber: item.prNumber || null, hasSpecDocument: item.hasSpecDocument || false, } as Parameters[1], userId) } else { // 새 아이템 추가 (문자열 타입만 허용) result = await addPRItemForBidding(biddingId, { projectId: item.projectId ?? undefined, projectInfo: item.projectInfo ?? null, shi: item.shi ?? null, materialGroupNumber: item.materialGroupNumber ?? null, materialGroupInfo: item.materialGroupInfo ?? null, materialNumber: item.materialNumber ?? null, materialInfo: item.materialInfo ?? null, quantity: item.quantity ?? null, quantityUnit: item.quantityUnit ?? null, totalWeight: item.totalWeight ?? null, weightUnit: item.weightUnit ?? null, priceUnit: item.priceUnit ?? null, purchaseUnit: item.purchaseUnit ?? null, materialWeight: item.materialWeight ?? null, wbsCode: item.wbsCode ?? null, wbsName: item.wbsName ?? null, costCenterCode: item.costCenterCode ?? null, costCenterName: item.costCenterName ?? null, glAccountCode: item.glAccountCode ?? null, glAccountName: item.glAccountName ?? null, targetUnitPrice: item.targetUnitPrice ? item.targetUnitPrice.replace(/,/g, '') : null, targetAmount: targetAmount, targetCurrency: item.targetCurrency || 'KRW', budgetAmount: item.budgetAmount ? item.budgetAmount.replace(/,/g, '') : null, budgetCurrency: item.budgetCurrency || 'KRW', actualAmount: item.actualAmount ? item.actualAmount.replace(/,/g, '') : null, actualCurrency: item.actualCurrency || 'KRW', requestedDeliveryDate: item.requestedDeliveryDate ?? null, currency: item.currency || 'KRW', annualUnitPrice: item.annualUnitPrice ?? null, prNumber: item.prNumber ?? null, hasSpecDocument: item.hasSpecDocument || false, }) } if (!result.success) { hasError = true } } // 삭제된 아이템들 서버에서 삭제 for (const deletedId of deletedItemIds) { const result = await removeBiddingItem(deletedId) if (!result.success) { hasError = true } } if (hasError) { toast.error('일부 품목 정보 저장에 실패했습니다.') } else { // 내정가 산정 기준 별도 저장 (서버 액션으로 처리) if (targetPriceCalculationCriteria.trim()) { try { const { updateTargetPriceCalculationCriteria } = await import('@/lib/bidding/service') const criteriaResult = await updateTargetPriceCalculationCriteria(biddingId, targetPriceCalculationCriteria.trim(), userId) if (!criteriaResult.success) { console.warn('Failed to save target price calculation criteria:', criteriaResult.error) } } catch (error) { console.error('Failed to save target price calculation criteria:', error) } } toast.success('품목 정보가 성공적으로 저장되었습니다.') // 삭제 목록 초기화 setDeletedItemIds(new Set()) // 데이터 다시 로딩하여 최신 상태 반영 console.log('🔄 저장 후 데이터 재로드 시작 - biddingId:', biddingId) const prItems = await getPRItemsForBidding(biddingId) console.log('📦 getPRItemsForBidding 결과:', prItems) if (prItems && prItems.length > 0) { console.log('✅ 저장된 아이템 수:', prItems.length) const formattedItems: PRItemInfo[] = prItems.map((item, index) => { console.log(`🔍 아이템 ${index + 1}:`, { id: item.id, materialGroupNumber: item.materialGroupNumber, materialNumber: item.materialNumber, quantity: item.quantity }) return { id: item.id, prNumber: item.prNumber || null, projectId: item.projectId || null, projectInfo: item.projectInfo || null, shi: item.shi || null, quantity: item.quantity ? item.quantity.toString() : null, quantityUnit: item.quantityUnit || null, totalWeight: item.totalWeight ? item.totalWeight.toString() : null, weightUnit: item.weightUnit || null, materialDescription: item.itemInfo || null, hasSpecDocument: item.hasSpecDocument || false, requestedDeliveryDate: item.requestedDeliveryDate ? new Date(item.requestedDeliveryDate).toISOString().split('T')[0] : null, isRepresentative: false, annualUnitPrice: item.annualUnitPrice ? item.annualUnitPrice.toString() : null, currency: item.currency || 'KRW', materialGroupNumber: item.materialGroupNumber || null, materialGroupInfo: item.materialGroupInfo || null, materialNumber: item.materialNumber || null, materialInfo: item.materialInfo || null, priceUnit: item.priceUnit || null, purchaseUnit: item.purchaseUnit || null, materialWeight: item.materialWeight ? item.materialWeight.toString() : null, wbsCode: item.wbsCode || null, wbsName: item.wbsName || null, costCenterCode: item.costCenterCode || null, costCenterName: item.costCenterName || null, glAccountCode: item.glAccountCode || null, glAccountName: item.glAccountName || null, targetUnitPrice: item.targetUnitPrice ? item.targetUnitPrice.toString() : null, targetAmount: item.targetAmount ? item.targetAmount.toString() : null, targetCurrency: item.targetCurrency || 'KRW', budgetAmount: item.budgetAmount ? item.budgetAmount.toString() : null, budgetCurrency: item.budgetCurrency || 'KRW', actualAmount: item.actualAmount ? item.actualAmount.toString() : null, actualCurrency: item.actualCurrency || 'KRW', } }) // 첫 번째 아이템을 대표로 설정 if (formattedItems.length > 0) { formattedItems[0].isRepresentative = true } console.log('📋 최종 formattedItems:', formattedItems) setItems(formattedItems) console.log('✅ 상태 업데이트 완료') } else { console.log('❌ 저장 후 데이터가 없음 - 빈 배열 설정') // 저장 후 데이터가 없으면 빈 배열로 설정 setItems([]) } } } catch (error) { console.error('Failed to save items:', error) toast.error('품목 정보 저장에 실패했습니다.') } finally { setIsSubmitting(false) } } const handleAddItem = () => { // 임시 ID 생성 (음수로 구분하여 실제 DB ID와 구분) const tempId = -(tempIdCounter + 1) setTempIdCounter(prev => prev + 1) // 즉시 UI에 새 아이템 추가 (서버 저장 없음) const newItem: PRItemInfo = { id: tempId, // 임시 ID prNumber: null, projectId: null, projectInfo: null, shi: null, quantity: null, quantityUnit: 'EA', totalWeight: null, weightUnit: 'KG', materialDescription: null, hasSpecDocument: false, requestedDeliveryDate: null, isRepresentative: items.length === 0, annualUnitPrice: null, currency: 'KRW', materialGroupNumber: null, materialGroupInfo: null, materialNumber: null, materialInfo: null, priceUnit: 1, purchaseUnit: 'EA', materialWeight: null, wbsCode: null, wbsName: null, costCenterCode: null, costCenterName: null, glAccountCode: null, glAccountName: null, targetUnitPrice: null, targetAmount: null, targetCurrency: 'KRW', budgetAmount: null, budgetCurrency: 'KRW', actualAmount: null, actualCurrency: 'KRW', } setItems((prev) => { // 첫 번째 아이템이면 대표로 설정 if (prev.length === 0) { return [newItem] } return [...prev, newItem] }) } const handleRemoveItem = (itemId: number) => { if (items.length <= 1) { toast.error('최소 하나의 품목이 필요합니다.') return } // 실제 아이템인 경우 삭제 목록에 추가 (저장 시 서버에서 삭제됨) if (itemId > 0) { setDeletedItemIds(prev => new Set([...prev, itemId])) } // UI에서 즉시 제거 setItems((prev) => { const filteredItems = prev.filter((item) => item.id !== itemId) const removedItem = prev.find((item) => item.id === itemId) if (removedItem?.isRepresentative && filteredItems.length > 0) { filteredItems[0].isRepresentative = true } return filteredItems }) } const updatePRItem = (id: number, updates: Partial) => { setItems((prev) => prev.map((item) => { if (item.id === id) { const updatedItem = { ...item, ...updates } // 내정단가, 수량, 중량, 가격단위가 변경되면 내정금액 재계산 if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.priceUnit) { updatedItem.targetAmount = calculateTargetAmount(updatedItem) } return updatedItem } return item }) ) } const setRepresentativeItem = (id: number) => { setItems((prev) => prev.map((item) => ({ ...item, isRepresentative: item.id === id, })) ) } const handleQuantityWeightModeChange = (mode: 'quantity' | 'weight') => { setQuantityWeightMode(mode) } // 천단위 콤마 포맷팅 헬퍼 함수들 const formatNumberWithCommas = (value: string | number | null | undefined): string => { if (!value) return '' const numValue = typeof value === 'number' ? value : parseFloat(value.toString().replace(/,/g, '')) if (isNaN(numValue)) return '' return numValue.toLocaleString() } const parseNumberFromCommas = (value: string): string => { return value.replace(/,/g, '') } const calculateTargetAmount = (item: PRItemInfo): string => { const unitPrice = parseFloat(item.targetUnitPrice?.replace(/,/g, '') || '0') || 0 const priceUnit = parseFloat(item.priceUnit || '1') || 1 let amount = 0 if (quantityWeightMode === 'quantity') { const quantity = parseFloat(item.quantity || '0') || 0 amount = (quantity / priceUnit) * unitPrice } else { const weight = parseFloat(item.totalWeight || '0') || 0 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 (
품목 정보를 불러오는 중...
) } // PR 아이템 테이블 렌더링 (create-bidding-dialog와 동일한 구조) const renderPrItemsTable = () => { return (
{/* 합계 행 */} {items.map((item, index) => ( ))}
대표 # 프로젝트코드 프로젝트명 자재그룹코드 * 자재그룹명 * 자재코드 자재명 수량(중량) * 단위 * 납품요청일 * 가격단위 구매단위 자재순중량 내정단가 내정금액 내정통화 예산금액 예산통화 실적금액 실적통화 WBS코드 WBS명 코스트센터코드 코스트센터명 GL계정코드 GL계정명 PR 번호 액션
합계 - - - - - - - {quantityWeightMode === 'quantity' ? `${formatNumberWithCommas(totals.quantityTotal.toString())}` : `${formatNumberWithCommas(totals.weightTotal.toString())}` } {quantityWeightMode === 'quantity' ? `${items[0]?.quantityUnit || 'EA'}` : `${items[0]?.weightUnit || 'KG'}` } - - - - - {formatNumberWithCommas(totals.targetAmountTotal.toString())} - {formatNumberWithCommas(totals.budgetAmountTotal.toString())} - {formatNumberWithCommas(totals.actualAmountTotal.toString())} - - - - - - - - -
setRepresentativeItem(item.id)} disabled={(items.length <= 1 && item.isRepresentative) || readonly} title="대표 아이템" /> {index + 1} { if (project) { updatePRItem(item.id, { projectId: project.id, projectInfo: project.projectName }) } else { updatePRItem(item.id, { projectId: null, projectInfo: null }) } }} placeholder="프로젝트 선택" disabled={readonly} /> {biddingType !== 'equipment' ? ( { if (procurementItem) { updatePRItem(item.id, { materialGroupNumber: procurementItem.itemCode, materialGroupInfo: procurementItem.itemName }) } else { updatePRItem(item.id, { materialGroupNumber: '', materialGroupInfo: '' }) } }} title="1회성 품목 선택" description="1회성 품목을 검색하고 선택해주세요." /> ) : ( { if (material) { updatePRItem(item.id, { materialGroupNumber: material.materialGroupCode, materialGroupInfo: material.materialGroupDescription }) } else { updatePRItem(item.id, { materialGroupNumber: '', materialGroupInfo: '' }) } }} title="자재그룹 선택" description="자재그룹을 검색하고 선택해주세요." /> )} { if (material) { updatePRItem(item.id, { materialNumber: material.materialCode, materialInfo: material.materialName }) } else { updatePRItem(item.id, { materialNumber: '', materialInfo: '' }) } }} title="자재 선택" description="자재를 검색하고 선택해주세요." /> {quantityWeightMode === 'quantity' ? ( updatePRItem(item.id, { quantity: e.target.value })} className="h-8 text-xs" required disabled={readonly} /> ) : ( updatePRItem(item.id, { totalWeight: e.target.value })} className="h-8 text-xs" required disabled={readonly} /> )} {quantityWeightMode === 'quantity' ? ( ) : ( )} updatePRItem(item.id, { requestedDeliveryDate: e.target.value })} className="h-8 text-xs" required disabled={readonly} min="1900-01-01" max="2100-12-31" /> updatePRItem(item.id, { priceUnit: e.target.value })} className="h-8 text-xs" disabled={readonly} /> updatePRItem(item.id, { materialWeight: e.target.value })} className="h-8 text-xs" disabled={readonly} /> updatePRItem(item.id, { targetUnitPrice: parseNumberFromCommas(e.target.value) })} className="h-8 text-xs" disabled={readonly} /> updatePRItem(item.id, { budgetAmount: parseNumberFromCommas(e.target.value) })} className="h-8 text-xs" disabled={readonly} /> updatePRItem(item.id, { actualAmount: parseNumberFromCommas(e.target.value) })} className="h-8 text-xs" disabled={readonly} /> { setWbsCodeDialogOpen(open) if (!open) setSelectedItemForWbs(null) }} selectedCode={item.wbsCode ? { WBS_ELMT: item.wbsCode, WBS_ELMT_NM: item.wbsName || '', } : undefined} onCodeSelect={(wbsCode) => { updatePRItem(item.id, { wbsCode: wbsCode.WBS_ELMT, wbsName: wbsCode.WBS_ELMT_NM }) setWbsCodeDialogOpen(false) setSelectedItemForWbs(null) }} title="WBS 코드 선택" description="WBS 코드를 선택하세요" showConfirmButtons={false} /> { setCostCenterDialogOpen(open) if (!open) setSelectedItemForCostCenter(null) }} selectedCode={item.costCenterCode ? { KOSTL: item.costCenterCode, KTEXT: item.costCenterName || '', } : undefined} onCodeSelect={(costCenter) => { updatePRItem(item.id, { costCenterCode: costCenter.KOSTL, costCenterName: costCenter.KTEXT }) setCostCenterDialogOpen(false) setSelectedItemForCostCenter(null) }} title="코스트센터 선택" description="코스트센터를 선택하세요" showConfirmButtons={false} /> { setGlAccountDialogOpen(open) if (!open) setSelectedItemForGlAccount(null) }} selectedCode={item.glAccountCode ? { SAKNR: item.glAccountCode, TEXT1: item.glAccountName || '' } : undefined} onCodeSelect={(glAccount) => { updatePRItem(item.id, { glAccountCode: glAccount.SAKNR, glAccountName: glAccount.TEXT1 }) setGlAccountDialogOpen(false) setSelectedItemForGlAccount(null) }} title="GL 계정 선택" description="GL 계정을 선택하세요" showConfirmButtons={false} />
) } return (
입찰 품목 목록

입찰 대상 품목들을 관리합니다. 최소 하나의 아이템이 필요하며, 자재그룹코드는 필수입니다

수량/단위 또는 중량/중량단위를 선택해서 입력하세요

{/* 내정가 산정 기준 입력 폼 */}