From be5d5ab488ae875e7c56306403aba923e1784021 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 18 Nov 2025 10:33:51 +0000 Subject: (최겸) 기술영업 첨부파일 결재 등록 및 요구사항 개발 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/techsales-rfq/service.ts | 145 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 21 deletions(-) (limited to 'lib/techsales-rfq/service.ts') diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index 9a198ee5..dc5950e0 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -36,8 +36,9 @@ import { sendEmail } from "../mail/sendEmail"; import { formatDate } from "../utils"; import { itemShipbuilding, itemOffshoreTop, itemOffshoreHull } from "@/db/schema/items"; import { techVendors, techVendorPossibleItems, techVendorContacts } from "@/db/schema/techVendors"; -import { deleteFile, saveDRMFile, saveFile } from "@/lib/file-stroage"; -import { decryptWithServerAction } from "@/components/drm/drmUtils"; +import { deleteFile, saveFile } from "@/lib/file-stroage"; +import { decryptWithServerAction, isDRMFile } from "@/components/drm/drmUtils"; +import { TECH_SALES_RFQ_STATUSES } from "@/db/schema/techSales"; // RFQ 아이템 정보 타입 interface RfqItemInfo { itemCode: string; @@ -558,15 +559,25 @@ export async function sendTechSalesRfqToVendors(input: { }; } - // 발송 가능한 상태인지 확인 - if (rfq.status !== "RFQ Vendor Assignned" && rfq.status !== "RFQ Sent") { + // 발송 가능한 상태인지 확인 (결재 진행중 상태는 제외) + if (rfq.status !== TECH_SALES_RFQ_STATUSES.RFQ_VENDOR_ASSIGNED && + rfq.status !== TECH_SALES_RFQ_STATUSES.RFQ_SENT && + rfq.status !== TECH_SALES_RFQ_STATUSES.APPROVAL_IN_PROGRESS) { return { success: false, message: "벤더가 할당된 RFQ 또는 이미 전송된 RFQ만 다시 전송할 수 있습니다", }; } + + // 결재 진행중 상태에서는 결재 승인 후처리 핸들러에서만 발송 가능 + if (rfq.status === TECH_SALES_RFQ_STATUSES.APPROVAL_IN_PROGRESS) { + return { + success: false, + message: "결재 진행 중인 RFQ는 결재 승인 후 자동으로 발송됩니다", + }; + } - const isResend = rfq.status === "RFQ Sent"; + const isResend = rfq.status === TECH_SALES_RFQ_STATUSES.RFQ_SENT; // 현재 사용자 정보 조회 const sender = await db.query.users.findFirst({ @@ -615,11 +626,39 @@ export async function sendTechSalesRfqToVendors(input: { }; } + // RFQ 첨부파일 중 DRM 파일 확인 + const attachments = await db.query.techSalesAttachments.findMany({ + where: eq(techSalesAttachments.techSalesRfqId, input.rfqId), + columns: { + id: true, + drmEncrypted: true, + fileName: true, + fileSize: true, + originalFileName: true, + } + }); + + const drmAttachments = attachments.filter(att => att.drmEncrypted === true); + + // DRM 파일이 있으면 결재 프로세스 필요 (이 함수는 직접 발송하지 않고 결재 필요 신호 반환) + if (drmAttachments.length > 0) { + return { + success: false, + requiresApproval: true, + message: "DRM 파일이 포함되어 있어 결재가 필요합니다", + drmAttachmentIds: drmAttachments.map(att => att.id), + drmAttachments: drmAttachments.map(att => ({ + fileName: att.originalFileName || att.fileName, + fileSize: att.fileSize, + })), + }; + } + // 트랜잭션 시작 await db.transaction(async (tx) => { // 1. RFQ 상태 업데이트 (최초 발송인 경우 rfqSendDate 설정) const updateData: Partial = { - status: "RFQ Sent", + status: TECH_SALES_RFQ_STATUSES.RFQ_SENT, sentBy: Number(session.user.id), updatedBy: Number(session.user.id), updatedAt: new Date(), @@ -1544,13 +1583,15 @@ export async function createTechSalesRfqAttachments(params: { await db.transaction(async (tx) => { for (const file of files) { - + // DRM 파일 검출 + const isDrmFile = await isDRMFile(file); - const saveResult = await saveDRMFile( + // saveFile로 변경 (DRM 복호화하지 않고 원본 저장) + const saveResult = await saveFile({ file, - decryptWithServerAction, - `techsales-rfq/${techSalesRfqId}` - ); + directory: `techsales-rfq/${techSalesRfqId}`, + userId: String(createdBy), + }); if (!saveResult.success) { throw new Error(saveResult.error || "파일 저장에 실패했습니다."); @@ -1566,6 +1607,7 @@ export async function createTechSalesRfqAttachments(params: { fileSize: file.size, fileType: file.type || undefined, description: description || undefined, + drmEncrypted: isDrmFile, // DRM 파일 여부 저장 createdBy, }).returning(); @@ -1652,6 +1694,39 @@ export async function getTechSalesRfqAttachmentsByType( } } +/** + * 벤더용 RFQ 첨부파일 조회 (DRM 해제된 파일만 반환) + */ +export async function getRfqAttachmentsForVendor(techSalesRfqId: number) { + unstable_noStore(); + try { + // DRM 해제된 파일만 조회 (drmEncrypted = false) + const attachments = await db.query.techSalesAttachments.findMany({ + where: and( + eq(techSalesAttachments.techSalesRfqId, techSalesRfqId), + eq(techSalesAttachments.drmEncrypted, false) + ), + orderBy: [desc(techSalesAttachments.createdAt)], + columns: { + id: true, + fileName: true, + originalFileName: true, + filePath: true, + fileSize: true, + fileType: true, + attachmentType: true, + description: true, + createdAt: true, + } + }); + + return { data: attachments, error: null }; + } catch (err) { + console.error("벤더용 기술영업 RFQ 첨부파일 조회 오류:", err); + return { data: [], error: getErrorMessage(err) }; + } +} + /** * 기술영업 RFQ 첨부파일 삭제 */ @@ -1834,17 +1909,23 @@ export async function processTechSalesRfqAttachments(params: { } // 2. 새 파일 업로드 처리 - if (newFiles.length > 0) { - for (const { file, attachmentType, description } of newFiles) { - const saveResult = await saveDRMFile( - file, - decryptWithServerAction, - `techsales-rfq/${techSalesRfqId}` - ); + if (newFiles.length > 0) { + const drmFiles: Array<{ file: File; attachmentType: string; description?: string }> = []; + + for (const { file, attachmentType, description } of newFiles) { + // DRM 파일 검출 + const isDrmFile = await isDRMFile(file); + + // saveFile로 변경 (DRM 복호화하지 않고 원본 저장) + const saveResult = await saveFile({ + file, + directory: `techsales-rfq/${techSalesRfqId}`, + userId: String(createdBy), + }); - if (!saveResult.success) { - throw new Error(saveResult.error || "파일 저장에 실패했습니다."); - } + if (!saveResult.success) { + throw new Error(saveResult.error || "파일 저장에 실패했습니다."); + } // DB에 첨부파일 레코드 생성 const [newAttachment] = await tx.insert(techSalesAttachments).values({ @@ -1856,10 +1937,22 @@ export async function processTechSalesRfqAttachments(params: { fileSize: file.size, fileType: file.type || undefined, description: description || undefined, + drmEncrypted: isDrmFile, // DRM 파일 여부 저장 createdBy, }).returning(); results.uploaded.push(newAttachment); + + // DRM 파일인 경우 목록에 추가 + if (isDrmFile) { + drmFiles.push({ file, attachmentType, description }); + } + } + + // RFQ 상태가 "RFQ Sent"이고 DRM 파일이 추가된 경우 재발송 결재 트리거 + if (rfq.status === TECH_SALES_RFQ_STATUSES.RFQ_SENT && drmFiles.length > 0) { + // 트랜잭션 외부에서 처리하기 위해 에러로 전달 + throw new Error("DRM_FILE_ADDED_TO_SENT_RFQ"); } } }); @@ -1878,6 +1971,16 @@ export async function processTechSalesRfqAttachments(params: { }; } catch (err) { console.error("기술영업 RFQ 첨부파일 일괄 처리 오류:", err); + + // DRM 파일 추가로 인한 재발송 결재 필요 에러 + if (err instanceof Error && err.message === "DRM_FILE_ADDED_TO_SENT_RFQ") { + return { + data: null, + error: "DRM_FILE_ADDED_TO_SENT_RFQ", + rfqId: techSalesRfqId, + }; + } + return { data: null, error: getErrorMessage(err) }; } } -- cgit v1.2.3