'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 : '개찰에 실패했습니다.' } } }