From 47fb72704161b4b58a27c7f5c679fc44618de9a1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 4 Nov 2025 10:03:32 +0000 Subject: (최겸) 구매 견적 내 RFQ Cancel/Delete, 연동제 적용, MRC Type 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/rfq-last/cancel-vendor-response-action.ts | 185 ++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 lib/rfq-last/cancel-vendor-response-action.ts (limited to 'lib/rfq-last/cancel-vendor-response-action.ts') diff --git a/lib/rfq-last/cancel-vendor-response-action.ts b/lib/rfq-last/cancel-vendor-response-action.ts new file mode 100644 index 00000000..e329a551 --- /dev/null +++ b/lib/rfq-last/cancel-vendor-response-action.ts @@ -0,0 +1,185 @@ +'use server' + +import { revalidatePath } from "next/cache"; +import db from "@/db/db"; +import { rfqLastDetails, rfqLastVendorResponses, rfqLastTbeSessions } from "@/db/schema"; +import { eq, and, inArray } from "drizzle-orm"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; + +/** + * RFQ 벤더 응답 취소 서버 액션 + * RFQ 발송 후 특정 벤더에 한하여 취소 처리 + * - vendor response를 "취소" 상태로 변경 + * - cancelReason 업데이트 + * - TBE 진행중이면 TBE 취소 처리 + * - 업체선정 진행중이면 업체선정 취소 처리 + */ +export async function cancelVendorResponse( + rfqId: number, + detailIds: number[], + cancelReason: string +): Promise<{ + success: boolean; + message: string; + results?: Array<{ detailId: number; success: boolean; error?: string }>; +}> { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.id) { + return { + success: false, + message: "인증이 필요합니다." + }; + } + + const userId = Number(session.user.id); + + if (!cancelReason || cancelReason.trim() === "") { + return { + success: false, + message: "취소 사유를 입력해주세요." + }; + } + + // 1. RFQ Detail 정보 조회 + const rfqDetails = await db.query.rfqLastDetails.findMany({ + where: and( + eq(rfqLastDetails.rfqsLastId, rfqId), + inArray(rfqLastDetails.id, detailIds), + eq(rfqLastDetails.isLatest, true) + ), + columns: { + id: true, + vendorsId: true, + } + }); + + if (rfqDetails.length === 0) { + return { + success: false, + message: "취소할 벤더를 찾을 수 없습니다." + }; + } + + const vendorIds = rfqDetails.map(d => d.vendorsId).filter(id => id != null) as number[]; + const results: Array<{ detailId: number; success: boolean; error?: string }> = []; + + // 2. 각 벤더에 대해 취소 처리 + for (const detail of rfqDetails) { + try { + await db.transaction(async (tx) => { + const vendorId = detail.vendorsId; + if (!vendorId) { + throw new Error("벤더 ID가 없습니다."); + } + + // 2-1. RFQ Detail의 cancelReason 업데이트 + await tx + .update(rfqLastDetails) + .set({ + cancelReason: cancelReason, + updatedBy: userId, + updatedAt: new Date() + }) + .where(eq(rfqLastDetails.id, detail.id)); + + // 2-2. 업체선정이 되어 있다면 취소 처리 + await tx + .update(rfqLastDetails) + .set({ + isSelected: false, + selectionDate: null, + selectionReason: null, + selectedBy: null, + updatedBy: userId, + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastDetails.id, detail.id), + eq(rfqLastDetails.isSelected, true) + ) + ); + + // 2-3. Vendor Response를 "취소" 상태로 변경 + await tx + .update(rfqLastVendorResponses) + .set({ + status: "취소", + updatedBy: userId, + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastVendorResponses.rfqsLastId, rfqId), + eq(rfqLastVendorResponses.vendorId, vendorId), + eq(rfqLastVendorResponses.isLatest, true), + // 이미 취소된 것은 제외 + inArray(rfqLastVendorResponses.status, ["대기중", "작성중", "제출완료", "수정요청", "최종확정"]) + ) + ); + + // 2-4. TBE 세션이 진행중이면 취소 처리 + await tx + .update(rfqLastTbeSessions) + .set({ + status: "취소", + updatedBy: userId, + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastTbeSessions.rfqsLastId, rfqId), + eq(rfqLastTbeSessions.vendorId, vendorId), + inArray(rfqLastTbeSessions.status, ["생성중", "준비중", "진행중", "검토중", "보류"]) + ) + ); + }); + + results.push({ + detailId: detail.id, + success: true + }); + + } catch (error) { + console.error(`벤더 응답 취소 실패 (Detail ID: ${detail.id}):`, error); + results.push({ + detailId: detail.id, + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + // 3. 캐시 갱신 + revalidatePath(`/evcp/rfq-last/${rfqId}`); + revalidatePath(`/evcp/rfq-last/${rfqId}/vendor`); + + const successCount = results.filter(r => r.success).length; + const failCount = results.length - successCount; + + if (failCount === 0) { + return { + success: true, + message: `RFQ 취소가 완료되었습니다. (${successCount}건)`, + results + }; + } else { + return { + success: false, + message: `RFQ 취소 중 일부 실패했습니다. (성공: ${successCount}건, 실패: ${failCount}건)`, + results + }; + } + + } catch (error) { + console.error("RFQ 벤더 응답 취소 처리 중 오류:", error); + return { + success: false, + message: error instanceof Error ? error.message : "RFQ 취소 처리 중 오류가 발생했습니다." + }; + } +} + -- cgit v1.2.3