diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-04 10:03:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-04 10:03:32 +0000 |
| commit | 47fb72704161b4b58a27c7f5c679fc44618de9a1 (patch) | |
| tree | af4fe1517352784d1876c164171f6dba2e40403a /lib/rfq-last/delete-action.ts | |
| parent | 1a034c7f6f50e443bc9f97c3d84bfb0a819af6ce (diff) | |
(최겸) 구매 견적 내 RFQ Cancel/Delete, 연동제 적용, MRC Type 개발
Diffstat (limited to 'lib/rfq-last/delete-action.ts')
| -rw-r--r-- | lib/rfq-last/delete-action.ts | 199 |
1 files changed, 199 insertions, 0 deletions
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 삭제 처리 중 오류가 발생했습니다."
+ };
+ }
+}
+
|
