// app/api/stage-submissions/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" /** * 단일 submission에 여러 파일을 업로드하는 API * single-upload-dialog.tsx에서 사용 */ 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[] if (files.length === 0) { return NextResponse.json( { error: "No files provided" }, { status: 400 } ) } // 메타데이터 추출 const documentId = parseInt(formData.get('documentId') as string) const stageId = parseInt(formData.get('stageId') as string) const revision = formData.get('revision') as string const description = formData.get('description') as string || "" if (!documentId || !stageId || !revision) { return NextResponse.json( { error: "Missing required fields: documentId, stageId, or revision" }, { status: 400 } ) } // 총 용량 체크 (5GB per submission) const totalSize = files.reduce((acc, file) => acc + file.size, 0) const maxSize = 5 * 1024 * 1024 * 1024 // 5GB if (totalSize > maxSize) { return NextResponse.json( { error: `Total file size exceeds 5GB limit` }, { status: 400 } ) } // 리비전 정보 추출 const revisionNumber = extractRevisionNumber(revision) const revisionCode = normalizeRevisionCode(revision) const uploadResults: Array<{ fileName: string success: boolean error?: string }> = [] let submissionId: number | null = null // 트랜잭션으로 처리 await db.transaction(async (tx) => { // 1. 해당 스테이지의 기존 submission 찾기 const existingSubmissions = await tx .select() .from(stageSubmissions) .where( and( eq(stageSubmissions.stageId, stageId), eq(stageSubmissions.documentId, documentId), eq(stageSubmissions.revisionNumber, revisionNumber) ) ) .limit(1) let submission if (existingSubmissions.length > 0) { // 기존 submission 업데이트 submission = existingSubmissions[0] await tx .update(stageSubmissions) .set({ totalFiles: (submission.totalFiles || 0) + files.length, totalFileSize: (submission.totalFileSize || 0) + totalSize, updatedAt: new Date(), lastModifiedBy: "EVCP", }) .where(eq(stageSubmissions.id, submission.id)) submissionId = submission.id } else { // 새 submission 생성 const [newSubmission] = await tx .insert(stageSubmissions) .values({ stageId, documentId, revisionNumber, revisionCode, revisionType: revisionNumber === 0 ? "INITIAL" : "RESUBMISSION", submissionStatus: "SUBMITTED", submittedBy: session.user.name || session.user.email || "Unknown", submittedByEmail: session.user.email || null, vendorId, vendorCode: vendor?.vendorCode || null, totalFiles: files.length, totalFileSize: totalSize, submissionTitle: description || `Revision ${revisionCode} Submission`, submissionNotes: description || null, syncStatus: "pending", lastModifiedBy: "EVCP", }) .returning() submission = newSubmission submissionId = newSubmission.id } // 2. 각 파일 저장 및 attachment 레코드 생성 const directory = `submissions/${documentId}/${stageId}/${revisionCode}` for (const file of files) { try { // 파일 저장 (대용량 파일은 스트리밍) let saveResult if (file.size > 100 * 1024 * 1024) { // 100MB 이상은 스트리밍 saveResult = await saveFileStream({ file, directory, originalName: file.name, userId: userId.toString() }) } else { const { saveFile } = await import("@/lib/file-stroage") saveResult = await saveFile({ file, directory, originalName: file.name, userId: userId.toString() }) } if (!saveResult.success) { throw new Error(saveResult.error || "File save failed") } // attachment 레코드 생성 const fileExtension = file.name.split('.').pop() || '' await tx.insert(stageSubmissionAttachments).values({ submissionId: submissionId!, fileName: saveResult.fileName!, originalFileName: file.name, fileType: file.type, fileExtension: fileExtension, 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: file.name, success: true }) } catch (error) { console.error(`Failed to upload ${file.name}:`, error) uploadResults.push({ fileName: file.name, success: false, error: error instanceof Error ? error.message : "Upload failed" }) throw error // 트랜잭션 롤백 } } }) const successCount = uploadResults.filter(r => r.success).length return NextResponse.json({ success: true, submissionId, uploaded: successCount, failed: uploadResults.length - successCount, results: uploadResults, message: `Successfully uploaded ${successCount} file(s)` }) } catch (error) { console.error("Upload error:", error) return NextResponse.json( { error: "Upload failed", details: error instanceof Error ? error.message : "Unknown error" }, { status: 500 } ) } }