// app/api/stage-submissions/bulk-upload/route.ts import { NextRequest, NextResponse } from "next/server" import { getServerSession } from "next-auth/next" import { authOptions } from "@/app/api/auth/[...nextauth]/route" import db from "@/db/db" import { stageSubmissions, stageSubmissionAttachments, vendors } from "@/db/schema" import { eq, and } from "drizzle-orm" import { extractRevisionNumber, normalizeRevisionCode } from "@/lib/vendor-document-list/plant/upload/util/filie-parser" import { saveFileStream } from "@/lib/file-stroage" export async function POST(request: NextRequest) { const session = await getServerSession(authOptions) if (!session?.user?.companyId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) } const vendorId = session.user.companyId const userId = session.user.id const vendor = await db.query.vendors.findFirst({ where: eq(vendors.id, session.user.companyId), columns: { vendorName: true, vendorCode: true, } }) try { const formData = await request.formData() const files = formData.getAll('files') as File[] // 메타데이터 파싱 const metadata: any[] = [] let index = 0 while (formData.has(`metadata[${index}]`)) { const meta = formData.get(`metadata[${index}]`) if (meta) { metadata.push(JSON.parse(meta as string)) } index++ } if (files.length !== metadata.length) { return NextResponse.json( { error: "Files and metadata count mismatch" }, { status: 400 } ) } // 총 용량 체크 (10GB) const totalSize = files.reduce((acc, file) => acc + file.size, 0) const maxSize = 10 * 1024 * 1024 * 1024 // 10GB if (totalSize > maxSize) { return NextResponse.json( { error: `Total file size exceeds 10GB limit` }, { status: 400 } ) } const uploadResults = [] // 각 파일 처리 for (let i = 0; i < files.length; i++) { const file = files[i] const meta = metadata[i] try { await db.transaction(async (tx) => { // 리비전 정보 추출 const revisionNumber = extractRevisionNumber(meta.revision) const revisionCode = normalizeRevisionCode(meta.revision) // ⭐ 추가 // 1. 해당 스테이지의 최신 submission 찾기 let submission = await tx .select() .from(stageSubmissions) .where( and( eq(stageSubmissions.stageId, meta.stageId), eq(stageSubmissions.documentId, meta.documentId) ) ) .orderBy(stageSubmissions.revisionNumber) .limit(1) if (!submission[0] || submission[0].revisionNumber < revisionNumber) { // 새 submission 생성 const [newSubmission] = await tx .insert(stageSubmissions) .values({ stageId: meta.stageId, documentId: meta.documentId, revisionNumber, revisionCode, // ⭐ 원본 리비전 코드 저장 revisionType: revisionNumber === 0 ? "INITIAL" : "RESUBMISSION", submissionStatus: "SUBMITTED", submittedBy: session.user.name || session.user.email || "Unknown", submittedByEmail: session.user.email, vendorId, vendorCode: vendor?.vendorCode || null, totalFiles: 1, totalFileSize: file.size, submissionTitle: `Revision ${revisionCode} Submission`, // ⭐ 코드 사용 syncStatus: "pending", lastModifiedBy: "EVCP", }) .returning() submission = [newSubmission] } else if (submission[0].revisionNumber === revisionNumber) { // 같은 리비전 업데이트 await tx .update(stageSubmissions) .set({ revisionCode, // ⭐ 코드 업데이트 totalFiles: submission[0].totalFiles + 1, totalFileSize: submission[0].totalFileSize + file.size, updatedAt: new Date(), }) .where(eq(stageSubmissions.id, submission[0].id)) } // 2. 파일 저장 (대용량 파일은 스트리밍) const directory = `submissions/${meta.documentId}/${meta.stageId}/${revisionCode}` // ⭐ 디렉토리에 리비전 코드 포함 let saveResult if (file.size > 100 * 1024 * 1024) { // 100MB 이상은 스트리밍 saveResult = await saveFileStream({ file, directory, originalName: meta.originalName, userId: userId.toString() }) } else { const { saveFile } = await import("@/lib/file-stroage") saveResult = await saveFile({ file, directory, originalName: meta.originalName, userId: userId.toString() }) } if (!saveResult.success) { throw new Error(saveResult.error || "File save failed") } // 3. attachment 레코드 생성 await tx.insert(stageSubmissionAttachments).values({ submissionId: submission[0].id, fileName: saveResult.fileName!, originalFileName: meta.originalName, fileType: file.type, fileExtension: meta.originalName.split('.').pop(), fileSize: file.size, storageType: "LOCAL", storagePath: saveResult.filePath!, storageUrl: saveResult.publicPath!, mimeType: file.type, uploadedBy: session.user.name || session.user.email || "Unknown", status: "ACTIVE", syncStatus: "pending", lastModifiedBy: "EVCP", }) }) uploadResults.push({ fileName: meta.originalName, success: true }) } catch (error) { console.error(`Failed to upload ${meta.originalName}:`, error) uploadResults.push({ fileName: meta.originalName, success: false, error: error instanceof Error ? error.message : "Upload failed" }) } } const successCount = uploadResults.filter(r => r.success).length return NextResponse.json({ success: true, uploaded: successCount, failed: uploadResults.length - successCount, results: uploadResults }) } catch (error) { console.error("Bulk upload error:", error) return NextResponse.json( { error: "Bulk upload failed" }, { status: 500 } ) } }