// 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, } }) }