summaryrefslogtreecommitdiff
path: root/app/api/revision-attachment
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/revision-attachment')
-rw-r--r--app/api/revision-attachment/route.ts102
1 files changed, 70 insertions, 32 deletions
diff --git a/app/api/revision-attachment/route.ts b/app/api/revision-attachment/route.ts
index 092eed8d..46c2e9c9 100644
--- a/app/api/revision-attachment/route.ts
+++ b/app/api/revision-attachment/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 { eq } from "drizzle-orm"
+/* 보안 강화된 파일 저장 유틸리티 */
+import { saveFile, SaveFileResult, saveFileStream } from "@/lib/file-stroage"
+
/* change log 유틸 */
import {
logAttachmentChange,
@@ -35,13 +34,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)
+ // 기본 파일 크기 검증 (보안 함수에서도 검증하지만 조기 체크)
+ const MAX = 1024 * 1024 * 1024 // 1GB로 증가 (첫 번째 파일과 동일)
+ for (const f of attachmentFiles) {
+ if (f.size > MAX) {
return NextResponse.json(
- { error: `${f.name} exceeds 50MB limit` },
+ { error: `${f.name} exceeds 1GB limit` },
{ status: 400 }
)
+ }
+ }
/* ------- 리비전 및 계약 정보 확보 ------- */
const [revisionInfo] = await db
@@ -52,6 +54,7 @@ export async function POST(request: NextRequest) {
usageType: revisions.usageType,
issueStageId: revisions.issueStageId,
projectId: documents.projectId,
+ vendorId: documents.vendorId,
})
.from(revisions)
.innerJoin(issueStages, eq(revisions.issueStageId, issueStages.id))
@@ -63,41 +66,60 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Revision not found" }, { status: 404 })
}
+ // vendorId가 null인 경우 처리
+ if (!revisionInfo.vendorId) {
+ return NextResponse.json({
+ error: "Revision must have a valid vendor ID for synchronization"
+ }, { status: 400 })
+ }
+
/* ------- 트랜잭션 ------- */
const result = await db.transaction(async (tx) => {
- /* 첨부파일 처리 */
+ /* ------- 보안 강화된 첨부파일 처리 ------- */
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)
-
- await writeFile(dest, Buffer.from(await file.arrayBuffer()))
-
+ console.log(`🔐 보안 검증 시작: ${file.name}`)
+
+ // 보안 강화된 파일 저장
+ const saveResult = file.size > 100 * 1024 * 1024
+ ? await saveFileStream({ file, directory: "documents", userId: uploaderName || "" })
+ : await saveFile({ file, directory: "documents", userId: uploaderName || "" })
+
+ if (!saveResult.success) {
+ console.error(`❌ 파일 보안 검증 실패: ${file.name} - ${saveResult.error}`)
+ securityFailures.push(`${file.name}: ${saveResult.error}`)
+ continue // 실패한 파일은 건너뛰고 계속 진행
+ }
+
+ 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
await logAttachmentChange(
- revisionInfo.projectId,
+ revisionInfo.vendorId!,
att.id,
"CREATE",
att,
@@ -108,6 +130,16 @@ export async function POST(request: NextRequest) {
)
}
+ // 보안 검증 실패한 파일이 있으면 경고 반환
+ if (securityFailures.length > 0) {
+ console.warn(`⚠️ 일부 파일의 보안 검증 실패:`, securityFailures)
+
+ // 모든 파일이 실패한 경우 에러 반환
+ if (uploadedFiles.length === 0) {
+ throw new Error(`모든 파일의 보안 검증이 실패했습니다: ${securityFailures.join(', ')}`)
+ }
+ }
+
/* 리비전 updatedAt 업데이트 */
await tx.update(revisions)
.set({ updatedAt: new Date() })
@@ -119,30 +151,36 @@ export async function POST(request: NextRequest) {
usage: revisionInfo.usage,
usageType: revisionInfo.usageType,
uploadedFiles,
- projectId: revisionInfo.projectId
+ vendorId: revisionInfo.vendorId,
+ securityFailures // 보안 실패 정보 포함
}
})
// 캐시 무효화
try {
- // revalidateTag(`enhanced-documents-${result.projectId}`)
- revalidateTag(`sync-status-${result.projectId}`)
-
- console.log(`✅ Cache invalidated for contract ${result.projectId}`)
+ revalidateTag(`sync-status-${result.vendorId}`)
+ console.log(`✅ Cache invalidated for contract ${result.vendorId}`)
} catch (cacheError) {
console.warn('⚠️ Cache invalidation failed:', cacheError)
}
+ // 응답 메시지 구성
+ let message = `${result.uploadedFiles.length}개 첨부파일이 추가되었습니다`
+ if (result.securityFailures.length > 0) {
+ message += ` (일부 파일 보안 검증 실패: ${result.securityFailures.length}개)`
+ }
+
return NextResponse.json({
success: true,
- message: `${result.uploadedFiles.length}개 첨부파일이 추가되었습니다`,
+ message,
data: {
revisionId: result.revisionId,
revision: result.revision,
usage: result.usage,
usageType: result.usageType,
uploadedFiles: result.uploadedFiles,
- filesCount: result.uploadedFiles.length
+ filesCount: result.uploadedFiles.length,
+ securityFailures: result.securityFailures, // 클라이언트에 보안 실패 정보 전달
},
})
} catch (e) {