summaryrefslogtreecommitdiff
path: root/lib/bidding/detail/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-12 10:42:36 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-12 10:42:36 +0000
commit8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 (patch)
tree36bd57d147ba929f1d72918d1fb91ad2c4778624 /lib/bidding/detail/service.ts
parent57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff)
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'lib/bidding/detail/service.ts')
-rw-r--r--lib/bidding/detail/service.ts120
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