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, sql } 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 { const prefix = attachmentType === "설계" ? "DES" : "PUR"; // 데이터베이스에서 최대 시리얼 번호 찾기 const maxSerialResult = await db .select({ serialNo: rfqLastAttachments.serialNo }) .from(rfqLastAttachments) .where( and( eq(rfqLastAttachments.rfqId, rfqId), eq(rfqLastAttachments.attachmentType, attachmentType as "설계" | "구매") ) ) .orderBy(sql`CAST(SUBSTRING(${rfqLastAttachments.serialNo} FROM '[^-]+$') AS INTEGER) DESC`) .limit(1); let nextNumber = 1; if (maxSerialResult.length > 0) { const lastSerialNo = maxSerialResult[0].serialNo; const lastNumber = parseInt(lastSerialNo.split('-').pop() || '0'); nextNumber = lastNumber + 1; } 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 } ); } }