diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-12 10:42:36 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-12 10:42:36 +0000 |
| commit | 8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 (patch) | |
| tree | 36bd57d147ba929f1d72918d1fb91ad2c4778624 /lib/bidding/detail/service.ts | |
| parent | 57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff) | |
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'lib/bidding/detail/service.ts')
| -rw-r--r-- | lib/bidding/detail/service.ts | 120 |
1 files changed, 86 insertions, 34 deletions
diff --git a/lib/bidding/detail/service.ts b/lib/bidding/detail/service.ts index d58ded8e..b5a3cce8 100644 --- a/lib/bidding/detail/service.ts +++ b/lib/bidding/detail/service.ts @@ -1,13 +1,14 @@ 'use server' import db from '@/db/db' -import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, priceAdjustmentForms, users } from '@/db/schema' -import { specificationMeetings } from '@/db/schema/bidding' +import { biddings, prItemsForBidding, biddingDocuments, biddingCompanies, vendors, companyPrItemBids, companyConditionResponses, vendorSelectionResults, priceAdjustmentForms, users, vendorContacts } from '@/db/schema' +import { specificationMeetings, biddingCompaniesContacts } from '@/db/schema/bidding' import { eq, and, sql, desc, ne } from 'drizzle-orm' import { revalidatePath, revalidateTag } from 'next/cache' import { unstable_cache } from "@/lib/unstable-cache"; import { sendEmail } from '@/lib/mail/sendEmail' import { saveFile } from '@/lib/file-stroage' +import { sendBiddingNoticeSms } from '@/lib/users/auth/passwordUtil' // userId를 user.name으로 변환하는 유틸리티 함수 async function getUserNameById(userId: string): Promise<string> { @@ -205,28 +206,20 @@ export async function getBiddingCompaniesData(biddingId: number) { } } -// prItemsForBidding 테이블에서 품목 정보 조회 (캐시 적용) +// prItemsForBidding 테이블에서 품목 정보 조회 (캐시 미적용, always fresh) export async function getPRItemsForBidding(biddingId: number) { - return unstable_cache( - async () => { - try { - const items = await db - .select() - .from(prItemsForBidding) - .where(eq(prItemsForBidding.biddingId, biddingId)) - .orderBy(prItemsForBidding.id) + try { + const items = await db + .select() + .from(prItemsForBidding) + .where(eq(prItemsForBidding.biddingId, biddingId)) + .orderBy(prItemsForBidding.id) - return items - } catch (error) { - console.error('Failed to get PR items for bidding:', error) - return [] - } - }, - [`pr-items-for-bidding-${biddingId}`], - { - tags: [`bidding-${biddingId}`, 'pr-items'] - } - )() + return items + } catch (error) { + console.error('Failed to get PR items for bidding:', error) + return [] + } } // 견적 시스템에서 협력업체 정보를 가져오는 함수 (캐시 적용) @@ -757,10 +750,10 @@ export async function markAsDisposal(biddingId: number, userId: string) { } } -// 입찰 등록 (사전견적에서 선정된 업체들에게 본입찰 초대 발송) +// 입찰 등록 ( 본입찰 초대 발송) export async function registerBidding(biddingId: number, userId: string) { try { - // 사전견적에서 선정된 업체들 + 본입찰에서 개별적으로 추가한 업체들 조회 + // 본입찰에서 개별적으로 추가한 업체들 조회 const selectedCompanies = await db .select({ companyId: biddingCompanies.companyId, @@ -769,10 +762,9 @@ export async function registerBidding(biddingId: number, userId: string) { }) .from(biddingCompanies) .leftJoin(vendors, eq(biddingCompanies.companyId, vendors.id)) - .where(and( - eq(biddingCompanies.biddingId, biddingId), - eq(biddingCompanies.isPreQuoteSelected, true) - )) + .where( + eq(biddingCompanies.biddingId, biddingId) + ) // 입찰 정보 조회 const biddingInfo = await db @@ -843,7 +835,37 @@ export async function registerBidding(biddingId: number, userId: string) { } } } + // 4. 입찰 공고 SMS 알림 전송 + for (const company of selectedCompanies) { + // biddingCompaniesContacts에서 모든 연락처 전화번호 조회 + const contactInfos = await db + .select({ + contactNumber: biddingCompaniesContacts.contactNumber + }) + .from(biddingCompaniesContacts) + .where(and( + eq(biddingCompaniesContacts.biddingId, biddingId), + eq(biddingCompaniesContacts.vendorId, company.companyId) + )); + + // 각 연락처에 SMS 전송 + for (const contactInfo of contactInfos) { + const contactPhone = contactInfo.contactNumber; + if (contactPhone) { + try { + const smsResult = await sendBiddingNoticeSms(contactPhone, bidding.title); + if (smsResult.success) { + console.log(`입찰 공고 SMS 전송 성공: ${contactPhone} - ${bidding.title}`); + } else { + console.error(`입찰 공고 SMS 전송 실패: ${contactPhone} - ${smsResult.error}`); + } + } catch (smsError) { + console.error(`Failed to send bidding notice SMS to ${contactPhone}:`, smsError) + } + } + } + } // 캐시 무효화 revalidateTag(`bidding-${biddingId}`) revalidateTag('bidding-detail') @@ -1352,7 +1374,7 @@ export async function updateBiddingParticipation( return { success: true, - message: `입찰 참여상태가 ${participated ? '응찰' : '미응찰'}로 업데이트되었습니다.`, + message: `입찰 참여상태가 ${participated ? '응찰' :'응찰포기'}로 업데이트되었습니다.`, } } catch (error) { console.error('Failed to update bidding participation:', error) @@ -1483,7 +1505,7 @@ export async function updatePartnerBiddingParticipation( return { success: true, - message: `입찰 참여상태가 ${participated ? '응찰' : '미응찰'}로 업데이트되었습니다.`, + message: `입찰 참여상태가 ${participated ? '응찰' : '응찰포기'}로 업데이트되었습니다.`, } } catch (error) { console.error('Failed to update partner bidding participation:', error) @@ -1802,8 +1824,6 @@ export async function submitPartnerResponse( } } - - // 3. 연동제 정보 저장 (연동제 적용이 true이고 연동제 정보가 있는 경우) // if (response.priceAdjustmentResponse && response.priceAdjustmentForm) { // const priceAdjustmentData = { @@ -1854,8 +1874,8 @@ export async function submitPartnerResponse( if (response.finalQuoteAmount !== undefined) { companyUpdateData.finalQuoteAmount = response.finalQuoteAmount companyUpdateData.finalQuoteSubmittedAt = new Date() - - // 최종제출 여부에 따라 상태 및 플래그 설정 + + // isFinalSubmission에 따라 상태 및 플래그 설정 if (response.isFinalSubmission) { companyUpdateData.isFinalSubmission = true companyUpdateData.invitationStatus = 'bidding_submitted' // 응찰 완료 @@ -1863,6 +1883,38 @@ export async function submitPartnerResponse( companyUpdateData.isFinalSubmission = false // 임시저장: invitationStatus는 변경하지 않음 (bidding_accepted 유지) } + + // 스냅샷은 임시저장/최종제출 관계없이 항상 생성 + if (response.prItemQuotations && response.prItemQuotations.length > 0) { + // 기존 스냅샷 조회 + const existingCompany = await tx + .select({ quotationSnapshots: biddingCompanies.quotationSnapshots }) + .from(biddingCompanies) + .where(eq(biddingCompanies.id, biddingCompanyId)) + .limit(1) + + const existingSnapshots = existingCompany[0]?.quotationSnapshots as any[] || [] + + // 새로운 스냅샷 생성 + const newSnapshot = { + id: Date.now().toString(), // 고유 ID + round: existingSnapshots.length + 1, // 차수 + submittedAt: new Date().toISOString(), + totalAmount: response.finalQuoteAmount, + currency: 'KRW', + isFinalSubmission: !!response.isFinalSubmission, + items: response.prItemQuotations.map(item => ({ + prItemId: item.prItemId, + bidUnitPrice: item.bidUnitPrice, + bidAmount: item.bidAmount, + proposedDeliveryDate: item.proposedDeliveryDate, + technicalSpecification: item.technicalSpecification + })) + } + + // 스냅샷 배열에 추가 + companyUpdateData.quotationSnapshots = [...existingSnapshots, newSnapshot] + } } await tx |
