From b75b1cd920efd61923f7b2dbc4c49987b7b0c4e1 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 20 Nov 2025 10:25:41 +0000
Subject: (최겸) 구매 입찰 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../bidding/manage/bidding-schedule-editor.tsx     | 137 +++++++++++++++++++--
 1 file changed, 129 insertions(+), 8 deletions(-)

(limited to 'components/bidding/manage/bidding-schedule-editor.tsx')

diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx
index f3260f04..b5f4aaf0 100644
--- a/components/bidding/manage/bidding-schedule-editor.tsx
+++ b/components/bidding/manage/bidding-schedule-editor.tsx
@@ -16,7 +16,7 @@ import { ApprovalPreviewDialog } from '@/lib/approval/approval-preview-dialog'
 import { requestBiddingInvitationWithApproval } from '@/lib/bidding/approval-actions'
 import { prepareBiddingApprovalData } from '@/lib/bidding/approval-actions'
 import { BiddingInvitationDialog } from '@/lib/bidding/detail/table/bidding-invitation-dialog'
-import { sendBiddingBasicContracts, getSelectedVendorsForBidding } from '@/lib/bidding/pre-quote/service'
+import { sendBiddingBasicContracts, getSelectedVendorsForBidding, getPrItemsForBidding } from '@/lib/bidding/pre-quote/service'
 import { registerBidding } from '@/lib/bidding/detail/service'
 import { useToast } from '@/hooks/use-toast'
 import { format } from 'date-fns'
@@ -61,6 +61,13 @@ interface VendorContractRequirement {
   agreementYn?: boolean
   biddingCompanyId: number
   biddingId: number
+  isPreQuoteSelected?: boolean
+  contacts?: Array<{
+    id: number
+    contactName: string
+    contactEmail: string
+    contactNumber?: string | null
+  }>
 }
 
 interface VendorWithContactInfo extends VendorContractRequirement {
@@ -216,6 +223,8 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc
           agreementYn: vendor.agreementYn,
           biddingCompanyId: vendor.biddingCompanyId,
           biddingId: vendor.biddingId,
+          isPreQuoteSelected: vendor.isPreQuoteSelected,
+          contacts: vendor.contacts || [],
         }))
       } else {
         console.error('선정된 업체 조회 실패:', 'error' in result ? result.error : '알 수 없는 오류')
@@ -237,8 +246,64 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc
   }, [isBiddingInvitationDialogOpen, getSelectedVendors])
 
   // 입찰공고 버튼 클릭 핸들러 - 입찰 초대 다이얼로그 열기
