/** * 기술영업 RFQ 발송 결재 핸들러 * * DRM 파일이 있는 기술영업 RFQ 발송 시 결재 승인 후 실제 발송을 처리하는 핸들러 */ 'use server'; import db from '@/db/db'; import { eq, and } from 'drizzle-orm'; import { techSalesAttachments } 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 DRM 해제 핸들러 (결재 승인 후 자동 실행) * * 이미 발송된 RFQ에 추가된 DRM 첨부파일을 복호화하여 저장한다. */ export async function resendTechSalesRfqWithDrmInternal(payload: { rfqId: number; rfqCode?: string; drmAttachmentIds?: number[]; currentUser: { id: string | number; name?: string | null; email?: string | null; epId?: string | null; }; }) { console.log('[TechSales RFQ DRM Unlock Handler] Starting DRM unlock after approval'); console.log('[TechSales RFQ DRM Unlock Handler] RFQ ID:', payload.rfqId); console.log('[TechSales RFQ DRM Unlock Handler] DRM Attachment Ids:', payload.drmAttachmentIds?.length || 0); try { // 1) DRM 첨부 조회 (지정된 ID가 있으면 필터링) const drmAttachments = await db.query.techSalesAttachments.findMany({ where: and( eq(techSalesAttachments.techSalesRfqId, payload.rfqId), eq(techSalesAttachments.drmEncrypted, true), ) }); const filteredAttachments = payload.drmAttachmentIds && payload.drmAttachmentIds.length > 0 ? drmAttachments.filter(att => payload.drmAttachmentIds?.includes(att.id)) : drmAttachments; if (filteredAttachments.length === 0) { console.log('[TechSales RFQ DRM Unlock Handler] No DRM attachments to process'); return { success: true, message: '처리할 DRM 첨부파일이 없습니다.' }; } const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || process.env.NEXT_PUBLIC_URL; for (const attachment of filteredAttachments) { try { // DRM 파일 다운로드 let fileUrl = attachment.filePath; const absoluteUrl = `${baseUrl}${fileUrl}`; console.log(`[TechSales RFQ DRM Unlock Handler] Fetching file from: ${absoluteUrl}`); const fileResponse = await fetch(absoluteUrl); if (!fileResponse.ok) { console.error(`[TechSales RFQ DRM Unlock Handler] Failed to fetch file: ${absoluteUrl} (status: ${fileResponse.status})`); continue; } const fileBlob = await fileResponse.blob(); const file = new File([fileBlob], attachment.originalFileName); // DRM 복호화 console.log(`[TechSales RFQ DRM Unlock 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 || '파일 저장 실패'); } // DB 업데이트: drmEncrypted = false, filePath/fileName 갱신 await db.update(techSalesAttachments) .set({ drmEncrypted: false, filePath: saveResult.publicPath!, fileName: saveResult.fileName!, fileSize: decryptedBuffer.byteLength, }) .where(eq(techSalesAttachments.id, attachment.id)); console.log(`[TechSales RFQ DRM Unlock Handler] ✅ Decrypted and saved: ${attachment.originalFileName}`); } catch (error) { console.error(`[TechSales RFQ DRM Unlock Handler] ❌ Failed to decrypt ${attachment.originalFileName}:`, error); throw error; } } // 캐시 무효화 const { revalidateTag, revalidatePath } = await import('next/cache'); revalidateTag("techSalesRfqs"); revalidateTag(`techSalesRfq-${payload.rfqId}`); revalidatePath("/partners/techsales"); return { success: true, message: 'DRM 첨부파일이 해제되었습니다.', }; } catch (error) { console.error('[TechSales RFQ DRM Unlock Handler] ❌ Failed to unlock DRM:', error); throw new Error( error instanceof Error ? `DRM 해제 실패: ${error.message}` : 'DRM 해제 중 오류가 발생했습니다.' ); } } /** * 템플릿 변수 매핑 함수 * 기술영업 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 || '사유 없음', }; }