diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-22 21:10:24 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-22 21:10:24 +0900 |
| commit | 6b2a561265fb649398e1770f720365ee10f542e9 (patch) | |
| tree | 50ae6453939d8ce4be850d603450d03d31e05442 /app | |
| parent | 2ecf88af270c5d044a853793f72f3a4536e05b89 (diff) | |
(김준회) SWP 문서 업로드
Diffstat (limited to 'app')
| -rw-r--r-- | app/api/stage-submissions/upload/route.ts | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/app/api/stage-submissions/upload/route.ts b/app/api/stage-submissions/upload/route.ts new file mode 100644 index 00000000..b2771005 --- /dev/null +++ b/app/api/stage-submissions/upload/route.ts @@ -0,0 +1,230 @@ +// 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 } + ) + } +} + |
