diff options
Diffstat (limited to 'lib/bidding/detail')
| -rw-r--r-- | lib/bidding/detail/bidding-actions.ts | 136 | ||||
| -rw-r--r-- | lib/bidding/detail/service.ts | 87 |
2 files changed, 88 insertions, 135 deletions
diff --git a/lib/bidding/detail/bidding-actions.ts b/lib/bidding/detail/bidding-actions.ts index fb659039..4140ec72 100644 --- a/lib/bidding/detail/bidding-actions.ts +++ b/lib/bidding/detail/bidding-actions.ts @@ -22,7 +22,7 @@ async function getUserNameById(userId: string): Promise<string> { }
}
-// 응찰 취소 서버 액션 (최종제출이 아닌 경우만 가능)
+// 응찰 포기 서버 액션 (최종제출이 아닌 경우만 가능)
export async function cancelBiddingResponse(
biddingCompanyId: number,
userId: string
@@ -63,7 +63,7 @@ export async function cancelBiddingResponse( finalQuoteAmount: null,
finalQuoteSubmittedAt: null,
isFinalSubmission: false,
- invitationStatus: 'bidding_cancelled', // 응찰 취소 상태
+ invitationStatus: 'bidding_cancelled', // 응찰 포기 상태
updatedAt: new Date()
})
.where(eq(biddingCompanies.id, biddingCompanyId))
@@ -86,142 +86,14 @@ export async function cancelBiddingResponse( return {
success: true,
- message: '응찰이 취소되었습니다.'
+ message: '응찰이 포기되었습니다.'
}
})
} catch (error) {
console.error('Failed to cancel bidding response:', error)
return {
success: false,
- error: error instanceof Error ? error.message : '응찰 취소에 실패했습니다.'
+ error: error instanceof Error ? error.message : '응찰 포기에 실패했습니다.'
}
}
}
-
-// 모든 벤더가 최종제출했는지 확인
-export async function checkAllVendorsFinalSubmitted(biddingId: number) {
- try {
- const companies = await db
- .select({
- id: biddingCompanies.id,
- isFinalSubmission: biddingCompanies.isFinalSubmission,
- invitationStatus: biddingCompanies.invitationStatus,
- })
- .from(biddingCompanies)
- .where(
- and(
- eq(biddingCompanies.biddingId, biddingId),
- eq(biddingCompanies.isBiddingInvited, true) // 본입찰 초대된 업체만
- )
- )
-
- // 초대된 업체가 없으면 false
- if (companies.length === 0) {
- return {
- allSubmitted: false,
- totalCompanies: 0,
- submittedCompanies: 0
- }
- }
-
- // 모든 업체가 최종제출했는지 확인
- const submittedCompanies = companies.filter(c => c.isFinalSubmission).length
- const allSubmitted = companies.every(c => c.isFinalSubmission)
-
- return {
- allSubmitted,
- totalCompanies: companies.length,
- submittedCompanies
- }
- } catch (error) {
- console.error('Failed to check all vendors final submitted:', error)
- return {
- allSubmitted: false,
- totalCompanies: 0,
- submittedCompanies: 0
- }
- }
-}
-
-// // 개찰 서버 액션 (조기개찰/개찰 구분)
-// export async function performBidOpening(
-// biddingId: number,
-// userId: string,
-// isEarly: boolean = false // 조기개찰 여부
-// ) {
-// try {
-// const userName = await getUserNameById(userId)
-
-// return await db.transaction(async (tx) => {
-// // 1. 입찰 정보 조회
-// const [bidding] = await tx
-// .select({
-// id: biddings.id,
-// status: biddings.status,
-// submissionEndDate: biddings.submissionEndDate,
-// })
-// .from(biddings)
-// .where(eq(biddings.id, biddingId))
-// .limit(1)
-
-// if (!bidding) {
-// return {
-// success: false,
-// error: '입찰 정보를 찾을 수 없습니다.'
-// }
-// }
-
-// // 2. 개찰 가능 여부 확인 (evaluation_of_bidding 상태에서만)
-// if (bidding.status !== 'evaluation_of_bidding') {
-// return {
-// success: false,
-// error: '입찰평가중 상태에서만 개찰할 수 있습니다.'
-// }
-// }
-
-// // 3. 모든 벤더가 최종제출했는지 확인
-// const checkResult = await checkAllVendorsFinalSubmitted(biddingId)
-// if (!checkResult.allSubmitted) {
-// return {
-// success: false,
-// error: `모든 벤더가 최종 제출해야 개찰할 수 있습니다. (${checkResult.submittedCompanies}/${checkResult.totalCompanies})`
-// }
-// }
-
-// // 4. 조기개찰 여부 결정
-// const now = new Date()
-// const submissionEndDate = bidding.submissionEndDate ? new Date(bidding.submissionEndDate) : null
-// const isBeforeDeadline = submissionEndDate && now < submissionEndDate
-
-// // 마감일 전이면 조기개찰, 마감일 후면 일반 개찰
-// const newStatus = (isEarly || isBeforeDeadline) ? 'early_bid_opening' : 'bid_opening'
-
-// // 5. 입찰 상태 변경
-// await tx
-// .update(biddings)
-// .set({
-// status: newStatus,
-// updatedAt: new Date()
-// })
-// .where(eq(biddings.id, biddingId))
-
-// // 캐시 무효화
-// revalidateTag(`bidding-${biddingId}`)
-// revalidateTag('bidding-detail')
-// revalidatePath(`/evcp/bid/${biddingId}`)
-
-// return {
-// success: true,
-// message: `${newStatus === 'early_bid_opening' ? '조기개찰' : '개찰'}이 완료되었습니다.`,
-// status: newStatus
-// }
-// })
-// } catch (error) {
-// console.error('Failed to perform bid opening:', error)
-// return {
-// success: false,
-// error: error instanceof Error ? error.message : '개찰에 실패했습니다.'
-// }
-// }
-// }
-
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index 8f9bf018..c9aaa66c 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -854,6 +854,7 @@ export async function registerBidding(biddingId: number, userId: string) { // 3. 선정된 업체들에게 본입찰 초대 메일 발송 debugLog('registerBidding: Sending emails...') for (const company of selectedCompanies) { + // 벤더 메인 이메일로 발송 if (company.contactEmail) { try { await sendEmail({ @@ -879,6 +880,51 @@ export async function registerBidding(biddingId: number, userId: string) { debugError(`Failed to send bidding invitation email to ${company.contactEmail}:`, emailError) } } + + // 추가 담당자들에게도 이메일 발송 + try { + const contactInfos = await db + .select({ + contactName: biddingCompaniesContacts.contactName, + contactEmail: biddingCompaniesContacts.contactEmail + }) + .from(biddingCompaniesContacts) + .where(and( + eq(biddingCompaniesContacts.biddingId, biddingId), + eq(biddingCompaniesContacts.vendorId, company.companyId) + )); + + for (const contact of contactInfos) { + // 벤더 메인 이메일과 중복되지 않는 경우에만 발송 + if (contact.contactEmail && contact.contactEmail !== company.contactEmail) { + try { + await sendEmail({ + to: contact.contactEmail, + template: 'bidding-invitation', + context: { + companyName: company.companyName, + biddingNumber: bidding.biddingNumber, + title: bidding.title, + projectName: bidding.projectName, + itemName: bidding.itemName, + biddingType: bidding.biddingType, + submissionStartDate: bidding.submissionStartDate, + submissionEndDate: bidding.submissionEndDate, + biddingUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/partners/bid/${biddingId}`, + bidPicName: bidding.bidPicName, + supplyPicName: bidding.supplyPicName, + language: 'ko' + } + }) + debugLog(`registerBidding: Email sent to contact ${contact.contactEmail}`) + } catch (emailError) { + debugError(`Failed to send bidding invitation email to contact ${contact.contactEmail}:`, emailError) + } + } + } + } catch (contactError) { + debugError('Failed to fetch contact emails:', contactError) + } } // 4. 입찰 공고 SMS 알림 전송 debugLog('registerBidding: Sending SMS...') @@ -1467,6 +1513,41 @@ export async function saveBiddingDraft( } } +// 본입찰용 품목별 견적 조회 (협력업체용) +export async function getPartnerBiddingItemQuotations(biddingCompanyId: number) { + try { + const savedQuotations = await db + .select({ + prItemId: companyPrItemBids.prItemId, + bidUnitPrice: companyPrItemBids.bidUnitPrice, + bidAmount: companyPrItemBids.bidAmount, + proposedDeliveryDate: companyPrItemBids.proposedDeliveryDate, + technicalSpecification: companyPrItemBids.technicalSpecification, + currency: companyPrItemBids.currency + }) + .from(companyPrItemBids) + .where( + and( + eq(companyPrItemBids.biddingCompanyId, biddingCompanyId), + eq(companyPrItemBids.isPreQuote, false) // 본입찰 데이터 + ) + ) + + // Decimal 타입을 number로 변환 + return savedQuotations.map(item => ({ + prItemId: item.prItemId, + bidUnitPrice: parseFloat(item.bidUnitPrice || '0'), + bidAmount: parseFloat(item.bidAmount || '0'), + proposedDeliveryDate: item.proposedDeliveryDate, + technicalSpecification: item.technicalSpecification, + currency: item.currency + })) + } catch (error) { + console.error('Failed to get partner bidding item quotations:', error) + return [] + } +} + // ================================================= // 협력업체 페이지용 함수들 (Partners) // ================================================= @@ -1839,14 +1920,14 @@ export async function submitPartnerResponse( // adjustmentDate: response.priceAdjustmentForm.adjustmentDate || null, // nonApplicableReason: response.priceAdjustmentForm.nonApplicableReason, // } - + // // // 기존 연동제 정보가 있는지 확인 // const existingPriceAdjustment = await tx // .select() // .from(priceAdjustmentForms) // .where(eq(priceAdjustmentForms.companyConditionResponsesId, companyConditionResponseId)) // .limit(1) - + // // if (existingPriceAdjustment.length > 0) { // // 업데이트 // await tx @@ -2573,4 +2654,4 @@ export async function setSpecificationMeetingParticipation(biddingCompanyId: num console.error('Failed to update specification meeting participation:', error) return { success: false, error: '사양설명회 참여상태 업데이트에 실패했습니다.' } } -}
\ No newline at end of file +} |
