From 10cb50753ccf318024c4394282f9e8d968dcd1a5 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 17 Sep 2025 10:40:12 +0000 Subject: (최겸) 구매 입찰 오류 수정 및 선적지,하역지 연동,TO Cont, TO PO 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/list/create-bidding-dialog.tsx | 249 ++++++++++++++++++++++++----- 1 file changed, 212 insertions(+), 37 deletions(-) (limited to 'lib/bidding/list/create-bidding-dialog.tsx') diff --git a/lib/bidding/list/create-bidding-dialog.tsx b/lib/bidding/list/create-bidding-dialog.tsx index 4fc4fd7b..a25dd363 100644 --- a/lib/bidding/list/create-bidding-dialog.tsx +++ b/lib/bidding/list/create-bidding-dialog.tsx @@ -67,7 +67,8 @@ import { } from "@/components/ui/file-list" import { Checkbox } from "@/components/ui/checkbox" -import { createBidding, type CreateBiddingInput, getActivePaymentTerms, getActiveIncoterms } from "@/lib/bidding/service" +import { createBidding, type CreateBiddingInput } from "@/lib/bidding/service" +import { getIncotermsForSelection, getPaymentTermsForSelection, getPlaceOfShippingForSelection, getPlaceOfDestinationForSelection } from "@/lib/procurement-select/service" import { createBiddingSchema, type CreateBiddingSchema @@ -127,12 +128,7 @@ interface PRItemInfo { const TAB_ORDER = ["basic", "contract", "schedule", "conditions", "details", "manager"] as const type TabType = typeof TAB_ORDER[number] -interface CreateBiddingDialogProps { - paymentTermsOptions?: Array<{code: string, description: string}> - incotermsOptions?: Array<{code: string, description: string}> -} - -export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions = [] }: CreateBiddingDialogProps) { +export function CreateBiddingDialog() { const router = useRouter() const [isSubmitting, setIsSubmitting] = React.useState(false) const { data: session } = useSession() @@ -141,6 +137,13 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions const [showSuccessDialog, setShowSuccessDialog] = React.useState(false) // 추가 const [createdBiddingId, setCreatedBiddingId] = React.useState(null) // 추가 + // Procurement 데이터 상태들 + const [paymentTermsOptions, setPaymentTermsOptions] = React.useState>([]) + const [incotermsOptions, setIncotermsOptions] = React.useState>([]) + const [shippingPlaces, setShippingPlaces] = React.useState>([]) + const [destinationPlaces, setDestinationPlaces] = React.useState>([]) + const [procurementLoading, setProcurementLoading] = React.useState(false) + // 사양설명회 정보 상태 const [specMeetingInfo, setSpecMeetingInfo] = React.useState({ meetingDate: "", @@ -157,8 +160,24 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions meetingFiles: [], // 사양설명회 첨부파일 }) - // PR 아이템들 상태 - const [prItems, setPrItems] = React.useState([]) + // PR 아이템들 상태 - 기본적으로 하나의 빈 아이템 생성 + const [prItems, setPrItems] = React.useState([ + { + id: `pr-default`, + prNumber: "", + itemCode: "", + itemInfo: "", + quantity: "", + quantityUnit: "EA", + totalWeight: "", + weightUnit: "KG", + materialDescription: "", + hasSpecDocument: false, + requestedDeliveryDate: "", + specFiles: [], + isRepresentative: true, // 첫 번째 아이템은 대표 아이템 + } + ]) // 파일 첨부를 위해 선택된 아이템 ID const [selectedItemForFile, setSelectedItemForFile] = React.useState(null) @@ -175,6 +194,69 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions sparePartOptions: "", }) + // Procurement 데이터 로드 함수들 + const loadPaymentTerms = React.useCallback(async () => { + setProcurementLoading(true); + try { + const data = await getPaymentTermsForSelection(); + setPaymentTermsOptions(data); + } catch (error) { + console.error("Failed to load payment terms:", error); + toast.error("결제조건 목록을 불러오는데 실패했습니다."); + } finally { + setProcurementLoading(false); + } + }, []); + + const loadIncoterms = React.useCallback(async () => { + setProcurementLoading(true); + try { + const data = await getIncotermsForSelection(); + setIncotermsOptions(data); + } catch (error) { + console.error("Failed to load incoterms:", error); + toast.error("운송조건 목록을 불러오는데 실패했습니다."); + } finally { + setProcurementLoading(false); + } + }, []); + + const loadShippingPlaces = React.useCallback(async () => { + setProcurementLoading(true); + try { + const data = await getPlaceOfShippingForSelection(); + setShippingPlaces(data); + } catch (error) { + console.error("Failed to load shipping places:", error); + toast.error("선적지 목록을 불러오는데 실패했습니다."); + } finally { + setProcurementLoading(false); + } + }, []); + + const loadDestinationPlaces = React.useCallback(async () => { + setProcurementLoading(true); + try { + const data = await getPlaceOfDestinationForSelection(); + setDestinationPlaces(data); + } catch (error) { + console.error("Failed to load destination places:", error); + toast.error("하역지 목록을 불러오는데 실패했습니다."); + } finally { + setProcurementLoading(false); + } + }, []); + + // 다이얼로그 열릴 때 procurement 데이터 로드 + React.useEffect(() => { + if (open) { + loadPaymentTerms(); + loadIncoterms(); + loadShippingPlaces(); + loadDestinationPlaces(); + } + }, [open, loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces]) + // 사양설명회 파일 추가 const addMeetingFiles = (files: File[]) => { @@ -211,7 +293,8 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions contractType: "general", biddingType: "equipment", awardCount: "single", - contractPeriod: "", + contractStartDate: "", + contractEndDate: "", submissionStartDate: "", submissionEndDate: "", @@ -268,9 +351,10 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions isValid: formValues.contractType && formValues.biddingType && formValues.awardCount && - formValues.contractPeriod.trim() !== "" && + formValues.contractStartDate && + formValues.contractEndDate && formValues.currency, - hasErrors: !!(formErrors.contractType || formErrors.biddingType || formErrors.awardCount || formErrors.contractPeriod || formErrors.currency) + hasErrors: !!(formErrors.contractType || formErrors.biddingType || formErrors.awardCount || formErrors.contractStartDate || formErrors.contractEndDate || formErrors.currency) }, schedule: { isValid: formValues.submissionStartDate && @@ -289,7 +373,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions hasErrors: false }, details: { - isValid: true, // 세부내역은 선택사항 + isValid: prItems.length > 0, hasErrors: false }, manager: { @@ -369,6 +453,12 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions // PR 아이템 제거 const removePRItem = (id: string) => { + // 최소 하나의 아이템은 유지해야 함 + if (prItems.length <= 1) { + toast.error("최소 하나의 품목이 필요합니다.") + return + } + setPrItems(prev => { const filteredItems = prev.filter(item => item.id !== id) // 만약 대표 아이템을 삭제했다면, 첫 번째 아이템을 대표로 설정 @@ -443,7 +533,9 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions toast.error("제출 시작일시와 마감일시를 입력해주세요") } } else if (activeTab === "conditions") { - toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건, 계약납품일, 선적지, 도착지)") + toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건, 계약납품일, 선적지, 하역지)") + } else if (activeTab === "details") { + toast.error("품목정보, 수량/단위 또는 중량/중량단위를 입력해주세요") } return } @@ -524,7 +616,8 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions contractType: "general", biddingType: "equipment", awardCount: "single", - contractPeriod: "", + contractStartDate: "", + contractEndDate: "", submissionStartDate: "", submissionEndDate: "", hasSpecificationMeeting: false, @@ -556,7 +649,23 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions isRequired: false, meetingFiles: [], }) - setPrItems([]) + setPrItems([ + { + id: `pr-default`, + prNumber: "", + itemCode: "", + itemInfo: "", + quantity: "", + quantityUnit: "EA", + totalWeight: "", + weightUnit: "KG", + materialDescription: "", + hasSpecDocument: false, + requestedDeliveryDate: "", + specFiles: [], + isRepresentative: true, // 첫 번째 아이템은 대표 아이템 + } + ]) setSelectedItemForFile(null) setBiddingConditions({ paymentTerms: "", @@ -705,6 +814,9 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions }`} > 세부내역 + {!tabValidation.details.isValid && ( + + )} @@ -1978,7 +2146,14 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions )} )} - {activeTab === "details" && "세부내역 아이템을 관리하세요 (선택사항)"} + {activeTab === "details" && ( + + 최소 하나의 품목을 입력하세요 + {!tabValidation.details.isValid && ( + • 필수 항목이 누락되었습니다 + )} + + )} {activeTab === "manager" && "담당자 정보를 확인하고 입찰을 생성하세요"} -- cgit v1.2.3