summaryrefslogtreecommitdiff
path: root/lib/rfq-last/cancel-vendor-response-action.ts
blob: e329a551cb09bb1b6a58b7933f7265494e89af8b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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 취소 처리 중 오류가 발생했습니다."
    };
  }
}