summaryrefslogtreecommitdiff
path: root/app/api/bulk-upload
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/bulk-upload')
-rw-r--r--app/api/bulk-upload/route.ts312
1 files changed, 312 insertions, 0 deletions
diff --git a/app/api/bulk-upload/route.ts b/app/api/bulk-upload/route.ts
new file mode 100644
index 00000000..f64fb192
--- /dev/null
+++ b/app/api/bulk-upload/route.ts
@@ -0,0 +1,312 @@
+// app/api/bulk-upload/route.ts
+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 db from '@/db/db'
+import { documents, issueStages, revisions, documentAttachments } from '@/db/schema/vendorDocu'
+import { and, eq } from 'drizzle-orm'
+import { revalidateTag } from 'next/cache'
+
+interface UploadItem {
+ documentId: number
+ stage: string
+ revision: string
+ fileName: string
+}
+
+interface ProcessResult {
+ success: boolean
+ documentId: number
+ stage: string
+ revision: string
+ fileName: string
+ error?: string
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const formData = await request.formData()
+
+ // FormData에서 메타데이터 추출
+ const uploaderName = formData.get("uploaderName") as string | null
+ const comment = formData.get("comment") as string | null
+ const projectType = formData.get("projectType") as string | null
+ const uploadDataStr = formData.get("uploadData") as string
+ const contractId = formData.get("contractId") as string | null
+
+ if (!uploadDataStr) {
+ return NextResponse.json(
+ { error: "업로드 데이터가 없습니다." },
+ { status: 400 }
+ )
+ }
+
+ const uploadData: UploadItem[] = JSON.parse(uploadDataStr)
+
+ if (!Array.isArray(uploadData) || uploadData.length === 0) {
+ return NextResponse.json(
+ { error: "업로드할 데이터가 없습니다." },
+ { status: 400 }
+ )
+ }
+
+ // 파일들 추출 (file_0, file_1, ...)
+ const files: File[] = []
+ for (let i = 0; i < uploadData.length; i++) {
+ const file = formData.get(`file_${i}`) as File | null
+ if (file) {
+ files.push(file)
+ }
+ }
+
+ if (files.length !== uploadData.length) {
+ return NextResponse.json(
+ { error: "업로드 데이터와 파일 수가 일치하지 않습니다." },
+ { status: 400 }
+ )
+ }
+
+ // 파일 크기 검증
+ const maxFileSize = 3 * 1024 * 1024 * 1024 // 3GB
+ for (const file of files) {
+ if (file.size > maxFileSize) {
+ return NextResponse.json(
+ { error: `파일 ${file.name}이 너무 큽니다. 최대 3GB까지 허용됩니다.` },
+ { status: 400 }
+ )
+ }
+ }
+
+ console.log("✅ 일괄 업로드 시작:", {
+ itemCount: uploadData.length,
+ fileCount: files.length,
+ uploaderName,
+ projectType
+ })
+
+ const results: ProcessResult[] = []
+ let successCount = 0
+ let errorCount = 0
+
+ // 각 파일을 개별적으로 처리 (부분 실패 허용)
+ for (let i = 0; i < uploadData.length; i++) {
+ const item = uploadData[i]
+ const file = files[i]
+
+ try {
+ // 개별 파일 업로드 처리
+ const result = await processIndividualUpload(item, file, uploaderName, comment)
+ results.push({
+ success: true,
+ documentId: item.documentId,
+ stage: item.stage,
+ revision: item.revision,
+ fileName: item.fileName,
+ })
+ successCount++
+
+ console.log(`✅ 파일 업로드 성공 [${i + 1}/${uploadData.length}]:`, item.fileName)
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error)
+ results.push({
+ success: false,
+ documentId: item.documentId,
+ stage: item.stage,
+ revision: item.revision,
+ fileName: item.fileName,
+ error: errorMessage,
+ })
+ errorCount++
+
+ console.error(`❌ 파일 업로드 실패 [${i + 1}/${uploadData.length}]:`, item.fileName, errorMessage)
+ }
+ }
+
+ const summary = {
+ total: uploadData.length,
+ success: successCount,
+ failed: errorCount,
+ results: results,
+ }
+
+ console.log("✅ 일괄 업로드 완료:", summary)
+
+ // 부분 성공도 성공으로 처리 (일부라도 업로드되었으면)
+ if (successCount > 0) {
+ if (contractId) {
+ revalidateTag(`enhanced-documents-${contractId}`)
+ console.log(`✅ 캐시 무효화 완료: enhanced-documents-${contractId}`)
+ }
+ return NextResponse.json({
+ success: true,
+ message: `일괄 업로드 완료: ${successCount}개 성공, ${errorCount}개 실패`,
+ data: {
+ uploadedCount: successCount,
+ failedCount: errorCount,
+ ...summary,
+ },
+ })
+ } else {
+ return NextResponse.json(
+ {
+ error: '모든 파일 업로드가 실패했습니다.',
+ details: summary,
+ },
+ { status: 500 }
+ )
+ }
+
+ } catch (error) {
+ console.error('❌ 일괄 업로드 전체 오류:', error)
+
+ return NextResponse.json(
+ {
+ error: 'Failed to process bulk upload',
+ details: error instanceof Error ? error.message : String(error),
+ },
+ { status: 500 }
+ )
+ }
+}
+
+// 개별 파일 업로드 처리 함수
+async function processIndividualUpload(
+ item: UploadItem,
+ file: File,
+ uploaderName: string | null,
+ comment: string | null
+) {
+ return await db.transaction(async (tx) => {
+ const { documentId, stage, revision, fileName } = item
+
+ // (1) 문서 존재 확인
+ const documentRecord = await tx
+ .select()
+ .from(documents)
+ .where(eq(documents.id, documentId))
+ .limit(1)
+
+ if (!documentRecord.length) {
+ throw new Error(`문서 ID ${documentId}를 찾을 수 없습니다.`)
+ }
+
+ // (2) issueStageId 찾기 또는 생성
+ let issueStageId: number
+ const stageRecord = await tx
+ .select()
+ .from(issueStages)
+ .where(and(eq(issueStages.stageName, stage), eq(issueStages.documentId, documentId)))
+ .limit(1)
+
+ if (!stageRecord.length) {
+ // Stage가 없으면 새로 생성
+ const [newStage] = await tx
+ .insert(issueStages)
+ .values({
+ documentId,
+ stageName: stage,
+ updatedAt: new Date(),
+ })
+ .returning()
+
+ issueStageId = newStage.id
+ console.log(`✅ 새 스테이지 생성: ${stage} (ID: ${issueStageId})`)
+ } else {
+ issueStageId = stageRecord[0].id
+ }
+
+ // (3) Revision 찾기 또는 생성
+ let revisionId: number
+ const revisionRecord = await tx
+ .select()
+ .from(revisions)
+ .where(and(eq(revisions.issueStageId, issueStageId), eq(revisions.revision, revision)))
+ .limit(1)
+
+ const currentDate = new Date().toISOString().split('T')[0] // YYYY-MM-DD 형식
+
+ if (!revisionRecord.length) {
+ // 리비전이 없으면 새로 생성 (일괄 업로드에서는 항상 새 리비전으로 처리)
+ const [newRevision] = await tx
+ .insert(revisions)
+ .values({
+ issueStageId,
+ revision,
+ uploaderType: "vendor", // 항상 vendor로 고정
+ uploaderName: uploaderName || undefined,
+ revisionStatus: "SUBMITTED", // 기본 상태
+ submittedDate: currentDate, // 제출일 설정
+ comment: comment || undefined,
+ updatedAt: new Date(),
+ })
+ .returning()
+
+ revisionId = newRevision.id
+ console.log(`✅ 새 리비전 생성: ${revision} (ID: ${revisionId}) for ${fileName}`)
+ } else {
+ // 기존 리비전에 파일 추가
+ revisionId = revisionRecord[0].id
+
+ // 기존 리비전 정보 업데이트 (코멘트나 업로더명 변경 가능)
+ await tx
+ .update(revisions)
+ .set({
+ uploaderName: uploaderName || revisionRecord[0].uploaderName,
+ comment: comment || revisionRecord[0].comment,
+ updatedAt: new Date(),
+ })
+ .where(eq(revisions.id, revisionId))
+
+ console.log(`✅ 기존 리비전에 파일 추가: ${revision} (ID: ${revisionId}) for ${fileName}`)
+ }
+
+ // (4) 파일 저장
+ if (file.size > 0) {
+ const originalName = file.name
+ const ext = path.extname(originalName)
+ const uniqueName = uuidv4() + ext
+ const baseDir = join(process.cwd(), "public", "documents")
+ const savePath = join(baseDir, uniqueName)
+
+ // 파일 저장
+ const arrayBuffer = await file.arrayBuffer()
+ const buffer = Buffer.from(arrayBuffer)
+ await writeFile(savePath, buffer)
+
+ // DB에 첨부파일 정보 저장
+ const [attachmentRecord] = await tx
+ .insert(documentAttachments)
+ .values({
+ revisionId,
+ fileName: originalName,
+ filePath: "/documents/" + uniqueName,
+ fileSize: file.size,
+ fileType: ext.replace('.', '').toLowerCase() || undefined,
+ updatedAt: new Date(),
+ })
+ .returning()
+
+ console.log(`✅ 파일 저장 완료: ${originalName} → ${uniqueName}`)
+ } else {
+ throw new Error('파일 크기가 0입니다.')
+ }
+
+ // (5) Documents 테이블의 updatedAt 갱신
+ await tx
+ .update(documents)
+ .set({ updatedAt: new Date() })
+ .where(eq(documents.id, documentId))
+
+ return {
+ documentId,
+ issueStageId,
+ revisionId,
+ stage,
+ revision,
+ fileName: file.name,
+ }
+ })
+} \ No newline at end of file