-  const handleBiddingInvitationClick = () => {
-    setIsBiddingInvitationDialogOpen(true)
+  const handleBiddingInvitationClick = async () => {
+    try {
+      // 1. 입찰서 제출기간 검증
+      if (!schedule.submissionStartDate || !schedule.submissionEndDate) {
+        toast({
+          title: '입찰서 제출기간 미설정',
+          description: '입찰서 제출 시작일시와 마감일시를 모두 설정해주세요.',
+          variant: 'destructive',
+        })
+        return
+      }
+
+      // 2. 선정된 업체들 조회 및 검증
+      const vendors = await getSelectedVendors()
+      if (vendors.length === 0) {
+        toast({
+          title: '선정된 업체 없음',
+          description: '입찰에 참여할 업체가 없습니다.',
+          variant: 'destructive',
+        })
+        return
+      }
+
+      // 3. 업체 담당자 검증
+      const vendorsWithoutContacts = vendors.filter(vendor =>
+        !vendor.contacts || vendor.contacts.length === 0
+      )
+      if (vendorsWithoutContacts.length > 0) {
+        toast({
+          title: '업체 담당자 정보 부족',
+          description: `${vendorsWithoutContacts.length}개 업체의 담당자가 없습니다. 각 업체에 담당자를 추가해주세요.`,
+          variant: 'destructive',
+        })
+        return
+      }
+
+      // 4. 입찰 품목 검증
+      const prItems = await getPrItemsForBidding(biddingId)
+      if (!prItems || prItems.length === 0) {
+        toast({
+          title: '입찰 품목 없음',
+          description: '입찰에 포함할 품목이 없습니다.',
+          variant: 'destructive',
+        })
+        return
+      }
+
+      // 모든 검증 통과 시 다이얼로그 열기
+      setSelectedVendors(vendors)
+      setIsBiddingInvitationDialogOpen(true)
+    } catch (error) {
+      console.error('입찰공고 검증 중 오류 발생:', error)
+      toast({
+        title: '오류',
+        description: '입찰공고 검증 중 오류가 발생했습니다.',
+        variant: 'destructive',
+      })
+    }
   }
 
   // 결재 상신 핸들러 - 결재 완료 시 실제 입찰 등록 실행
@@ -331,7 +396,7 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc
   // 입찰 초대 발송 핸들러 - 결재 준비 및 결재 다이얼로그 열기
   const handleBiddingInvitationSend = async (data: BiddingInvitationData) => {
     try {
-      if (!session?.user?.id || !session.user.epId) {
+      if (!session?.user?.id) {
         toast({
           title: '오류',
           description: '사용자 정보가 없습니다.',
@@ -384,7 +449,18 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc
     setIsSubmitting(true)
     try {
       const userId = session?.user?.id?.toString() || '1'
-      
+
+      // 입찰서 제출기간 필수 검증
+      if (!schedule.submissionStartDate || !schedule.submissionEndDate) {
+        toast({
+          title: '입찰서 제출기간 미설정',
+          description: '입찰서 제출 시작일시와 마감일시를 모두 설정해주세요.',
+          variant: 'destructive',
+        })
+        setIsSubmitting(false)
+        return
+      }
+
       // 사양설명회 정보 유효성 검사
       if (schedule.hasSpecificationMeeting) {
         if (!specMeetingInfo.meetingDate || !specMeetingInfo.location || !specMeetingInfo.contactPerson) {
@@ -430,8 +506,45 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc
   }
 
   const handleScheduleChange = (field: keyof BiddingSchedule, value: string | boolean) => {
+    // 마감일시 검증 - 현재일 이전 설정 불가
+    if (field === 'submissionEndDate' && typeof value === 'string' && value) {
+      const selectedDate = new Date(value)
+      const now = new Date()
+      now.setHours(0, 0, 0, 0) // 시간을 00:00:00으로 설정하여 날짜만 비교
+
+      if (selectedDate < now) {
+        toast({
+          title: '마감일시 오류',
+          description: '마감일시는 현재일 이전으로 설정할 수 없습니다.',
+          variant: 'destructive',
+        })
+        return // 변경을 적용하지 않음
+      }
+    }
+
+    // 긴급여부 미선택 시 당일 제출시작 불가
+    if (field === 'submissionStartDate' && typeof value === 'string' && value) {
+      const selectedDate = new Date(value)
+      const today = new Date()
+      today.setHours(0, 0, 0, 0) // 시간을 00:00:00으로 설정
+      selectedDate.setHours(0, 0, 0, 0)
+
+      // 현재 긴급 여부 확인 (field가 'isUrgent'인 경우 value 사용, 아니면 기존 schedule 값)
+      const isUrgent = field === 'isUrgent' ? (value as boolean) : schedule.isUrgent || false
+
+      // 긴급이 아닌 경우 당일 시작 불가
+      if (!isUrgent && selectedDate.getTime() === today.getTime()) {
+        toast({
+          title: '제출 시작일시 오류',
+          description: '긴급 입찰이 아닌 경우 당일 제출 시작은 불가능합니다.',
+          variant: 'destructive',
+        })
+        return // 변경을 적용하지 않음
+      }
+    }
+
     setSchedule(prev => ({ ...prev, [field]: value }))
-    
+
     // 사양설명회 실시 여부가 false로 변경되면 상세 정보 초기화
     if (field === 'hasSpecificationMeeting' && value === false) {
       setSpecMeetingInfo({
@@ -480,22 +593,30 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc
             </h3>
             <div className="grid grid-cols-2 gap-4">
               <div className="space-y-2">
-                <Label htmlFor="submission-start">제출 시작일시</Label>
+                <Label htmlFor="submission-start">제출 시작일시 <span className="text-red-500">*</span></Label>
                 <Input
                   id="submission-start"
                   type="datetime-local"
                   value={schedule.submissionStartDate}
                   onChange={(e) => handleScheduleChange('submissionStartDate', e.target.value)}
+                  className={!schedule.submissionStartDate ? 'border-red-200' : ''}
                 />
+                {!schedule.submissionStartDate && (
+                  <p className="text-sm text-red-500">제출 시작일시는 필수입니다</p>
+                )}
               </div>
               <div className="space-y-2">
-                <Label htmlFor="submission-end">제출 마감일시</Label>
+                <Label htmlFor="submission-end">제출 마감일시 <span className="text-red-500">*</span></Label>
                 <Input
                   id="submission-end"
                   type="datetime-local"
                   value={schedule.submissionEndDate}
                   onChange={(e) => handleScheduleChange('submissionEndDate', e.target.value)}
+                  className={!schedule.submissionEndDate ? 'border-red-200' : ''}
                 />
+                {!schedule.submissionEndDate && (
+                  <p className="text-sm text-red-500">제출 마감일시는 필수입니다</p>
+                )}
               </div>
             </div>
           </div>
-- 
cgit v1.2.3

