diff options
Diffstat (limited to 'lib/techsales-rfq/approval-handlers.ts')
| -rw-r--r-- | lib/techsales-rfq/approval-handlers.ts | 313 |
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 || '사유 없음', + }; +} + |
