diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-22 02:57:00 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-22 02:57:00 +0000 |
| commit | ee57cc221ff2edafd3c0f12a181214c602ed257e (patch) | |
| tree | 148f552f503798f7a350d6eff936b889f16be49f /app/api/revision-upload-ship | |
| parent | 14f61e24947fb92dd71ec0a7196a6e815f8e66da (diff) | |
(대표님, 최겸) 이메일 템플릿, 벤더데이터 변경사항 대응, 기술영업 변경요구사항 구현
Diffstat (limited to 'app/api/revision-upload-ship')
| -rw-r--r-- | app/api/revision-upload-ship/route.ts | 90 |
1 files changed, 62 insertions, 28 deletions
diff --git a/app/api/revision-upload-ship/route.ts b/app/api/revision-upload-ship/route.ts index 549d15bd..38762e5d 100644 --- a/app/api/revision-upload-ship/route.ts +++ b/app/api/revision-upload-ship/route.ts @@ -1,8 +1,4 @@ import { NextRequest, NextResponse } from "next/server" -import { writeFile } from "fs/promises" -import { join } from "path" -import { v4 as uuidv4 } from "uuid" -import path from "path" import { revalidateTag } from "next/cache" import db from "@/db/db" @@ -14,6 +10,9 @@ import { } from "@/db/schema/vendorDocu" import { and, eq } from "drizzle-orm" +/* 보안 강화된 파일 저장 유틸리티 */ +import { saveFile, SaveFileResult } from "@/lib/file-stroage" + /* change log 유틸 */ import { logRevisionChange, @@ -42,13 +41,16 @@ export async function POST(request: NextRequest) { if (!attachmentFiles.length) return NextResponse.json({ error: "No files provided" }, { status: 400 }) + // 기본 파일 크기 검증 (보안 함수에서도 검증하지만 조기 체크) const MAX = 50 * 1024 * 1024 // 50MB (다이얼로그 제한과 맞춤) - for (const f of attachmentFiles) - if (f.size > MAX) + for (const f of attachmentFiles) { + if (f.size > MAX) { return NextResponse.json( { error: `${f.name} exceeds 50MB limit` }, { status: 400 } ) + } + } /* ------- 계약 ID 확보 ------- */ const [docInfo] = await db @@ -93,7 +95,7 @@ export async function POST(request: NextRequest) { /* ------- 트랜잭션 ------- */ const result = await db.transaction(async (tx) => { - /* Revision 생성 */ + /* Revision 생성/업데이트 */ const today = new Date().toISOString().slice(0, 10) // 동일한 revision이 이미 있는지 확인 (usage, usageType도 포함) @@ -187,34 +189,49 @@ export async function POST(request: NextRequest) { ) } - /* 첨부파일 처리 */ + /* ------- 보안 강화된 첨부파일 처리 ------- */ const uploadedFiles: any[] = [] - const baseDir = join(process.cwd(), "public", "documents") + const securityFailures: string[] = [] for (const file of attachmentFiles) { - const ext = path.extname(file.name) - const fname = uuidv4() + ext - const dest = join(baseDir, fname) + console.log(`🔐 보안 검증 시작: ${file.name}`) + + // 보안 강화된 파일 저장 + const saveResult: SaveFileResult = await saveFile({ + file, + directory: "documents", // 문서 전용 디렉토리 + originalName: file.name, + userId: uploaderName || "anonymous", // 업로더 정보 로깅용 + }) + + if (!saveResult.success) { + console.error(`❌ 파일 보안 검증 실패: ${file.name} - ${saveResult.error}`) + securityFailures.push(`${file.name}: ${saveResult.error}`) + continue // 실패한 파일은 건너뛰고 계속 진행 + } - await writeFile(dest, Buffer.from(await file.arrayBuffer())) + console.log(`✅ 파일 보안 검증 통과: ${file.name}`) + console.log(`📁 저장된 경로: ${saveResult.publicPath}`) + // DB에 첨부파일 정보 저장 const [att] = await tx.insert(documentAttachments) .values({ revisionId, - fileName: file.name, - filePath: "/documents/" + fname, - fileSize: file.size, - fileType: ext.slice(1).toLowerCase() || undefined, + fileName: saveResult.originalName!, // 원본 파일명 + filePath: saveResult.publicPath!, // 웹 접근 경로 + fileSize: saveResult.fileSize!, + fileType: saveResult.fileName!.split('.').pop()?.toLowerCase() || undefined, updatedAt: new Date(), }) .returning() uploadedFiles.push({ id: att.id, - fileName: file.name, - fileSize: file.size, - filePath: att.filePath, - fileType: ext.slice(1).toLowerCase() || null, // ✅ 추가 + fileName: saveResult.originalName, + fileSize: saveResult.fileSize, + filePath: saveResult.publicPath, + fileType: saveResult.fileName!.split('.').pop()?.toLowerCase() || null, + securityChecks: saveResult.securityChecks, // 보안 검증 결과 }) // change_logs: attachment CREATE @@ -230,6 +247,16 @@ export async function POST(request: NextRequest) { ) } + // 보안 검증 실패한 파일이 있으면 경고 반환 + if (securityFailures.length > 0) { + console.warn(`⚠️ 일부 파일의 보안 검증 실패:`, securityFailures) + + // 모든 파일이 실패한 경우 에러 반환 + if (uploadedFiles.length === 0) { + throw new Error(`모든 파일의 보안 검증이 실패했습니다: ${securityFailures.join(', ')}`) + } + } + /* documents.updatedAt 업데이트 */ await tx.update(documents) .set({ updatedAt: new Date() }) @@ -237,38 +264,45 @@ export async function POST(request: NextRequest) { return { revisionId, - issueStageId, // ✅ 추가 + issueStageId, stage, revision, uploadedFiles, contractId: docInfo.contractId, usage, - usageType + usageType, + securityFailures // 보안 실패 정보 포함 } }) // 캐시 무효화 try { revalidateTag(`sync-status-${result.contractId}`) - console.log(`✅ Cache invalidated for contract ${result.contractId}`) } catch (cacheError) { console.warn('⚠️ Cache invalidation failed:', cacheError) } + // 응답 메시지 구성 + let message = `리비전 ${result.revision}이 성공적으로 업로드되었습니다` + if (result.securityFailures.length > 0) { + message += ` (일부 파일 보안 검증 실패: ${result.securityFailures.length}개)` + } + return NextResponse.json({ success: true, - message: `리비전 ${result.revision}이 성공적으로 업로드되었습니다`, + message, data: { revisionId: result.revisionId, - issueStageId: issueStageId, // ✅ 추가 + issueStageId: result.issueStageId, stage: result.stage, revision: result.revision, usage: result.usage, usageType: result.usageType, - uploaderName: uploaderName, // ✅ 추가 + uploaderName: uploaderName, uploadedFiles: result.uploadedFiles, - filesCount: result.uploadedFiles.length + filesCount: result.uploadedFiles.length, + securityFailures: result.securityFailures, // 클라이언트에 보안 실패 정보 전달 }, }) } catch (e) { |
