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/actions.ts | |
| parent | 57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff) | |
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'lib/bidding/actions.ts')
| -rw-r--r-- | lib/bidding/actions.ts | 230 |
1 files changed, 228 insertions, 2 deletions
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<string> { try { @@ -588,3 +653,164 @@ async function getUserNameById(userId: string): Promise<string> { 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<number>`COUNT(*)`.as('participant_expected'), + participantParticipated: sql<number>`COUNT(CASE WHEN invitation_status = 'bidding_submitted' THEN 1 END)`.as('participant_participated'), + participantDeclined: sql<number>`COUNT(CASE WHEN invitation_status IN ('bidding_declined', 'bidding_cancelled') THEN 1 END)`.as('participant_declined'), + participantPending: sql<number>`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 : '개찰 중 오류가 발생했습니다.' + } + } +} |
