diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-08 06:16:32 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-12-08 06:16:32 +0000 |
| commit | ad29c8d9dc5ce3f57d1e994e84603edcdb961c12 (patch) | |
| tree | 9dc720cb67a9b7c3ea33c1d90bc47b44d184f3b4 /lib | |
| parent | e3593cf5c29dcae064b4b07b699e4ccf1cd90430 (diff) | |
(최겸) 기술영업 drm 첨부 시 결재 로직 적용
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/techsales-rfq/approval-actions.ts | 42 | ||||
| -rw-r--r-- | lib/techsales-rfq/approval-handlers.ts | 150 | ||||
| -rw-r--r-- | lib/techsales-rfq/service.ts | 27 | ||||
| -rw-r--r-- | lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx | 143 |
4 files changed, 238 insertions, 124 deletions
diff --git a/lib/techsales-rfq/approval-actions.ts b/lib/techsales-rfq/approval-actions.ts index 335be760..cf914592 100644 --- a/lib/techsales-rfq/approval-actions.ts +++ b/lib/techsales-rfq/approval-actions.ts @@ -172,17 +172,19 @@ function getTechSalesRevalidationPath(rfqType: "SHIP" | "TOP" | "HULL"): string } } /** - * 기술영업 RFQ 재발송 결재 상신 + * 기술영업 RFQ DRM 첨부 해제 결재 상신 * - * 이미 발송된 RFQ에 DRM 파일이 추가된 경우 재발송을 위한 결재 상신 + * 이미 발송된 RFQ에 DRM 파일이 추가된 경우 DRM 해제를 위한 결재 상신 */ export async function requestRfqResendWithDrmApproval(data: { rfqId: number; rfqCode?: string; - drmFiles: Array<{ - file: File; - attachmentType: string; - description?: string; + drmAttachmentIds: number[]; + drmAttachments: Array<{ + id: number; + fileName?: string | null; + fileSize?: number | null; + attachmentType?: string | null; }>; applicationReason: string; currentUser: { @@ -197,18 +199,18 @@ export async function requestRfqResendWithDrmApproval(data: { throw new Error('Knox EP ID가 필요합니다.'); } - console.log('[RFQ Resend Approval] Starting resend approval process'); - console.log('[RFQ Resend Approval] RFQ ID:', data.rfqId); - console.log('[RFQ Resend Approval] DRM Files:', data.drmFiles.length); + 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.drmFiles.map(f => ({ - fileName: f.file.name, - fileSize: f.file.size, + attachments: data.drmAttachments.map(att => ({ + fileName: att.fileName, + fileSize: att.fileSize, })), - vendorNames: [], // 기존 벤더 목록은 후처리에서 조회 + vendorNames: [], applicationReason: data.applicationReason, }); @@ -216,7 +218,7 @@ export async function requestRfqResendWithDrmApproval(data: { const approvalPayload = { rfqId: data.rfqId, rfqCode: data.rfqCode, - drmFiles: data.drmFiles, + drmAttachmentIds: data.drmAttachmentIds, currentUser: { id: data.currentUser.id, name: data.currentUser.name, @@ -231,8 +233,8 @@ export async function requestRfqResendWithDrmApproval(data: { 'tech_sales_rfq_resend_with_drm', // 핸들러 키 approvalPayload, { - title: `DRM 파일 재발송 결재 - ${data.rfqCode || 'RFQ'}`, - description: `이미 발송된 RFQ에 ${data.drmFiles.length}개의 DRM 파일이 추가되어 재발송을 요청합니다.`, + title: `DRM 파일 해제 결재 - ${data.rfqCode || 'RFQ'}`, + description: `발송 완료된 RFQ에 추가된 DRM 첨부파일 ${data.drmAttachmentIds.length}개 해제를 요청합니다.`, templateName: '암호화해제 신청', variables, approvers: data.approvers, @@ -247,20 +249,20 @@ export async function requestRfqResendWithDrmApproval(data: { const result = await saga.execute(); - console.log('[RFQ Resend Approval] ✅ Resend approval submitted successfully'); + console.log('[RFQ DRM Unlock Approval] ✅ DRM unlock approval submitted successfully'); return { success: true, ...result, - message: `재발송 결재가 상신되었습니다. (결재 ID: ${result.approvalId})`, + message: `DRM 해제 결재가 상신되었습니다. (결재 ID: ${result.approvalId})`, }; } catch (error) { - console.error('[RFQ Resend Approval] ❌ Failed to submit resend approval:', error); + console.error('[RFQ DRM Unlock Approval] ❌ Failed to submit DRM unlock approval:', error); throw new Error( error instanceof Error ? error.message - : 'RFQ 재발송 결재 상신에 실패했습니다.' + : 'RFQ DRM 해제 결재 상신에 실패했습니다.' ); } } diff --git a/lib/techsales-rfq/approval-handlers.ts b/lib/techsales-rfq/approval-handlers.ts index 979096b7..9b7b61ff 100644 --- a/lib/techsales-rfq/approval-handlers.ts +++ b/lib/techsales-rfq/approval-handlers.ts @@ -8,7 +8,7 @@ import db from '@/db/db'; import { eq, and } from 'drizzle-orm'; -import { techSalesAttachments, techSalesRfqs, TECH_SALES_RFQ_STATUSES } from '@/db/schema/techSales'; +import { techSalesAttachments } from '@/db/schema/techSales'; import { sendTechSalesRfqToVendors } from './service'; import { decryptWithServerAction } from '@/components/drm/drmUtils'; import { saveFile, deleteFile } from '@/lib/file-stroage'; @@ -127,18 +127,14 @@ export async function sendTechSalesRfqWithApprovalInternal(payload: { } /** - * 기술영업 RFQ 재발송 핸들러 (결재 승인 후 자동 실행) - * - * 이미 발송된 RFQ에 DRM 파일이 추가된 경우 재발송 처리 + * 기술영업 RFQ DRM 해제 핸들러 (결재 승인 후 자동 실행) + * + * 이미 발송된 RFQ에 추가된 DRM 첨부파일을 복호화하여 저장한다. */ export async function resendTechSalesRfqWithDrmInternal(payload: { rfqId: number; rfqCode?: string; - drmFiles: Array<{ - file: File; - attachmentType: string; - description?: string; - }>; + drmAttachmentIds?: number[]; currentUser: { id: string | number; name?: string | null; @@ -146,104 +142,94 @@ export async function resendTechSalesRfqWithDrmInternal(payload: { 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); + 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 파일들 복호화 및 저장 - for (const drmFile of payload.drmFiles) { + // 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 Resend Handler] Decrypting: ${drmFile.file.name}`); - const decryptedBuffer = await decryptWithServerAction(drmFile.file); - - // 복호화된 파일 저장 + console.log(`[TechSales RFQ DRM Unlock Handler] Decrypting: ${attachment.originalFileName}`); + const decryptedBuffer = await decryptWithServerAction(file); + + // 복호화된 파일로 재저장 (기존 파일은 보존) const saveResult = await saveFile({ - file: new File([decryptedBuffer], drmFile.file.name), + file: new File([decryptedBuffer], attachment.originalFileName), 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, + + // DB 업데이트: drmEncrypted = false, filePath/fileName 갱신 + await db.update(techSalesAttachments) + .set({ + drmEncrypted: false, filePath: saveResult.publicPath!, + fileName: saveResult.fileName!, 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}`); + }) + .where(eq(techSalesAttachments.id, attachment.id)); + + console.log(`[TechSales RFQ DRM Unlock Handler] ✅ Decrypted and saved: ${attachment.originalFileName}`); } catch (error) { - console.error(`[TechSales RFQ Resend Handler] ❌ Failed to decrypt ${drmFile.file.name}:`, error); + console.error(`[TechSales RFQ DRM Unlock 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 { 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'); + // 캐시 무효화 + const { revalidateTag, revalidatePath } = await import('next/cache'); + revalidateTag("techSalesRfqs"); + revalidateTag(`techSalesRfq-${payload.rfqId}`); + revalidatePath("/partners/techsales"); return { success: true, - ...sendResult, + message: 'DRM 첨부파일이 해제되었습니다.', }; } catch (error) { - console.error('[TechSales RFQ Resend Handler] ❌ Failed to resend RFQ:', error); + console.error('[TechSales RFQ DRM Unlock Handler] ❌ Failed to unlock DRM:', error); throw new Error( error instanceof Error - ? `RFQ 재발송 실패: ${error.message}` - : 'RFQ 재발송 중 오류가 발생했습니다.' + ? `DRM 해제 실패: ${error.message}` + : 'DRM 해제 중 오류가 발생했습니다.' ); } } diff --git a/lib/techsales-rfq/service.ts b/lib/techsales-rfq/service.ts index 8ce41cba..e52c7a36 100644 --- a/lib/techsales-rfq/service.ts +++ b/lib/techsales-rfq/service.ts @@ -1971,12 +1971,6 @@ export async function processTechSalesRfqAttachments(params: { 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");
- }
}
});
@@ -1990,20 +1984,19 @@ export async function processTechSalesRfqAttachments(params: { return {
data: results,
error: null,
- message: `${results.uploaded.length}개 업로드, ${results.deleted.length}개 삭제 완료`
+ message: `${results.uploaded.length}개 업로드, ${results.deleted.length}개 삭제 완료`,
+ requiresDrmApproval: rfq.status === TECH_SALES_RFQ_STATUSES.RFQ_SENT && results.uploaded.some(att => att.drmEncrypted),
+ drmAttachments: results.uploaded
+ .filter(att => att.drmEncrypted)
+ .map(att => ({
+ id: att.id,
+ fileName: att.originalFileName || att.fileName,
+ fileSize: att.fileSize,
+ attachmentType: att.attachmentType,
+ })),
};
} 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) };
}
}
diff --git a/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx b/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx index f2ae1084..95d4e1a3 100644 --- a/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx +++ b/lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx @@ -49,6 +49,10 @@ import { import prettyBytes from "pretty-bytes"
import { formatDate } from "@/lib/utils"
import { processTechSalesRfqAttachments } from "@/lib/techsales-rfq/service"
+import { mapTechSalesRfqSendToTemplateVariables } from "@/lib/techsales-rfq/approval-handlers"
+import { requestRfqResendWithDrmApproval } from "@/lib/techsales-rfq/approval-actions"
+import { ApplicationReasonDialog } from "@/lib/rfq-last/vendor/application-reason-dialog"
+import { ApprovalPreviewDialog } from "@/lib/approval/client"
import { useSession } from "next-auth/react"
const MAX_FILE_SIZE = 6e8 // 600MB
@@ -130,6 +134,18 @@ export function TechSalesRfqAttachmentsSheet({ }: TechSalesRfqAttachmentsSheetProps) {
const [isPending, setIsPending] = React.useState(false)
const session = useSession()
+ const [drmApprovalData, setDrmApprovalData] = React.useState<{
+ attachments: Array<{
+ id: number
+ fileName?: string | null
+ fileSize?: number | null
+ attachmentType?: string | null
+ }>
+ } | null>(null)
+ const [applicationReason, setApplicationReason] = React.useState("")
+ const [approvalPreviewVariables, setApprovalPreviewVariables] = React.useState<Record<string, string> | null>(null)
+ const [showApplicationReasonDialog, setShowApplicationReasonDialog] = React.useState(false)
+ const [showApprovalPreview, setShowApprovalPreview] = React.useState(false)
// 파일 다운로드 핸들러
const handleDownloadClick = React.useCallback(async (filePath: string, fileName: string) => {
@@ -266,6 +282,77 @@ export function TechSalesRfqAttachmentsSheet({ removeExisting(index)
}, [removeExisting])
+ // DRM 해제 결재 - 신청사유 확인
+ const handleApplicationReasonConfirm = React.useCallback(async (reason: string) => {
+ if (!drmApprovalData || !rfq) {
+ toast.error("결재 데이터가 없습니다.")
+ return
+ }
+
+ try {
+ const templateVariables = await mapTechSalesRfqSendToTemplateVariables({
+ attachments: drmApprovalData.attachments.map(att => ({
+ fileName: att.fileName,
+ fileSize: att.fileSize,
+ })),
+ vendorNames: [],
+ applicationReason: reason,
+ })
+
+ setApplicationReason(reason)
+ setApprovalPreviewVariables(templateVariables)
+ setShowApprovalPreview(true)
+ } catch (error) {
+ console.error("템플릿 변수 생성 실패:", error)
+ toast.error("결재 문서 생성에 실패했습니다.")
+ }
+ }, [drmApprovalData, rfq])
+
+ // DRM 해제 결재 - 상신
+ const handleApprovalConfirm = React.useCallback(async (approvalData: {
+ approvers: string[];
+ title: string;
+ description?: string;
+ }) => {
+ if (!drmApprovalData || !rfq) {
+ toast.error("결재 데이터가 없습니다.")
+ return
+ }
+
+ if (!session.data?.user?.epId) {
+ toast.error("Knox EP ID가 필요합니다.")
+ return
+ }
+
+ try {
+ const result = await requestRfqResendWithDrmApproval({
+ rfqId: rfq.id,
+ rfqCode: rfq.rfqCode || undefined,
+ drmAttachmentIds: drmApprovalData.attachments.map(att => att.id),
+ drmAttachments: drmApprovalData.attachments,
+ applicationReason,
+ currentUser: {
+ id: parseInt(session.data?.user.id || "0"),
+ epId: session.data.user.epId || null,
+ name: session.data.user.name || undefined,
+ email: session.data.user.email || undefined,
+ },
+ approvers: approvalData.approvers,
+ })
+
+ toast.success(result.message || "DRM 해제 결재가 상신되었습니다.")
+ setShowApprovalPreview(false)
+ setShowApplicationReasonDialog(false)
+ setDrmApprovalData(null)
+ setApprovalPreviewVariables(null)
+ // 결재 상신 후 시트 닫기
+ props.onOpenChange?.(false)
+ } catch (error) {
+ console.error("DRM 해제 결재 상신 실패:", error)
+ toast.error(error instanceof Error ? error.message : "DRM 해제 결재 상신에 실패했습니다.")
+ }
+ }, [applicationReason, drmApprovalData, rfq, session.data?.user?.email, session.data?.user?.epId, session.data?.user?.id, session.data?.user?.name, props])
+
// Handle form submission
const onSubmit = async (data: AttachmentsFormValues) => {
if (!rfq) {
@@ -314,11 +401,26 @@ export function TechSalesRfqAttachmentsSheet({ } else if (deletedCount > 0) {
successMessage = `${deletedCount}개 파일이 삭제되었습니다.`
}
-
- toast.success(successMessage)
-
- // 다이얼로그 자동 닫기
- props.onOpenChange?.(false)
+
+ const requiresDrmApproval = (result as any)?.requiresDrmApproval
+ const drmAttachments = ((result as any)?.drmAttachments as ExistingTechSalesAttachment[] | undefined) || []
+
+ if (requiresDrmApproval) {
+ toast.success(`${successMessage} DRM 첨부파일이 포함되어 결재가 필요합니다.`)
+ setDrmApprovalData({
+ attachments: drmAttachments.map(att => ({
+ id: att.id,
+ fileName: att.originalFileName || att.fileName,
+ fileSize: att.fileSize,
+ attachmentType: att.attachmentType,
+ }))
+ })
+ setShowApplicationReasonDialog(true)
+ } else {
+ toast.success(successMessage)
+ // 다이얼로그 자동 닫기
+ props.onOpenChange?.(false)
+ }
// // 즉시 첨부파일 목록 새로고침
// const refreshResult = await getTechSalesRfqAttachments(rfq.id)
@@ -565,6 +667,37 @@ export function TechSalesRfqAttachmentsSheet({ </form>
</Form>
</SheetContent>
+
+ {/* DRM 첨부 결재 - 신청사유 입력 */}
+ {drmApprovalData && (
+ <ApplicationReasonDialog
+ open={showApplicationReasonDialog}
+ onOpenChange={setShowApplicationReasonDialog}
+ onConfirm={handleApplicationReasonConfirm}
+ vendorCount={0}
+ attachmentCount={drmApprovalData.attachments.length}
+ />
+ )}
+
+ {/* DRM 첨부 결재 - 미리보기 */}
+ {drmApprovalData && approvalPreviewVariables && (
+ <ApprovalPreviewDialog
+ open={showApprovalPreview}
+ onOpenChange={setShowApprovalPreview}
+ templateName="암호화해제 신청"
+ variables={approvalPreviewVariables}
+ title={`DRM 첨부파일 해제 - ${rfq?.rfqCode || "RFQ"}`}
+ currentUser={{
+ id: parseInt(session.data?.user.id || "0"),
+ epId: session.data?.user.epId,
+ name: session.data?.user.name || undefined,
+ email: session.data?.user.email || undefined,
+ }}
+ onConfirm={handleApprovalConfirm}
+ allowTitleEdit={false}
+ allowDescriptionEdit={false}
+ />
+ )}
</Sheet>
)
}
\ No newline at end of file |
