From 10cb50753ccf318024c4394282f9e8d968dcd1a5 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<number | null>(null) // 추가
 
+    // Procurement 데이터 상태들
+    const [paymentTermsOptions, setPaymentTermsOptions] = React.useState<Array<{code: string, description: string}>>([])
+    const [incotermsOptions, setIncotermsOptions] = React.useState<Array<{code: string, description: string}>>([])
+    const [shippingPlaces, setShippingPlaces] = React.useState<Array<{code: string, description: string}>>([])
+    const [destinationPlaces, setDestinationPlaces] = React.useState<Array<{code: string, description: string}>>([])
+    const [procurementLoading, setProcurementLoading] = React.useState(false)
+
     // 사양설명회 정보 상태
     const [specMeetingInfo, setSpecMeetingInfo] = React.useState<SpecificationMeetingInfo>({
         meetingDate: "",
@@ -157,8 +160,24 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
         meetingFiles: [], // 사양설명회 첨부파일
     })
 
-    // PR 아이템들 상태
-    const [prItems, setPrItems] = React.useState<PRItemInfo[]>([])
+    // PR 아이템들 상태 - 기본적으로 하나의 빈 아이템 생성
+    const [prItems, setPrItems] = React.useState<PRItemInfo[]>([
+        {
+            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<string | null>(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 && (
+                                                    <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span>
+                                                )}
                                             </button>
                                             <button
                                                 type="button"
@@ -927,18 +1039,34 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                             )}
                                                         />
 
-                                                        {/* 계약기간 */}
+                                                        {/* 계약 시작일 */}
                                                         <FormField
                                                             control={form.control}
-                                                            name="contractPeriod"
+                                                            name="contractStartDate"
                                                             render={({ field }) => (
                                                                 <FormItem>
-                                                                    <FormLabel>
-                                                                        계약기간 <span className="text-red-500">*</span>
-                                                                    </FormLabel>
+                                                                    <FormLabel>계약 시작일  <span className="text-red-500">*</span></FormLabel>
+                                                                    <FormControl>
+                                                                        <Input
+                                                                            type="date"
+                                                                            {...field}
+                                                                        />
+                                                                    </FormControl>
+                                                                    <FormMessage />
+                                                                </FormItem>
+                                                            )}
+                                                        />
+
+                                                        {/* 계약 종료일 */}
+                                                        <FormField
+                                                            control={form.control}
+                                                            name="contractEndDate"
+                                                            render={({ field }) => (
+                                                                <FormItem>
+                                                                    <FormLabel>계약 종료일  <span className="text-red-500">*</span></FormLabel>
                                                                     <FormControl>
                                                                         <Input
-                                                                            placeholder="예: 계약일로부터 60일"
+                                                                            type="date"
                                                                             {...field}
                                                                         />
                                                                     </FormControl>
@@ -1403,26 +1531,58 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
 
                                                         <div className="space-y-2">
                                                             <label className="text-sm font-medium">선적지 <span className="text-red-500">*</span></label>
-                                                            <Input
-                                                                placeholder="예: 부산항, 인천항"
+                                                            <Select
                                                                 value={biddingConditions.shippingPort}
-                                                                onChange={(e) => setBiddingConditions(prev => ({
+                                                                onValueChange={(value) => setBiddingConditions(prev => ({
                                                                     ...prev,
-                                                                    shippingPort: e.target.value
+                                                                    shippingPort: value
                                                                 }))}
-                                                            />
+                                                            >
+                                                                <SelectTrigger>
+                                                                    <SelectValue placeholder="선적지 선택" />
+                                                                </SelectTrigger>
+                                                                <SelectContent>
+                                                                    {shippingPlaces.length > 0 ? (
+                                                                        shippingPlaces.map((place) => (
+                                                                            <SelectItem key={place.code} value={place.code}>
+                                                                                {place.code} {place.description && `(${place.description})`}
+                                                                            </SelectItem>
+                                                                        ))
+                                                                    ) : (
+                                                                        <SelectItem value="loading" disabled>
+                                                                            데이터를 불러오는 중...
+                                                                        </SelectItem>
+                                                                    )}
+                                                                </SelectContent>
+                                                            </Select>
                                                         </div>
 
                                                         <div className="space-y-2">
-                                                            <label className="text-sm font-medium">도착지 <span className="text-red-500">*</span></label>
-                                                            <Input
-                                                                placeholder="예: 현장 직납, 창고 납품"
+                                                            <label className="text-sm font-medium">하역지 <span className="text-red-500">*</span></label>
+                                                            <Select
                                                                 value={biddingConditions.destinationPort}
-                                                                onChange={(e) => setBiddingConditions(prev => ({
+                                                                onValueChange={(value) => setBiddingConditions(prev => ({
                                                                     ...prev,
-                                                                    destinationPort: e.target.value
+                                                                    destinationPort: value
                                                                 }))}
-                                                            />
+                                                            >
+                                                                <SelectTrigger>
+                                                                    <SelectValue placeholder="하역지 선택" />
+                                                                </SelectTrigger>
+                                                                <SelectContent>
+                                                                    {destinationPlaces.length > 0 ? (
+                                                                        destinationPlaces.map((place) => (
+                                                                            <SelectItem key={place.code} value={place.code}>
+                                                                                {place.code} {place.description && `(${place.description})`}
+                                                                            </SelectItem>
+                                                                        ))
+                                                                    ) : (
+                                                                        <SelectItem value="loading" disabled>
+                                                                            데이터를 불러오는 중...
+                                                                        </SelectItem>
+                                                                    )}
+                                                                </SelectContent>
+                                                            </Select>
                                                         </div>
                                                     </div>
 
@@ -1463,7 +1623,10 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                     <div>
                                                         <CardTitle>세부내역 관리</CardTitle>
                                                         <p className="text-sm text-muted-foreground mt-1">
-                                                            PR 아이템 또는 수기 아이템을 추가하여 입찰 세부내역을 관리하세요
+                                                            최소 하나의 품목을 입력해야 합니다
+                                                        </p>
+                                                        <p className="text-xs text-amber-600 mt-1">
+                                                            수량/단위 또는 중량/중량단위를 선택해서 입력하세요
                                                         </p>
                                                     </div>
                                                     <Button
@@ -1487,7 +1650,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                                             <TableHead className="w-[60px]">대표</TableHead>
                                                                             <TableHead className="w-[120px]">PR 번호</TableHead>
                                                                             <TableHead className="w-[120px]">품목코드</TableHead>
-                                                                            <TableHead>품목정보</TableHead>
+                                                                            <TableHead>품목정보 *</TableHead>
                                                                             <TableHead className="w-[80px]">수량</TableHead>
                                                                             <TableHead className="w-[80px]">단위</TableHead>
                                                                             <TableHead className="w-[80px]">중량</TableHead>
@@ -1526,7 +1689,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                                                 </TableCell>
                                                                                 <TableCell>
                                                                                     <Input
-                                                                                        placeholder="품목정보"
+                                                                                        placeholder="품목정보 *"
                                                                                         value={item.itemInfo}
                                                                                         onChange={(e) => updatePRItem(item.id, { itemInfo: e.target.value })}
                                                                                         className="h-8"
@@ -1535,6 +1698,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                                                 <TableCell>
                                                                                     <Input
                                                                                         type="number"
+                                                                                        min="0"
                                                                                         placeholder="수량"
                                                                                         value={item.quantity}
                                                                                         onChange={(e) => updatePRItem(item.id, { quantity: e.target.value })}
@@ -1562,6 +1726,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                                                 <TableCell>
                                                                                     <Input
                                                                                         type="number"
+                                                                                        min="0"
                                                                                         placeholder="중량"
                                                                                         value={item.totalWeight}
                                                                                         onChange={(e) => updatePRItem(item.id, { totalWeight: e.target.value })}
@@ -1590,6 +1755,7 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                                                         value={item.requestedDeliveryDate}
                                                                                         onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })}
                                                                                         className="h-8"
+                                                                                        placeholder="납품요청일"
                                                                                     />
                                                                                 </TableCell>
                                                                                 <TableCell>
@@ -1612,7 +1778,9 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                                                         variant="outline"
                                                                                         size="sm"
                                                                                         onClick={() => removePRItem(item.id)}
+                                                                                        disabled={prItems.length <= 1}
                                                                                         className="h-8 w-8 p-0"
+                                                                                        title={prItems.length <= 1 ? "최소 하나의 품목이 필요합니다" : "품목 삭제"}
                                                                                     >
                                                                                         <Trash2 className="h-4 w-4" />
                                                                                     </Button>
@@ -1978,7 +2146,14 @@ export function CreateBiddingDialog({ paymentTermsOptions = [], incotermsOptions
                                                 )}
                                             </span>
                                         )}
-                                        {activeTab === "details" && "세부내역 아이템을 관리하세요 (선택사항)"}
+                                        {activeTab === "details" && (
+                                            <span>
+                                                최소 하나의 품목을 입력하세요
+                                                {!tabValidation.details.isValid && (
+                                                    <span className="text-red-500 ml-2">• 필수 항목이 누락되었습니다</span>
+                                                )}
+                                            </span>
+                                        )}
                                         {activeTab === "manager" && "담당자 정보를 확인하고 입찰을 생성하세요"}
                                     </div>
 
-- 
cgit v1.2.3

