diff options
Diffstat (limited to 'app/api/bulk-upload')
| -rw-r--r-- | app/api/bulk-upload/route.ts | 312 |
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 |
