summaryrefslogtreecommitdiff
path: root/components/bidding/manage/bidding-schedule-editor.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/bidding/manage/bidding-schedule-editor.tsx')
-rw-r--r--components/bidding/manage/bidding-schedule-editor.tsx137
1 files changed, 129 insertions, 8 deletions
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>