/** * 기술영업 RFQ 발송 결재 서버 액션 * * DRM 파일이 있는 기술영업 RFQ를 발송할 때 결재를 거치는 서버 액션 */ 'use server'; import { ApprovalSubmissionSaga } from '@/lib/approval'; import { mapTechSalesRfqSendToTemplateVariables } from './approval-handlers'; import { revalidatePath, revalidateTag } from 'next/cache'; interface TechSalesRfqSendApprovalData { // RFQ 기본 정보 rfqId: number; rfqCode?: string; rfqType: "SHIP" | "TOP" | "HULL"; // 발송 데이터 vendorIds: number[]; selectedContacts?: Array<{ vendorId: number; contactId: number; contactEmail: string; contactName: string; }>; drmAttachmentIds: number[]; // 첨부파일 정보 (파일명, 크기 등) drmAttachments: Array<{ fileName?: string | null; fileSize?: number | null; }>; // 신청 사유 applicationReason: string; // 결재 정보 currentUser: { id: number; epId: string | null; name?: string; email?: string; }; approvers?: string[]; // Knox EP ID 배열 } /** * 기술영업 RFQ 발송 결재 상신 (초기 발송) * * DRM 파일이 있는 경우 결재를 거쳐 RFQ를 발송합니다. */ export async function requestTechSalesRfqSendWithApproval(data: TechSalesRfqSendApprovalData) { // 1. 입력 검증 if (!data.currentUser.epId) { throw new Error('Knox EP ID가 필요합니다. 시스템 관리자에게 문의하세요.'); } if (!data.vendorIds || data.vendorIds.length === 0) { throw new Error('발송할 벤더를 선택해주세요.'); } if (!data.drmAttachmentIds || data.drmAttachmentIds.length === 0) { throw new Error('DRM 첨부파일이 없습니다. 결재가 필요하지 않습니다.'); } console.log('[TechSales RFQ Approval] Starting approval process for RFQ send'); console.log('[TechSales RFQ Approval] RFQ ID:', data.rfqId); console.log('[TechSales RFQ Approval] Vendors:', data.vendorIds.length); console.log('[TechSales RFQ Approval] DRM Attachments:', data.drmAttachmentIds.length); try { // 2. RFQ 상태를 "결재 진행중"으로 변경 const db = (await import('@/db/db')).default; const { techSalesRfqs, TECH_SALES_RFQ_STATUSES } = await import('@/db/schema/techSales'); const { eq } = await import('drizzle-orm'); await db.update(techSalesRfqs) .set({ status: TECH_SALES_RFQ_STATUSES.APPROVAL_IN_PROGRESS, updatedAt: new Date(), }) .where(eq(techSalesRfqs.id, data.rfqId)); console.log('[TechSales RFQ Approval] RFQ status updated to APPROVAL_IN_PROGRESS'); // 3. 벤더 이름 조회 const { getTechSalesRfqVendors } = await import('./service'); const vendorsResult = await getTechSalesRfqVendors(data.rfqId); const vendorNames = vendorsResult.data?.filter(v => data.vendorIds.includes(v.vendorId)) .map(v => v.vendorName) || []; // 4. 템플릿 변수 매핑 const variables = await mapTechSalesRfqSendToTemplateVariables({ attachments: data.drmAttachments, vendorNames: vendorNames, applicationReason: data.applicationReason, }); // 5. Knox 상신용 첨부파일 준비 const knoxAttachments = await prepareKnoxDrmAttachments(data.drmAttachmentIds); if (knoxAttachments.length === 0) { throw new Error('상신할 DRM 첨부파일을 준비하지 못했습니다.'); } // 6. 결재 상신용 payload 구성 const approvalPayload = { rfqId: data.rfqId, rfqCode: data.rfqCode, vendorIds: data.vendorIds, selectedContacts: data.selectedContacts, drmAttachmentIds: data.drmAttachmentIds, currentUser: { id: data.currentUser.id, name: data.currentUser.name, email: data.currentUser.email, epId: data.currentUser.epId, }, }; // 7. Saga로 결재 상신 const saga = new ApprovalSubmissionSaga( 'tech_sales_rfq_send_with_drm', // 핸들러 키 approvalPayload, // 결재 승인 후 실행될 데이터 { title: `암호화해제 신청 - ${data.rfqCode || 'RFQ'}`, description: `${vendorNames.length}개 업체에 DRM 첨부파일 ${data.drmAttachmentIds.length}개를 포함한 암호화해제 신청`, templateName: '암호화해제 신청', // DB에 있어야 함 variables, approvers: data.approvers, currentUser: { id: data.currentUser.id, epId: data.currentUser.epId, email: data.currentUser.email, }, attachments: knoxAttachments, } ); const result = await saga.execute(); console.log('[TechSales RFQ Approval] ✅ Approval submitted successfully'); console.log('[TechSales RFQ Approval] Approval ID:', result.approvalId); console.log('[TechSales RFQ Approval] Pending Action ID:', result.pendingActionId); revalidateTag("techSalesRfqs"); revalidateTag("techSalesVendorQuotations"); revalidateTag(`techSalesRfq-${data.rfqId}`); revalidatePath(getTechSalesRevalidationPath(data.rfqType || "SHIP")); return { success: true, ...result, message: `결재가 상신되었습니다. (결재 ID: ${result.approvalId})`, }; } catch (error) { console.error('[TechSales RFQ Approval] ❌ Failed to submit approval:', error); throw new Error( error instanceof Error ? error.message : '기술영업 RFQ 발송 결재 상신에 실패했습니다.' ); } } /** * RFQ 타입에 따른 캐시 무효화 경로 반환 */ function getTechSalesRevalidationPath(rfqType: "SHIP" | "TOP" | "HULL"): string { switch (rfqType) { case "SHIP": return "/evcp/budgetary-tech-sales-ship"; case "TOP": return "/evcp/budgetary-tech-sales-top"; case "HULL": return "/evcp/budgetary-tech-sales-hull"; default: return "/evcp/budgetary-tech-sales-ship"; } } /** * Knox 상신용 DRM 첨부파일을 File 객체로 준비 */ async function prepareKnoxDrmAttachments(attachmentIds: number[]): Promise { if (!attachmentIds || attachmentIds.length === 0) return []; const db = (await import('@/db/db')).default; const { techSalesAttachments } = await import('@/db/schema/techSales'); const { inArray } = await import('drizzle-orm'); const attachments = await db.query.techSalesAttachments.findMany({ where: inArray(techSalesAttachments.id, attachmentIds), columns: { id: true, filePath: true, originalFileName: true, fileName: true, fileType: true, }, }); const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || process.env.NEXT_PUBLIC_URL; const files: File[] = []; for (const attachment of attachments) { if (!attachment.filePath || !baseUrl) { console.error('[TechSales RFQ Approval] 첨부파일 경로나 BASE_URL이 없습니다.', attachment.id); continue; } const fileUrl = `${baseUrl}${attachment.filePath}`; const response = await fetch(fileUrl); if (!response.ok) { console.error(`[TechSales RFQ Approval] 첨부파일 다운로드 실패: ${fileUrl} (status: ${response.status})`); continue; } const blob = await response.blob(); const file = new File( [blob], attachment.originalFileName || attachment.fileName || 'attachment', { type: attachment.fileType || blob.type || 'application/octet-stream', } ); files.push(file); } return files; } /** * 기술영업 RFQ DRM 첨부 해제 결재 상신 * * 이미 발송된 RFQ에 DRM 파일이 추가된 경우 DRM 해제를 위한 결재 상신 */ export async function requestRfqResendWithDrmApproval(data: { rfqId: number; rfqCode?: string; drmAttachmentIds: number[]; drmAttachments: Array<{ id: number; fileName?: string | null; fileSize?: number | null; attachmentType?: string | null; }>; applicationReason: string; currentUser: { id: number; epId: string | null; name?: string; email?: string; }; approvers?: string[]; }) { if (!data.currentUser.epId) { throw new Error('Knox EP ID가 필요합니다.'); } console.log('[RFQ DRM Unlock Approval] Starting DRM unlock approval process'); console.log('[RFQ DRM Unlock Approval] RFQ ID:', data.rfqId); console.log('[RFQ DRM Unlock Approval] DRM Attachments:', data.drmAttachmentIds.length); try { // 템플릿 변수 매핑 const variables = await mapTechSalesRfqSendToTemplateVariables({ attachments: data.drmAttachments.map(att => ({ fileName: att.fileName, fileSize: att.fileSize, })), vendorNames: [], applicationReason: data.applicationReason, }); // DRM 첨부파일을 Knox 상신용 File 객체로 준비 const knoxAttachments = await prepareKnoxDrmAttachments(data.drmAttachmentIds); if (knoxAttachments.length === 0) { throw new Error('상신할 DRM 첨부파일을 준비하지 못했습니다.'); } // 결재 payload 구성 const approvalPayload = { rfqId: data.rfqId, rfqCode: data.rfqCode, drmAttachmentIds: data.drmAttachmentIds, currentUser: { id: data.currentUser.id, name: data.currentUser.name, email: data.currentUser.email, epId: data.currentUser.epId, }, }; console.log('approvalPayload', approvalPayload); // Saga로 결재 상신 const saga = new ApprovalSubmissionSaga( 'tech_sales_rfq_resend_with_drm', // 핸들러 키 approvalPayload, { title: `DRM 파일 해제 결재 - ${data.rfqCode || 'RFQ'}`, description: `발송 완료된 RFQ에 추가된 DRM 첨부파일 ${data.drmAttachmentIds.length}개 해제를 요청합니다.`, templateName: '암호화해제 신청', variables, approvers: data.approvers, currentUser: { id: data.currentUser.id, name: data.currentUser.name, epId: data.currentUser.epId, email: data.currentUser.email, }, attachments: knoxAttachments, } ); const result = await saga.execute(); console.log('[RFQ DRM Unlock Approval] ✅ DRM unlock approval submitted successfully'); return { success: true, ...result, message: `DRM 해제 결재가 상신되었습니다. (결재 ID: ${result.approvalId})`, }; } catch (error) { console.error('[RFQ DRM Unlock Approval] ❌ Failed to submit DRM unlock approval:', error); throw new Error( error instanceof Error ? error.message : 'RFQ DRM 해제 결재 상신에 실패했습니다.' ); } }