From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/detail/bidding-actions.ts | 227 ++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 lib/bidding/detail/bidding-actions.ts (limited to 'lib/bidding/detail/bidding-actions.ts') diff --git a/lib/bidding/detail/bidding-actions.ts b/lib/bidding/detail/bidding-actions.ts new file mode 100644 index 00000000..70bba1c3 --- /dev/null +++ b/lib/bidding/detail/bidding-actions.ts @@ -0,0 +1,227 @@ +'use server' + +import db from '@/db/db' +import { biddings, biddingCompanies, companyPrItemBids } from '@/db/schema/bidding' +import { eq, and } from 'drizzle-orm' +import { revalidateTag, revalidatePath } from 'next/cache' +import { users } from '@/db/schema' + +// userId를 user.name으로 변환하는 유틸리티 함수 +async function getUserNameById(userId: string): Promise { + try { + const user = await db + .select({ name: users.name }) + .from(users) + .where(eq(users.id, parseInt(userId))) + .limit(1) + + return user[0]?.name || userId + } catch (error) { + console.error('Failed to get user name:', error) + return userId + } +} + +// 응찰 취소 서버 액션 (최종제출이 아닌 경우만 가능) +export async function cancelBiddingResponse( + biddingCompanyId: number, + userId: string +) { + try { + const userName = await getUserNameById(userId) + + return await db.transaction(async (tx) => { + // 1. 현재 상태 확인 (최종제출 여부) + const [company] = await tx + .select({ + isFinalSubmission: biddingCompanies.isFinalSubmission, + biddingId: biddingCompanies.biddingId, + }) + .from(biddingCompanies) + .where(eq(biddingCompanies.id, biddingCompanyId)) + .limit(1) + + if (!company) { + return { + success: false, + error: '업체 정보를 찾을 수 없습니다.' + } + } + + // 최종제출한 경우 취소 불가 + if (company.isFinalSubmission) { + return { + success: false, + error: '최종 제출된 응찰은 취소할 수 없습니다.' + } + } + + // 2. 응찰 데이터 초기화 + await tx + .update(biddingCompanies) + .set({ + finalQuoteAmount: null, + finalQuoteSubmittedAt: null, + isFinalSubmission: false, + invitationStatus: 'bidding_cancelled', // 응찰 취소 상태 + updatedAt: new Date() + }) + .where(eq(biddingCompanies.id, biddingCompanyId)) + + // 3. 품목별 견적 삭제 (본입찰 데이터) + await tx + .delete(companyPrItemBids) + .where( + and( + eq(companyPrItemBids.biddingCompanyId, biddingCompanyId), + eq(companyPrItemBids.isPreQuote, false) + ) + ) + + // 캐시 무효화 + revalidateTag(`bidding-${company.biddingId}`) + revalidateTag('quotation-vendors') + revalidateTag('quotation-details') + revalidatePath(`/partners/bid/${company.biddingId}`) + + return { + success: true, + message: '응찰이 취소되었습니다.' + } + }) + } catch (error) { + console.error('Failed to cancel bidding response:', error) + return { + success: false, + 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 : '개찰에 실패했습니다.' + } + } +} + -- cgit v1.2.3