summaryrefslogtreecommitdiff
path: root/lib/techsales-rfq/approval-handlers.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/techsales-rfq/approval-handlers.ts')
-rw-r--r--lib/techsales-rfq/approval-handlers.ts313
1 files changed, 313 insertions, 0 deletions
diff --git a/lib/techsales-rfq/approval-handlers.ts b/lib/techsales-rfq/approval-handlers.ts
new file mode 100644
index 00000000..6ffd9fb4
--- /dev/null
+++ b/lib/techsales-rfq/approval-handlers.ts
@@ -0,0 +1,313 @@
+/**
+ * 기술영업 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 파일 다운로드
+ const fileResponse = await fetch(attachment.filePath);
+ if (!fileResponse.ok) {
+ console.error(`[TechSales RFQ Approval Handler] Failed to fetch file: ${attachment.filePath}`);
+ 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 상태를 "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 sendResult = await sendTechSalesRfqToVendors({
+ rfqId: payload.rfqId,
+ vendorIds: payload.vendorIds,
+ selectedContacts: payload.selectedContacts,
+ });
+
+ 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,
+ });
+
+ 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 || '사유 없음',
+ };
+}
+