diff options
| author | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2025-11-10 11:25:19 +0900 |
|---|---|---|
| committer | TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> | 2025-11-10 11:25:19 +0900 |
| commit | a5501ad1d1cb836d2b2f84e9b0f06049e22c901e (patch) | |
| tree | 667ed8c5d6ec35b109190e9f976d66ae54def4ce /lib/bidding/detail/service.ts | |
| parent | b0fe980376fcf1a19ff4b90851ca8b01f378fdc0 (diff) | |
| parent | f8a38907911d940cb2e8e6c9aa49488d05b2b578 (diff) | |
Merge remote-tracking branch 'origin/dujinkim' into master_homemaster
Diffstat (limited to 'lib/bidding/detail/service.ts')
| -rw-r--r-- | lib/bidding/detail/service.ts | 160 |
1 files changed, 97 insertions, 63 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index 404bc3cd..d58ded8e 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -81,6 +81,7 @@ export interface QuotationVendor { vendorId: number vendorName: string vendorCode: string + vendorEmail?: string // 벤더의 기본 이메일 contactPerson: string contactEmail: string contactPhone: string @@ -90,7 +91,7 @@ export interface QuotationVendor { isWinner: boolean | null // 낙찰여부 (null: 미정, true: 낙찰, false: 탈락) awardRatio: number | null // 발주비율 isBiddingParticipated: boolean | null // 본입찰 참여여부 - status: 'pending' | 'submitted' | 'selected' | 'rejected' + invitationStatus: 'pending' | 'pre_quote_sent' | 'pre_quote_accepted' | 'pre_quote_declined' | 'pre_quote_submitted' | 'bidding_sent' | 'bidding_accepted' | 'bidding_declined' | 'bidding_cancelled' | 'bidding_submitted' documents: Array<{ id: number fileName: string @@ -241,6 +242,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV vendorId: biddingCompanies.companyId, vendorName: vendors.vendorName, vendorCode: vendors.vendorCode, + vendorEmail: vendors.email, // 벤더의 기본 이메일 contactPerson: biddingCompanies.contactPerson, contactEmail: biddingCompanies.contactEmail, contactPhone: biddingCompanies.contactPhone, @@ -251,12 +253,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV // awardRatio: sql<number>`CASE WHEN ${biddingCompanies.isWinner} THEN 100 ELSE 0 END`, awardRatio: biddingCompanies.awardRatio, isBiddingParticipated: biddingCompanies.isBiddingParticipated, - status: sql<string>`CASE - WHEN ${biddingCompanies.isWinner} THEN 'selected' - WHEN ${biddingCompanies.finalQuoteSubmittedAt} IS NOT NULL THEN 'submitted' - WHEN ${biddingCompanies.respondedAt} IS NOT NULL THEN 'submitted' - ELSE 'pending' - END`, + invitationStatus: biddingCompanies.invitationStatus, }) .from(biddingCompanies) .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) @@ -272,6 +269,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV vendorId: vendor.vendorId, vendorName: vendor.vendorName || `Vendor ${vendor.vendorId}`, vendorCode: vendor.vendorCode || '', + vendorEmail: vendor.vendorEmail || '', // 벤더의 기본 이메일 contactPerson: vendor.contactPerson || '', contactEmail: vendor.contactEmail || '', contactPhone: vendor.contactPhone || '', @@ -281,7 +279,7 @@ export async function getQuotationVendors(biddingId: number): Promise<QuotationV isWinner: vendor.isWinner, awardRatio: vendor.awardRatio ? Number(vendor.awardRatio) : null, isBiddingParticipated: vendor.isBiddingParticipated, - status: vendor.status as 'pending' | 'submitted' | 'selected' | 'rejected', + invitationStatus: vendor.invitationStatus, documents: [], // 빈 배열로 초기화 })) } catch (error) { @@ -622,7 +620,8 @@ export async function updateBiddingDetailVendor( // 본입찰용 업체 추가 export async function createBiddingDetailVendor( biddingId: number, - vendorId: number + vendorId: number, + isPriceAdjustmentApplicableQuestion?: boolean ) { try { const result = await db.transaction(async (tx) => { @@ -630,9 +629,10 @@ export async function createBiddingDetailVendor( const biddingCompanyResult = await tx.insert(biddingCompanies).values({ biddingId: biddingId, companyId: vendorId, - invitationStatus: 'pending', + invitationStatus: 'pending', // 초대 대기 isPreQuoteSelected: true, // 본입찰 등록 기본값 isWinner: null, // 미정 상태로 초기화 0916 + isPriceAdjustmentApplicableQuestion: isPriceAdjustmentApplicableQuestion ?? false, createdAt: new Date(), updatedAt: new Date(), }).returning({ id: biddingCompanies.id }) @@ -730,9 +730,8 @@ export async function markAsDisposal(biddingId: number, userId: string) { itemName: bidding.itemName, biddingType: bidding.biddingType, processedDate: new Date().toLocaleDateString('ko-KR'), - managerName: bidding.managerName, - managerEmail: bidding.managerEmail, - managerPhone: bidding.managerPhone, + bidPicName: bidding.bidPicName, + supplyPicName: bidding.supplyPicName, language: 'ko' } }) @@ -807,7 +806,7 @@ export async function registerBidding(biddingId: number, userId: string) { .update(biddingCompanies) .set({ isBiddingInvited: true, - invitationStatus: 'sent', + invitationStatus: 'bidding_sent', // 입찰 초대 발송 updatedAt: new Date() }) .where(and( @@ -834,9 +833,8 @@ export async function registerBidding(biddingId: number, userId: string) { submissionStartDate: bidding.submissionStartDate, submissionEndDate: bidding.submissionEndDate, biddingUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/partners/bid/${biddingId}`, - managerName: bidding.managerName, - managerEmail: bidding.managerEmail, - managerPhone: bidding.managerPhone, + bidPicName: bidding.bidPicName, + supplyPicName: bidding.supplyPicName, language: 'ko' } }) @@ -945,9 +943,8 @@ export async function createRebidding(biddingId: number, userId: string) { submissionStartDate: originalBidding.submissionStartDate, submissionEndDate: originalBidding.submissionEndDate, biddingUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/partners/bid/${biddingId}`, - managerName: originalBidding.managerName, - managerEmail: originalBidding.managerEmail, - managerPhone: originalBidding.managerPhone, + bidPicName: originalBidding.bidPicName, + supplyPicName: originalBidding.supplyPicName, language: 'ko' } }) @@ -1521,6 +1518,7 @@ export interface PartnersBiddingListItem { // biddings 정보 biddingId: number biddingNumber: string + originalBiddingNumber: string | null // 원입찰번호 revision: number | null projectName: string itemName: string @@ -1533,9 +1531,10 @@ export interface PartnersBiddingListItem { submissionStartDate: Date | null submissionEndDate: Date | null status: string - managerName: string | null - managerEmail: string | null - managerPhone: string | null + // 입찰담당자 + bidPicName: string | null + // 조달담당자 + supplyPicName: string | null currency: string budget: number | null isUrgent: boolean | null // 긴급여부 @@ -1572,6 +1571,7 @@ export async function getBiddingListForPartners(companyId: number): Promise<Part // biddings 정보 biddingId: biddings.id, biddingNumber: biddings.biddingNumber, + originalBiddingNumber: biddings.originalBiddingNumber, // 원입찰번호 revision: biddings.revision, projectName: biddings.projectName, itemName: biddings.itemName, @@ -1584,9 +1584,12 @@ export async function getBiddingListForPartners(companyId: number): Promise<Part submissionStartDate: biddings.submissionStartDate, submissionEndDate: biddings.submissionEndDate, status: biddings.status, - managerName: biddings.managerName, - managerEmail: biddings.managerEmail, - managerPhone: biddings.managerPhone, + // 기존 담당자 필드 (하위호환성 유지) + + // 입찰담당자 + bidPicName: biddings.bidPicName, + // 조달담당자 + supplyPicName: biddings.supplyPicName, currency: biddings.currency, budget: biddings.budget, isUrgent: biddings.isUrgent, @@ -1635,7 +1638,6 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId: itemName: biddings.itemName, title: biddings.title, description: biddings.description, - content: biddings.content, // 계약 정보 contractType: biddings.contractType, @@ -1659,15 +1661,15 @@ export async function getBiddingDetailsForPartners(biddingId: number, companyId: // 상태 및 담당자 status: biddings.status, isUrgent: biddings.isUrgent, - managerName: biddings.managerName, - managerEmail: biddings.managerEmail, - managerPhone: biddings.managerPhone, + bidPicName: biddings.bidPicName, + supplyPicName: biddings.supplyPicName, // 협력업체 특정 정보 biddingCompanyId: biddingCompanies.id, invitationStatus: biddingCompanies.invitationStatus, finalQuoteAmount: biddingCompanies.finalQuoteAmount, finalQuoteSubmittedAt: biddingCompanies.finalQuoteSubmittedAt, + isFinalSubmission: biddingCompanies.isFinalSubmission, isWinner: biddingCompanies.isWinner, isAttendingMeeting: biddingCompanies.isAttendingMeeting, isPreQuoteSelected: biddingCompanies.isPreQuoteSelected, @@ -1718,6 +1720,7 @@ export async function submitPartnerResponse( sparePartResponse?: string additionalProposals?: string finalQuoteAmount?: number + isFinalSubmission?: boolean // 최종제출 여부 추가 prItemQuotations?: Array<{ prItemId: number bidUnitPrice: number @@ -1851,7 +1854,15 @@ export async function submitPartnerResponse( if (response.finalQuoteAmount !== undefined) { companyUpdateData.finalQuoteAmount = response.finalQuoteAmount companyUpdateData.finalQuoteSubmittedAt = new Date() - companyUpdateData.invitationStatus = 'submitted' + + // 최종제출 여부에 따라 상태 및 플래그 설정 + if (response.isFinalSubmission) { + companyUpdateData.isFinalSubmission = true + companyUpdateData.invitationStatus = 'bidding_submitted' // 응찰 완료 + } else { + companyUpdateData.isFinalSubmission = false + // 임시저장: invitationStatus는 변경하지 않음 (bidding_accepted 유지) + } } await tx @@ -1868,8 +1879,8 @@ export async function submitPartnerResponse( const biddingId = biddingCompanyInfo[0]?.biddingId - // 응찰 제출 시 입찰 상태를 평가중으로 변경 (bidding_opened 상태에서만) - if (biddingId && response.finalQuoteAmount !== undefined) { + // 최종제출인 경우, 입찰 상태를 평가중으로 변경 (bidding_opened 상태에서만) + if (biddingId && response.finalQuoteAmount !== undefined && response.isFinalSubmission) { await tx .update(biddings) .set({ @@ -2023,14 +2034,15 @@ export async function updatePartnerAttendance( }) .where(eq(biddingCompanies.id, biddingCompanyId)) - // 참석하는 경우, 사양설명회 담당자에게 이메일 발송을 위한 정보 반환 + // 참석하는 경우, 사양설명회 담당자(contactEmail)에 이메일 발송을 위한 정보 반환 if (attendanceData.isAttending) { + // 입찰 + 사양설명회 + 업체 정보 불러오기 const biddingInfo = await tx .select({ biddingId: biddingCompanies.biddingId, companyId: biddingCompanies.companyId, - managerEmail: biddings.managerEmail, - managerName: biddings.managerName, + bidPicName: biddings.bidPicName, + supplyPicName: biddings.supplyPicName, title: biddings.title, biddingNumber: biddings.biddingNumber, }) @@ -2040,7 +2052,7 @@ export async function updatePartnerAttendance( .limit(1) if (biddingInfo.length > 0) { - // 협력업체 정보 조회 + // 업체 정보 const companyInfo = await tx .select({ vendorName: vendors.vendorName, @@ -2051,37 +2063,59 @@ export async function updatePartnerAttendance( const companyName = companyInfo.length > 0 ? companyInfo[0].vendorName : '알 수 없음' - // 메일 발송 (템플릿 사용) - try { - const { sendEmail } = await import('@/lib/mail/sendEmail') - - await sendEmail({ - to: biddingInfo[0].managerEmail, - template: 'specification-meeting-attendance', - context: { - biddingNumber: biddingInfo[0].biddingNumber, - title: biddingInfo[0].title, - companyName: companyName, - attendeeCount: attendanceData.attendeeCount, - representativeName: attendanceData.representativeName, - representativePhone: attendanceData.representativePhone, - managerName: biddingInfo[0].managerName, - managerEmail: biddingInfo[0].managerEmail, - currentYear: new Date().getFullYear(), - language: 'ko' - } + // 사양설명회 상세 정보(담당자 email 포함) + const specificationMeetingInfo = await tx + .select({ + contactEmail: specificationMeetings.contactEmail, + meetingDate: specificationMeetings.meetingDate, + meetingTime: specificationMeetings.meetingTime, + location: specificationMeetings.location, }) - - console.log(`사양설명회 참석 알림 메일 발송 완료: ${biddingInfo[0].managerEmail}`) - } catch (emailError) { - console.error('메일 발송 실패:', emailError) - // 메일 발송 실패해도 참석 여부 업데이트는 성공으로 처리 + .from(specificationMeetings) + .where(eq(specificationMeetings.biddingId, biddingInfo[0].biddingId)) + .limit(1) + + const contactEmail = specificationMeetingInfo.length > 0 ? specificationMeetingInfo[0].contactEmail : null + + // 메일 발송 (템플릿 사용) + if (contactEmail) { + try { + const { sendEmail } = await import('@/lib/mail/sendEmail') + + await sendEmail({ + to: contactEmail, + template: 'specification-meeting-attendance', + context: { + biddingNumber: biddingInfo[0].biddingNumber, + title: biddingInfo[0].title, + companyName: companyName, + attendeeCount: attendanceData.attendeeCount, + representativeName: attendanceData.representativeName, + representativePhone: attendanceData.representativePhone, + bidPicName: biddingInfo[0].bidPicName, + supplyPicName: biddingInfo[0].supplyPicName, + meetingDate: specificationMeetingInfo[0]?.meetingDate, + meetingTime: specificationMeetingInfo[0]?.meetingTime, + location: specificationMeetingInfo[0]?.location, + contactEmail: contactEmail, + currentYear: new Date().getFullYear(), + language: 'ko' + } + }) + + console.log(`사양설명회 참석 알림 메일 발송 완료: ${contactEmail}`) + } catch (emailError) { + console.error('메일 발송 실패:', emailError) + // 메일 발송 실패해도 참석 여부 업데이트는 성공으로 처리 + } + } else { + console.warn('사양설명회 담당자 이메일이 없습니다.') } - + // 캐시 무효화 revalidateTag(`bidding-${biddingInfo[0].biddingId}`) revalidateTag('quotation-vendors') - + return { ...biddingInfo[0], companyName, |
