summaryrefslogtreecommitdiff
path: root/lib/bidding/actions.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/actions.ts
parent57ea2f740abf1c7933671561cfe0e421fb5ef3fc (diff)
(최겸) 구매 일반계약, 입찰 수정
Diffstat (limited to 'lib/bidding/actions.ts')
-rw-r--r--lib/bidding/actions.ts230
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 : '개찰 중 오류가 발생했습니다.'
+ }
+ }
+}