'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 취소 처리 중 오류가 발생했습니다." }; } }