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/delete-action.ts | 199 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 lib/rfq-last/delete-action.ts (limited to 'lib/rfq-last/delete-action.ts') diff --git a/lib/rfq-last/delete-action.ts b/lib/rfq-last/delete-action.ts new file mode 100644 index 00000000..3b5f13de --- /dev/null +++ b/lib/rfq-last/delete-action.ts @@ -0,0 +1,199 @@ +'use server' + +import { revalidatePath } from "next/cache"; +import db from "@/db/db"; +import { rfqsLast, rfqLastTbeSessions, rfqLastDetails } from "@/db/schema"; +import { rfqLastVendorResponses } from "@/db/schema/rfqVendor"; +import { eq, and, inArray } from "drizzle-orm"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { cancelRFQ } from "@/lib/soap/ecc/send/delete-rfq"; + +/** + * RFQ 삭제 (상태 변경) 서버 액션 + * ANFNR이 있는 RFQ만 삭제 가능하며, ECC로 SOAP 취소 요청을 전송한 후 + * 성공 시 RFQ 상태를 "RFQ 삭제"로 변경하고 연결된 TBE 세션을 "취소" 상태로 변경 + * 또한 연결된 vendor response를 "RFQ 삭제" 상태로 변경하고, + * 업체선정이 진행중인 경우 업체선정 취소 처리 + */ +export async function deleteRfq(rfqIds: number[], deleteReason?: string): Promise<{ + success: boolean; + message: string; + results?: Array<{ rfqId: 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); + + // 1. RFQ 정보 조회 및 ANFNR 유효성 검증 + const rfqs = await db.query.rfqsLast.findMany({ + where: inArray(rfqsLast.id, rfqIds), + columns: { + id: true, + rfqCode: true, + ANFNR: true, + status: true, + } + }); + + // ANFNR이 있는 RFQ만 필터링 + const rfqsWithAnfnr = rfqs.filter(rfq => rfq.ANFNR && rfq.ANFNR.trim() !== ""); + + if (rfqsWithAnfnr.length === 0) { + return { + success: false, + message: "ANFNR이 있는 RFQ가 선택되지 않았습니다." + }; + } + + // 요청된 RFQ 중 일부가 없거나 ANFNR이 없는 경우 확인 + const missingIds = rfqIds.filter(id => !rfqs.find(r => r.id === id)); + const rfqsWithoutAnfnr = rfqs.filter(rfq => !rfq.ANFNR || rfq.ANFNR.trim() === ""); + + if (missingIds.length > 0 || rfqsWithoutAnfnr.length > 0) { + const warnings: string[] = []; + if (missingIds.length > 0) { + warnings.push(`존재하지 않는 RFQ: ${missingIds.join(", ")}`); + } + if (rfqsWithoutAnfnr.length > 0) { + warnings.push(`ANFNR이 없는 RFQ: ${rfqsWithoutAnfnr.map(r => r.rfqCode || r.id).join(", ")}`); + } + } + + const results: Array<{ rfqId: number; success: boolean; error?: string }> = []; + + // 2. 각 RFQ에 대해 ECC 취소 요청 및 상태 변경 처리 + for (const rfq of rfqsWithAnfnr) { + try { + // 2-1. ECC로 SOAP 취소 요청 전송 + const cancelResult = await cancelRFQ(rfq.ANFNR!); + + if (!cancelResult.success) { + results.push({ + rfqId: rfq.id, + success: false, + error: cancelResult.message + }); + continue; + } + + // 2-2. ECC 요청 성공 시 트랜잭션 내에서 상태 변경 + await db.transaction(async (tx) => { + // RFQ 상태를 "RFQ 삭제"로 변경 및 삭제 사유 저장 + await tx + .update(rfqsLast) + .set({ + status: "RFQ 삭제", + deleteReason: deleteReason || null, + updatedBy: userId, + updatedAt: new Date() + }) + .where(eq(rfqsLast.id, rfq.id)); + + // 연결된 모든 TBE 세션을 "취소" 상태로 변경 + // TBE 세션이 없어도 정상 동작 (조건에 맞는 레코드가 없으면 업데이트 없이 종료) + await tx + .update(rfqLastTbeSessions) + .set({ + status: "취소", + updatedBy: userId, + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastTbeSessions.rfqsLastId, rfq.id), + inArray(rfqLastTbeSessions.status, ["생성중", "준비중", "진행중", "검토중", "보류"]) + ) + ); + + // 연결된 모든 vendor response를 "취소" 상태로 변경 (RFQ 삭제 처리) + // 참고: 스키마에 "RFQ 삭제" 상태가 없으므로 "취소" 상태를 사용 + await tx + .update(rfqLastVendorResponses) + .set({ + status: "취소", + updatedBy: userId, + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastVendorResponses.rfqsLastId, rfq.id), + eq(rfqLastVendorResponses.isLatest, true), + inArray(rfqLastVendorResponses.status, ["대기중", "작성중", "제출완료", "수정요청", "최종확정"]) + ) + ); + + // 업체선정이 진행중인 경우 취소 처리 + // isSelected가 true인 경우 또는 contractStatus가 "일반계약 진행중"인 경우 + await tx + .update(rfqLastDetails) + .set({ + isSelected: false, + selectionDate: null, + selectionReason: null, + selectedBy: null, + contractStatus: null, // 계약 상태 초기화 + updatedBy: userId, + updatedAt: new Date() + }) + .where( + and( + eq(rfqLastDetails.rfqsLastId, rfq.id), + eq(rfqLastDetails.isLatest, true) + ) + ); + }); + + // 2-3. 캐시 갱신 + revalidatePath("/evcp/rfq-last"); + revalidatePath(`/evcp/rfq-last/${rfq.id}`); + + results.push({ + rfqId: rfq.id, + success: true + }); + + } catch (error) { + console.error(`RFQ 삭제 실패 (ID: ${rfq.id}, ANFNR: ${rfq.ANFNR}):`, error); + results.push({ + rfqId: rfq.id, + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + 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