From 8642ee064ddf96f1db2b948b4cc8bbbd6cfee820 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 12 Nov 2025 10:42:36 +0000 Subject: (최겸) 구매 일반계약, 입찰 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/bidding/actions.ts | 230 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 2 deletions(-) (limited to 'lib/bidding/actions.ts') diff --git a/lib/bidding/actions.ts b/lib/bidding/actions.ts index b5736707..d0c7a0cd 100644 --- a/lib/bidding/actions.ts +++ b/lib/bidding/actions.ts @@ -1,7 +1,9 @@ "use server" import db from "@/db/db" -import { eq, and } from "drizzle-orm" +import { eq, and, sql } from "drizzle-orm" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" import { biddings, biddingCompanies, @@ -484,8 +486,15 @@ export async function bidClosureAction( description: string files: File[] }, - userId: string + userId: string | undefined ) { + if (!userId) { + return { + success: false, + error: '사용자 정보가 필요합니다.' + } + } + try { const userName = await getUserNameById(userId) @@ -573,6 +582,62 @@ export async function bidClosureAction( } } +// 유찰취소 액션 +export async function cancelDisposalAction( + biddingId: number, + userId: string +) { + try { + const userName = await getUserNameById(userId) + + return await db.transaction(async (tx) => { + // 1. 입찰 정보 확인 + const [existingBidding] = await tx + .select() + .from(biddings) + .where(eq(biddings.id, biddingId)) + .limit(1) + + if (!existingBidding) { + return { + success: false, + error: '입찰 정보를 찾을 수 없습니다.' + } + } + + // 2. 유찰 또는 폐찰 상태인지 확인 + if (existingBidding.status !== 'bidding_disposal' && existingBidding.status !== 'bid_closure') { + return { + success: false, + error: '유찰 또는 폐찰 상태인 입찰만 취소할 수 있습니다.' + } + } + + // 3. 입찰 상태를 입찰 진행중으로 변경 + await tx + .update(biddings) + .set({ + status: 'evaluation_of_bidding', + updatedAt: new Date(), + updatedBy: userName, + }) + .where(eq(biddings.id, biddingId)) + + return { + success: true, + message: '유찰 취소가 완료되었습니다.' + } + }) + + } catch (error) { + console.error('유찰취소 실패:', error) + return { + success: false, + error: error instanceof Error ? error.message : '유찰취소 중 오류가 발생했습니다.' + } + } +} + // 사용자 이름 조회 헬퍼 함수 async function getUserNameById(userId: string): Promise { try { @@ -588,3 +653,164 @@ async function getUserNameById(userId: string): Promise { return userId } } + +// 조기개찰 액션 +export async function earlyOpenBiddingAction(biddingId: number) { + try { + const session = await getServerSession(authOptions) + if (!session?.user?.name) { + return { success: false, message: '인증이 필요합니다.' } + } + + const userName = session.user.name + + return await db.transaction(async (tx) => { + // 1. 입찰 정보 확인 + const [bidding] = await tx + .select({ + id: biddings.id, + status: biddings.status, + submissionEndDate: biddings.submissionEndDate, + title: biddings.title + }) + .from(biddings) + .where(eq(biddings.id, biddingId)) + .limit(1) + + if (!bidding) { + return { success: false, message: '입찰 정보를 찾을 수 없습니다.' } + } + + // 2. 입찰서 제출기간 내인지 확인 + const now = new Date() + if (bidding.submissionEndDate && now > bidding.submissionEndDate) { + return { success: false, message: '입찰서 제출기간이 종료되었습니다.' } + } + + // 3. 참여 현황 확인 + const [participationStats] = await tx + .select({ + participantExpected: db.$count(biddingCompanies), + participantParticipated: db.$count(biddingCompanies, eq(biddingCompanies.invitationStatus, 'bidding_submitted')), + participantDeclined: db.$count(biddingCompanies, and( + eq(biddingCompanies.invitationStatus, 'bidding_declined'), + eq(biddingCompanies.biddingId, biddingId) + )), + participantPending: db.$count(biddingCompanies, and( + eq(biddingCompanies.invitationStatus, 'pending'), + eq(biddingCompanies.biddingId, biddingId) + )), + }) + .from(biddingCompanies) + .where(eq(biddingCompanies.biddingId, biddingId)) + + // 실제 SQL 쿼리로 변경 + const [stats] = await tx + .select({ + participantExpected: sql`COUNT(*)`.as('participant_expected'), + participantParticipated: sql`COUNT(CASE WHEN invitation_status = 'bidding_submitted' THEN 1 END)`.as('participant_participated'), + participantDeclined: sql`COUNT(CASE WHEN invitation_status IN ('bidding_declined', 'bidding_cancelled') THEN 1 END)`.as('participant_declined'), + participantPending: sql`COUNT(CASE WHEN invitation_status IN ('pending', 'bidding_sent', 'bidding_accepted') THEN 1 END)`.as('participant_pending'), + }) + .from(biddingCompanies) + .where(eq(biddingCompanies.biddingId, biddingId)) + + const participantExpected = Number(stats.participantExpected) || 0 + const participantParticipated = Number(stats.participantParticipated) || 0 + const participantDeclined = Number(stats.participantDeclined) || 0 + const participantPending = Number(stats.participantPending) || 0 + + // 4. 조기개찰 조건 검증 + // - 미제출 협력사 = 0 + if (participantPending > 0) { + return { success: false, message: `미제출 협력사가 ${participantPending}명 있어 조기개찰이 불가능합니다.` } + } + + // - 참여협력사 + 포기협력사 = 참여예정협력사 + if (participantParticipated + participantDeclined !== participantExpected) { + return { success: false, message: '모든 협력사가 참여 또는 포기하지 않아 조기개찰이 불가능합니다.' } + } + + // 5. 참여협력사 중 최종응찰 버튼을 클릭한 업체들만 있는지 검증 + // bidding_submitted 상태인 업체들이 있는지 확인 (이미 위에서 검증됨) + + // 6. 조기개찰 상태로 변경 + await tx + .update(biddings) + .set({ + status: 'evaluation_of_bidding', + openedAt: new Date(), + openedBy: userName, + updatedAt: new Date(), + updatedBy: userName, + }) + .where(eq(biddings.id, biddingId)) + + return { success: true, message: '조기개찰이 완료되었습니다.' } + }) + + } catch (error) { + console.error('조기개찰 실패:', error) + return { + success: false, + message: error instanceof Error ? error.message : '조기개찰 중 오류가 발생했습니다.' + } + } +} + +// 개찰 액션 +export async function openBiddingAction(biddingId: number) { + try { + const session = await getServerSession(authOptions) + if (!session?.user?.name) { + return { success: false, message: '인증이 필요합니다.' } + } + + const userName = session.user.name + + return await db.transaction(async (tx) => { + // 1. 입찰 정보 확인 + const [bidding] = await tx + .select({ + id: biddings.id, + status: biddings.status, + submissionEndDate: biddings.submissionEndDate, + title: biddings.title + }) + .from(biddings) + .where(eq(biddings.id, biddingId)) + .limit(1) + + if (!bidding) { + return { success: false, message: '입찰 정보를 찾을 수 없습니다.' } + } + + // 2. 입찰서 제출기간이 종료되었는지 확인 + const now = new Date() + if (bidding.submissionEndDate && now <= bidding.submissionEndDate) { + return { success: false, message: '입찰서 제출기간이 아직 종료되지 않았습니다.' } + } + + // 3. 입찰평가중 상태로 변경 + await tx + .update(biddings) + .set({ + status: 'evaluation_of_bidding', + openedAt: new Date(), + openedBy: userName, + updatedAt: new Date(), + updatedBy: userName, + }) + .where(eq(biddings.id, biddingId)) + + return { success: true, message: '개찰이 완료되었습니다.' } + }) + + } catch (error) { + console.error('개찰 실패:', error) + return { + success: false, + message: error instanceof Error ? error.message : '개찰 중 오류가 발생했습니다.' + } + } +} -- cgit v1.2.3