summaryrefslogtreecommitdiff
path: root/app/api
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-04 08:31:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-04 08:31:31 +0000
commitb67e36df49f067cbd5ba899f9fbcc755f38d4b4f (patch)
tree5a71c5960f90d988cd509e3ef26bff497a277661 /app/api
parentb7f54b06c1ef9e619f5358fb0a5caad9703c8905 (diff)
(대표님, 최겸, 임수민) 작업사항 커밋
Diffstat (limited to 'app/api')
-rw-r--r--app/api/rfq-attachments/[id]/route.ts78
-rw-r--r--app/api/rfq-attachments/revision/route.ts122
-rw-r--r--app/api/rfq-attachments/upload/route.ts201
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