diff options
Diffstat (limited to 'app/api')
| -rw-r--r-- | app/api/rfq-attachments/[id]/route.ts | 78 | ||||
| -rw-r--r-- | app/api/rfq-attachments/revision/route.ts | 122 | ||||
| -rw-r--r-- | app/api/rfq-attachments/upload/route.ts | 201 |
3 files changed, 401 insertions, 0 deletions
diff --git a/app/api/rfq-attachments/[id]/route.ts b/app/api/rfq-attachments/[id]/route.ts new file mode 100644 index 00000000..df99c1ad --- /dev/null +++ b/app/api/rfq-attachments/[id]/route.ts @@ -0,0 +1,78 @@ +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 } from "drizzle-orm"; +import { rfqLastAttachments, rfqLastAttachmentRevisions } from "@/db/schema"; +import { deleteFile } from "@/lib/file-storage"; + +export async function DELETE( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, message: "인증이 필요합니다" }, + { status: 401 } + ); + } + + const attachmentId = parseInt(params.id); + + // 트랜잭션 시작 + const result = await db.transaction(async (tx) => { + // 1. 첨부파일 정보 조회 + const [attachment] = await tx + .select() + .from(rfqLastAttachments) + .where(eq(rfqLastAttachments.id, attachmentId)); + + if (!attachment) { + throw new Error("첨부파일을 찾을 수 없습니다"); + } + + // 2. 모든 리비전 조회 + const revisions = await tx + .select() + .from(rfqLastAttachmentRevisions) + .where(eq(rfqLastAttachmentRevisions.attachmentId, attachmentId)); + + // 3. 모든 리비전 파일 삭제 (공용 삭제 함수 사용) + for (const revision of revisions) { + if (revision.filePath) { + await deleteFile(revision.filePath); + } + } + + // 4. 리비전 레코드 삭제 + await tx + .delete(rfqLastAttachmentRevisions) + .where(eq(rfqLastAttachmentRevisions.attachmentId, attachmentId)); + + // 5. 첨부파일 레코드 삭제 + await tx + .delete(rfqLastAttachments) + .where(eq(rfqLastAttachments.id, attachmentId)); + + return attachment; + }); + + return NextResponse.json({ + success: true, + message: "파일이 삭제되었습니다", + data: result, + }); + + } catch (error) { + console.error("Delete attachment error:", error); + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : "파일 삭제 실패" + }, + { status: 500 } + ); + } +}
\ No newline at end of file diff --git a/app/api/rfq-attachments/revision/route.ts b/app/api/rfq-attachments/revision/route.ts new file mode 100644 index 00000000..2592ae78 --- /dev/null +++ b/app/api/rfq-attachments/revision/route.ts @@ -0,0 +1,122 @@ +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 } from "drizzle-orm"; +import { rfqLastAttachments, rfqLastAttachmentRevisions } from "@/db/schema"; +import { saveFile, deleteFile } from "@/lib/file-storage"; + +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 attachmentId = parseInt(formData.get("attachmentId") as string); + const revisionComment = formData.get("revisionComment") as string; + const file = formData.get("file") as File; + + if (!file || file.size === 0) { + return NextResponse.json( + { success: false, message: "파일이 없습니다" }, + { status: 400 } + ); + } + + // 파일 크기 검증 (100MB) + if (file.size > 100 * 1024 * 1024) { + return NextResponse.json( + { success: false, message: "파일 크기는 100MB를 초과할 수 없습니다" }, + { status: 400 } + ); + } + + // 트랜잭션 시작 + const result = await db.transaction(async (tx) => { + // 1. 기존 첨부파일 정보 조회 + const [existingAttachment] = await tx + .select() + .from(rfqLastAttachments) + .where(eq(rfqLastAttachments.id, attachmentId)); + + if (!existingAttachment) { + throw new Error("첨부파일을 찾을 수 없습니다"); + } + + // 2. 현재 리비전 번호 계산 + const currentRevision = existingAttachment.currentRevision || "A"; + const nextRevision = String.fromCharCode(currentRevision.charCodeAt(0) + 1); + + // 3. 공용 파일 저장 함수 사용 + const saveResult = await saveFile({ + file, + directory: `uploads/rfq-attachments/rfq-${existingAttachment.rfqId}`, + originalName: file.name, + userId: session.user.id, + }); + + if (!saveResult.success) { + throw new Error(saveResult.error || "파일 저장 실패"); + } + + // 4. 기존 latest 플래그 해제 + if (existingAttachment.latestRevisionId) { + await tx + .update(rfqLastAttachmentRevisions) + .set({ isLatest: false }) + .where(eq(rfqLastAttachmentRevisions.id, existingAttachment.latestRevisionId)); + } + + // 5. 새 리비전 생성 + const [revision] = await tx + .insert(rfqLastAttachmentRevisions) + .values({ + attachmentId, + revisionNo: nextRevision, + fileName: saveResult.fileName!, + originalFileName: saveResult.originalName!, + filePath: saveResult.publicPath!, + fileSize: saveResult.fileSize!, + fileType: file.type || "unknown", + isLatest: true, + revisionComment, + uploadedBy: parseInt(session.user.id), + uploadedAt: new Date(), + }) + .returning(); + + // 6. 첨부파일 정보 업데이트 + await tx + .update(rfqLastAttachments) + .set({ + currentRevision: nextRevision, + latestRevisionId: revision.id, + updatedAt: new Date(), + }) + .where(eq(rfqLastAttachments.id, attachmentId)); + + return { attachment: existingAttachment, revision }; + }); + + return NextResponse.json({ + success: true, + message: `새 버전이 업로드되었습니다 (Rev. ${result.revision.revisionNo})`, + data: result.revision, + }); + + } catch (error) { + console.error("Update revision error:", error); + return NextResponse.json( + { + success: false, + message: error instanceof Error ? error.message : "리비전 업데이트 실패" + }, + { status: 500 } + ); + } +}
\ No newline at end of file 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 |
