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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
|
/**
* 기술영업 RFQ 발송 결재 핸들러
*
* DRM 파일이 있는 기술영업 RFQ 발송 시 결재 승인 후 실제 발송을 처리하는 핸들러
*/
'use server';
import db from '@/db/db';
import { eq, and } from 'drizzle-orm';
import { techSalesAttachments, techSalesRfqs, TECH_SALES_RFQ_STATUSES } from '@/db/schema/techSales';
import { sendTechSalesRfqToVendors } from './service';
import { decryptWithServerAction } from '@/components/drm/drmUtils';
import { saveFile, deleteFile } from '@/lib/file-stroage';
/**
* 기술영업 RFQ 발송 핸들러 (결재 승인 후 자동 실행)
*
* @param payload - 결재 상신 시 저장한 RFQ 발송 데이터
*/
export async function sendTechSalesRfqWithApprovalInternal(payload: {
rfqId: number;
rfqCode?: string;
vendorIds: number[];
selectedContacts?: Array<{
vendorId: number;
contactId: number;
contactEmail: string;
contactName: string;
}>;
drmAttachmentIds: number[];
currentUser: {
id: string | number;
name?: string | null;
email?: string | null;
epId?: string | null;
};
}) {
console.log('[TechSales RFQ Approval Handler] Starting RFQ send after approval');
console.log('[TechSales RFQ Approval Handler] RFQ ID:', payload.rfqId);
console.log('[TechSales RFQ Approval Handler] Vendors count:', payload.vendorIds.length);
console.log('[TechSales RFQ Approval Handler] DRM Attachments count:', payload.drmAttachmentIds.length);
try {
// 1. DRM 파일들 복호화 및 재저장
const drmAttachments = await db.query.techSalesAttachments.findMany({
where: and(
eq(techSalesAttachments.techSalesRfqId, payload.rfqId),
eq(techSalesAttachments.drmEncrypted, true)
)
});
console.log(`[TechSales RFQ Approval Handler] Found ${drmAttachments.length} DRM files to decrypt`);
for (const attachment of drmAttachments) {
try {
// DRM 파일 다운로드 - 상대 경로를 절대 URL로 변환
let fileUrl = attachment.filePath;
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || process.env.NEXT_PUBLIC_URL;
fileUrl = `${baseUrl}${fileUrl}`;
console.log(`[TechSales RFQ Approval Handler] Fetching file from: ${fileUrl}`);
const fileResponse = await fetch(fileUrl);
if (!fileResponse.ok) {
console.error(`[TechSales RFQ Approval Handler] Failed to fetch file: ${fileUrl} (status: ${fileResponse.status})`);
continue;
}
const fileBlob = await fileResponse.blob();
const file = new File([fileBlob], attachment.originalFileName);
// DRM 복호화
console.log(`[TechSales RFQ Approval Handler] Decrypting: ${attachment.originalFileName}`);
const decryptedBuffer = await decryptWithServerAction(file);
// 복호화된 파일로 재저장
const saveResult = await saveFile({
file: new File([decryptedBuffer], attachment.originalFileName),
directory: `techsales-rfq/${payload.rfqId}`,
userId: String(payload.currentUser.id),
});
if (!saveResult.success) {
throw new Error(saveResult.error || '파일 저장 실패');
}
// 기존 파일 삭제
await deleteFile(attachment.filePath);
// DB 업데이트: drmEncrypted = false, filePath 업데이트
await db.update(techSalesAttachments)
.set({
drmEncrypted: false,
filePath: saveResult.publicPath!,
fileName: saveResult.fileName!,
})
.where(eq(techSalesAttachments.id, attachment.id));
console.log(`[TechSales RFQ Approval Handler] ✅ Decrypted and saved: ${attachment.originalFileName}`);
} catch (error) {
console.error(`[TechSales RFQ Approval Handler] ❌ Failed to decrypt ${attachment.originalFileName}:`, error);
throw error;
}
}
// 2. 실제 RFQ 발송 실행 (상태 변경과 이메일 발송은 sendTechSalesRfqToVendors에서 처리)
const sendResult = await sendTechSalesRfqToVendors({
rfqId: payload.rfqId,
vendorIds: payload.vendorIds,
selectedContacts: payload.selectedContacts,
currentUser: payload.currentUser,
});
console.log('[TechSales RFQ Approval Handler] ✅ RFQ sent successfully after DRM decryption');
return {
success: true,
...sendResult,
};
} catch (error) {
console.error('[TechSales RFQ Approval Handler] ❌ Failed to send RFQ:', error);
throw new Error(
error instanceof Error
? `RFQ 발송 실패: ${error.message}`
: 'RFQ 발송 중 오류가 발생했습니다.'
);
}
}
/**
* 기술영업 RFQ 재발송 핸들러 (결재 승인 후 자동 실행)
*
* 이미 발송된 RFQ에 DRM 파일이 추가된 경우 재발송 처리
*/
export async function resendTechSalesRfqWithDrmInternal(payload: {
rfqId: number;
rfqCode?: string;
drmFiles: Array<{
file: File;
attachmentType: string;
description?: string;
}>;
currentUser: {
id: string | number;
name?: string | null;
email?: string | null;
epId?: string | null;
};
}) {
console.log('[TechSales RFQ Resend Handler] Starting DRM resend after approval');
console.log('[TechSales RFQ Resend Handler] RFQ ID:', payload.rfqId);
console.log('[TechSales RFQ Resend Handler] DRM Files:', payload.drmFiles.length);
try {
// 1. 새로 추가된 DRM 파일들 복호화 및 저장
for (const drmFile of payload.drmFiles) {
try {
// DRM 복호화
console.log(`[TechSales RFQ Resend Handler] Decrypting: ${drmFile.file.name}`);
const decryptedBuffer = await decryptWithServerAction(drmFile.file);
// 복호화된 파일 저장
const saveResult = await saveFile({
file: new File([decryptedBuffer], drmFile.file.name),
directory: `techsales-rfq/${payload.rfqId}`,
userId: String(payload.currentUser.id),
});
if (!saveResult.success) {
throw new Error(saveResult.error || '파일 저장 실패');
}
// 기존 DRM 파일 레코드 찾기 및 업데이트
const existingAttachment = await db.query.techSalesAttachments.findFirst({
where: and(
eq(techSalesAttachments.techSalesRfqId, payload.rfqId),
eq(techSalesAttachments.originalFileName, drmFile.file.name),
eq(techSalesAttachments.drmEncrypted, true)
)
});
if (existingAttachment) {
// 기존 파일 삭제
await deleteFile(existingAttachment.filePath);
// DB 업데이트: drmEncrypted = false, filePath 업데이트
await db.update(techSalesAttachments)
.set({
drmEncrypted: false,
filePath: saveResult.publicPath!,
fileName: saveResult.fileName!,
})
.where(eq(techSalesAttachments.id, existingAttachment.id));
} else {
// 새 레코드 생성
await db.insert(techSalesAttachments).values({
techSalesRfqId: payload.rfqId,
attachmentType: drmFile.attachmentType as "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT",
fileName: saveResult.fileName!,
originalFileName: drmFile.file.name,
filePath: saveResult.publicPath!,
fileSize: decryptedBuffer.byteLength,
fileType: drmFile.file.type || undefined,
description: drmFile.description,
drmEncrypted: false, // DRM 해제됨
createdBy: Number(payload.currentUser.id),
});
}
console.log(`[TechSales RFQ Resend Handler] ✅ Decrypted and saved: ${drmFile.file.name}`);
} catch (error) {
console.error(`[TechSales RFQ Resend Handler] ❌ Failed to decrypt ${drmFile.file.name}:`, error);
throw error;
}
}
// 2. RFQ 상태를 "RFQ Sent"로 변경
await db.update(techSalesRfqs)
.set({
status: TECH_SALES_RFQ_STATUSES.RFQ_SENT,
updatedAt: new Date(),
})
.where(eq(techSalesRfqs.id, payload.rfqId));
// 3. RFQ 재발송 실행 (기존에 할당된 모든 벤더에게)
const { getTechSalesRfqVendors } = await import('./service');
const vendorsResult = await getTechSalesRfqVendors(payload.rfqId);
const vendorIds = vendorsResult.data?.map(v => v.vendorId) || [];
const sendResult = await sendTechSalesRfqToVendors({
rfqId: payload.rfqId,
vendorIds: vendorIds,
currentUser: payload.currentUser,
});
console.log('[TechSales RFQ Resend Handler] ✅ RFQ resent successfully after DRM decryption');
return {
success: true,
...sendResult,
};
} catch (error) {
console.error('[TechSales RFQ Resend Handler] ❌ Failed to resend RFQ:', error);
throw new Error(
error instanceof Error
? `RFQ 재발송 실패: ${error.message}`
: 'RFQ 재발송 중 오류가 발생했습니다.'
);
}
}
/**
* 템플릿 변수 매핑 함수
* 기술영업 RFQ 발송 정보를 결재 템플릿 변수로 변환
*/
export async function mapTechSalesRfqSendToTemplateVariables(data: {
attachments: Array<{
fileName?: string | null;
fileSize?: number | null;
}>;
vendorNames: string[];
applicationReason: string;
}) {
const { htmlTableConverter, htmlListConverter } = await import('@/lib/approval/template-utils');
// 파일 크기를 읽기 쉬운 형식으로 변환
const formatFileSize = (bytes?: number | null): string => {
if (!bytes || bytes === 0) return '-';
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
};
// 첨부파일 테이블 데이터 준비 (순번, 파일명, 파일크기만)
const attachmentTableData = data.attachments.map((att, index) => ({
'순번': String(index + 1),
'파일명': att.fileName || '-',
'파일 크기': formatFileSize(att.fileSize),
}));
// 첨부파일 테이블 HTML 생성
const attachmentTableHtml = await htmlTableConverter(
attachmentTableData.length > 0 ? attachmentTableData : [],
[
{ key: '순번', label: '순번' },
{ key: '파일명', label: '파일명' },
{ key: '파일 크기', label: '파일 크기' },
]
);
// 제출처 (벤더 이름들) HTML 생성
const vendorListHtml = await htmlListConverter(
data.vendorNames.length > 0
? data.vendorNames
: ['제출처가 없습니다.']
);
return {
'파일 테이블': attachmentTableHtml,
'제출처': vendorListHtml,
'신청사유': data.applicationReason || '사유 없음',
};
}
|