diff options
Diffstat (limited to 'app/api/rfq-attachments/upload')
| -rw-r--r-- | app/api/rfq-attachments/upload/route.ts | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/app/api/rfq-attachments/upload/route.ts b/app/api/rfq-attachments/upload/route.ts new file mode 100644 index 00000000..3343c905 --- /dev/null +++ b/app/api/rfq-attachments/upload/route.ts @@ -0,0 +1,201 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import db from "@/db/db"; +import { eq, and } from "drizzle-orm"; +import { rfqLastAttachments, rfqLastAttachmentRevisions } from "@/db/schema"; +import { saveFile } from "@/lib/file-stroage"; + +// 시리얼 번호 생성 함수 +async function generateSerialNo(rfqId: number, attachmentType: string, index: number = 0): Promise<string> { + const prefix = attachmentType === "설계" ? "DES" : "PUR"; + + const existingAttachments = await db + .select({ id: rfqLastAttachments.id }) + .from(rfqLastAttachments) + .where( + and( + eq(rfqLastAttachments.rfqId, rfqId), + eq(rfqLastAttachments.attachmentType, attachmentType as "설계" | "구매") + ) + ); + + const nextNumber = existingAttachments.length + 1 + index; + const paddedNumber = String(nextNumber).padStart(4, "0"); + + return `${prefix}-${rfqId}-${paddedNumber}`; +} + +export async function POST(request: NextRequest) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, message: "인증이 필요합니다" }, + { status: 401 } + ); + } + + const formData = await request.formData(); + const rfqId = parseInt(formData.get("rfqId") as string); + const attachmentType = formData.get("attachmentType") as "구매" | "설계"; + const description = formData.get("description") as string; + const files = formData.getAll("files") as File[]; + + // 파일 유효성 검증 + if (!files || files.length === 0) { + return NextResponse.json( + { success: false, message: "파일이 없습니다" }, + { status: 400 } + ); + } + + // 최대 파일 개수 검증 + const MAX_FILES = 10; + if (files.length > MAX_FILES) { + return NextResponse.json( + { success: false, message: `최대 ${MAX_FILES}개까지 업로드 가능합니다` }, + { status: 400 } + ); + } + + // 각 파일 크기 검증 (100MB) + const MAX_FILE_SIZE = 100 * 1024 * 1024; + const oversizedFiles = files.filter(file => file.size > MAX_FILE_SIZE); + if (oversizedFiles.length > 0) { + return NextResponse.json( + { + success: false, + message: `다음 파일들이 100MB를 초과합니다: ${oversizedFiles.map(f => f.name).join(", ")}` + }, + { status: 400 } + ); + } + + // 업로드 결과 저장 + const uploadedAttachments = []; + const failedUploads = []; + + // 각 파일에 대해 트랜잭션 처리 + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + try { + const result = await db.transaction(async (tx) => { + // 1. 시리얼 번호 생성 (인덱스 전달) + const serialNo = await generateSerialNo(rfqId, attachmentType, i); + + // 2. 파일 저장 + const saveResult = await saveFile({ + file, + directory: `uploads/rfq-attachments/rfq-${rfqId}`, + originalName: file.name, + userId: session.user.id, + }); + + if (!saveResult.success) { + throw new Error(saveResult.error || `파일 저장 실패: ${file.name}`); + } + + // 3. 첨부파일 레코드 생성 + const [attachment] = await tx + .insert(rfqLastAttachments) + .values({ + rfqId, + attachmentType, + serialNo, + description: description || null, + currentRevision: "A", + createdBy: parseInt(session.user.id), + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning(); + + // 4. 리비전 레코드 생성 + const [revision] = await tx + .insert(rfqLastAttachmentRevisions) + .values({ + attachmentId: attachment.id, + revisionNo: "A", + fileName: saveResult.fileName!, + originalFileName: saveResult.originalName!, + filePath: saveResult.publicPath!, + fileSize: saveResult.fileSize!, + fileType: file.type || "application/octet-stream", + isLatest: true, + revisionComment: "초기 업로드", + createdBy: parseInt(session.user.id), + createdAt: new Date(), + }) + .returning(); + + // 5. 첨부파일의 latestRevisionId 업데이트 + await tx + .update(rfqLastAttachments) + .set({ + latestRevisionId: revision.id + }) + .where(eq(rfqLastAttachments.id, attachment.id)); + + return { + attachment, + revision, + fileName: file.name, + serialNo + }; + }); + + uploadedAttachments.push(result); + } catch (error) { + console.error(`Upload error for file ${file.name}:`, error); + failedUploads.push({ + fileName: file.name, + error: error instanceof Error ? error.message : "알 수 없는 오류" + }); + } + } + + // 결과 반환 + if (uploadedAttachments.length === 0) { + return NextResponse.json( + { + success: false, + message: "모든 파일 업로드가 실패했습니다", + failedUploads + }, + { status: 500 } + ); + } + + // 부분 성공 또는 완전 성공 + const isPartialSuccess = failedUploads.length > 0; + const message = isPartialSuccess + ? `${uploadedAttachments.length}개 파일 업로드 성공, ${failedUploads.length}개 실패` + : `${uploadedAttachments.length}개 파일이 성공적으로 업로드되었습니다`; + + return NextResponse.json({ + success: true, + message, + uploadedCount: uploadedAttachments.length, + data: { + uploaded: uploadedAttachments.map(item => ({ + id: item.attachment.id, + serialNo: item.serialNo, + fileName: item.fileName + })), + failed: failedUploads + } + }); + + } catch (error) { + console.error("Upload error:", error); + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : "파일 업로드 실패" + }, + { status: 500 } + ); + } +}
\ No newline at end of file |
