/** * 기술영업 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 || '사유 없음', }; }