summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-22 21:10:24 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-22 21:10:24 +0900
commit6b2a561265fb649398e1770f720365ee10f542e9 (patch)
tree50ae6453939d8ce4be850d603450d03d31e05442 /app
parent2ecf88af270c5d044a853793f72f3a4536e05b89 (diff)
(김준회) SWP 문서 업로드
Diffstat (limited to 'app')
-rw-r--r--app/api/stage-submissions/upload/route.ts230
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 }
+ )
+ }
+}
+