summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/techsales-rfq/approval-actions.ts42
-rw-r--r--lib/techsales-rfq/approval-handlers.ts150
-rw-r--r--lib/techsales-rfq/service.ts27
-rw-r--r--lib/techsales-rfq/table/tech-sales-rfq-attachments-sheet.tsx143
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