summaryrefslogtreecommitdiff
path: root/lib/bidding/detail/service.ts
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2025-11-10 11:25:19 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2025-11-10 11:25:19 +0900
commita5501ad1d1cb836d2b2f84e9b0f06049e22c901e (patch)
tree667ed8c5d6ec35b109190e9f976d66ae54def4ce /lib/bidding/detail/service.ts
parentb0fe980376fcf1a19ff4b90851ca8b01f378fdc0 (diff)
parentf8a38907911d940cb2e8e6c9aa49488d05b2b578 (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.ts160
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,