diff options
Diffstat (limited to 'app/api')
| -rw-r--r-- | app/api/evaluation/attachments/[id]/route.ts | 151 | ||||
| -rw-r--r-- | app/api/evaluation/attachments/route.ts | 201 | ||||
| -rw-r--r-- | app/api/vendors/route.ts | 7 |
3 files changed, 358 insertions, 1 deletions
diff --git a/app/api/evaluation/attachments/[id]/route.ts b/app/api/evaluation/attachments/[id]/route.ts new file mode 100644 index 00000000..dfdc0eb9 --- /dev/null +++ b/app/api/evaluation/attachments/[id]/route.ts @@ -0,0 +1,151 @@ +// app/api/evaluation/attachments/[id]/route.ts +import { NextRequest, NextResponse } from "next/server"; +import db from "@/db/db"; +import { reviewerEvaluationAttachments } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { deleteFile } from "@/lib/file-stroage"; + +interface Context { + params: { + id: string; + }; +} + +// 첨부파일 삭제 +export async function DELETE( + request: NextRequest, + { params }: Context +) { + try { + // 인증 확인 + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: "인증이 필요합니다." }, + { status: 401 } + ); + } + + const attachmentId = parseInt(params.id); + if (isNaN(attachmentId)) { + return NextResponse.json( + { success: false, error: "유효하지 않은 첨부파일 ID입니다." }, + { status: 400 } + ); + } + + const result = await db.transaction(async (tx) => { + // 1. 첨부파일 정보 조회 + const attachment = await tx + .select({ + id: reviewerEvaluationAttachments.id, + publicPath: reviewerEvaluationAttachments.publicPath, + uploadedBy: reviewerEvaluationAttachments.uploadedBy, + }) + .from(reviewerEvaluationAttachments) + .where(eq(reviewerEvaluationAttachments.id, attachmentId)) + .limit(1); + + if (attachment.length === 0) { + throw new Error("첨부파일을 찾을 수 없습니다."); + } + + // 2. 권한 확인 (업로드한 사용자만 삭제 가능) + if (attachment[0].uploadedBy !== parseInt(session.user.id)) { + throw new Error("삭제 권한이 없습니다."); + } + + // 3. DB에서 첨부파일 정보 삭제 + await tx + .delete(reviewerEvaluationAttachments) + .where(eq(reviewerEvaluationAttachments.id, attachmentId)); + + // 4. 물리적 파일 삭제 + try { + await deleteFile(attachment[0].publicPath); + } catch (fileError) { + console.warn("물리적 파일 삭제 실패:", fileError); + // 파일 삭제 실패는 DB 삭제를 롤백하지 않음 + } + + return true; + }); + + return NextResponse.json({ + success: true, + message: "첨부파일이 삭제되었습니다.", + }); + + } catch (error) { + console.error("첨부파일 삭제 실패:", error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : "첨부파일 삭제 중 오류가 발생했습니다.", + }, + { status: 500 } + ); + } +} + +// 특정 첨부파일 정보 조회 +export async function GET( + request: NextRequest, + { params }: Context +) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: "인증이 필요합니다." }, + { status: 401 } + ); + } + + const attachmentId = parseInt(params.id); + if (isNaN(attachmentId)) { + return NextResponse.json( + { success: false, error: "유효하지 않은 첨부파일 ID입니다." }, + { status: 400 } + ); + } + + const attachment = await db + .select({ + id: reviewerEvaluationAttachments.id, + originalFileName: reviewerEvaluationAttachments.originalFileName, + publicPath: reviewerEvaluationAttachments.publicPath, + fileSize: reviewerEvaluationAttachments.fileSize, + mimeType: reviewerEvaluationAttachments.mimeType, + description: reviewerEvaluationAttachments.description, + createdAt: reviewerEvaluationAttachments.createdAt, + }) + .from(reviewerEvaluationAttachments) + .where(eq(reviewerEvaluationAttachments.id, attachmentId)) + .limit(1); + + if (attachment.length === 0) { + return NextResponse.json( + { success: false, error: "첨부파일을 찾을 수 없습니다." }, + { status: 404 } + ); + } + + return NextResponse.json({ + success: true, + attachment: attachment[0], + }); + + } catch (error) { + console.error("첨부파일 조회 실패:", error); + return NextResponse.json( + { + success: false, + error: "첨부파일 조회 중 오류가 발생했습니다.", + }, + { status: 500 } + ); + } +}
\ No newline at end of file diff --git a/app/api/evaluation/attachments/route.ts b/app/api/evaluation/attachments/route.ts new file mode 100644 index 00000000..c856832b --- /dev/null +++ b/app/api/evaluation/attachments/route.ts @@ -0,0 +1,201 @@ +// app/api/evaluation/attachments/route.ts +import { NextRequest, NextResponse } from "next/server"; +import db from "@/db/db"; +import { reviewerEvaluationAttachments, reviewerEvaluationDetails, reviewerEvaluations } from "@/db/schema"; +import { eq, and } from "drizzle-orm"; +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { saveFile } from "@/lib/file-stroage"; + +// 파일 업로드 +export async function POST(request: NextRequest) { + try { + // 인증 확인 + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: "인증이 필요합니다." }, + { status: 401 } + ); + } + + const formData = await request.formData(); + const file = formData.get("file") as File; + const questionId = formData.get("questionId") as string; + const evaluationId = formData.get("evaluationId") as string; + const description = formData.get("description") as string | null; + + if (!file || !questionId || !evaluationId) { + return NextResponse.json( + { success: false, error: "필수 파라미터가 누락되었습니다." }, + { status: 400 } + ); + } + + // 파일 크기 제한 (10MB) + const maxSize = 10 * 1024 * 1024; + if (file.size > maxSize) { + return NextResponse.json( + { success: false, error: "파일 크기는 10MB를 초과할 수 없습니다." }, + { status: 400 } + ); + } + + // 파일 타입 제한 + const allowedTypes = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.hancom.hwp', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'image/jpeg', + 'image/png', + 'image/gif' + ]; + + if (!allowedTypes.includes(file.type)) { + return NextResponse.json( + { success: false, error: "지원하지 않는 파일 형식입니다." }, + { status: 400 } + ); + } + + const result = await db.transaction(async (tx) => { + // 1. 해당 평가 항목에 대한 reviewerEvaluationDetailId 찾기 + const evaluationDetail = await tx + .select({ + id: reviewerEvaluationDetails.id, + reviewerEvaluationId: reviewerEvaluationDetails.reviewerEvaluationId, + }) + .from(reviewerEvaluationDetails) + .innerJoin( + reviewerEvaluations, + eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluations.id) + ) + .where( + and( + eq(reviewerEvaluations.id, parseInt(evaluationId)), + // questionId는 실제로는 criteriaId를 의미 + // 여기서는 regEvalCriteriaDetailsId를 통해 연결해야 함 + ) + ) + .limit(1); + + if (evaluationDetail.length === 0) { + throw new Error("평가 세부사항을 찾을 수 없습니다."); + } + + // 2. 파일 저장 + const fileResult = await saveFile({ + file, + directory: "evaluation-attachments", + originalName: file.name, + }); + + if (!fileResult.success) { + throw new Error(fileResult.error || "파일 저장에 실패했습니다."); + } + + // 3. DB에 첨부파일 정보 저장 + const [attachment] = await tx + .insert(reviewerEvaluationAttachments) + .values({ + reviewerEvaluationDetailId: evaluationDetail[0].id, + originalFileName: file.name, + storedFileName: fileResult.fileName!, + filePath: fileResult.filePath!, + publicPath: fileResult.publicPath!, + fileSize: file.size, + mimeType: file.type, + fileExtension: file.name.split('.').pop()?.toLowerCase() || '', + description: description || null, + uploadedBy: parseInt(session.user.id), + }) + .returning({ + id: reviewerEvaluationAttachments.id, + originalFileName: reviewerEvaluationAttachments.originalFileName, + publicPath: reviewerEvaluationAttachments.publicPath, + fileSize: reviewerEvaluationAttachments.fileSize, + description: reviewerEvaluationAttachments.description, + createdAt: reviewerEvaluationAttachments.createdAt, + }); + + return attachment; + }); + + return NextResponse.json({ + success: true, + attachment: result, + }); + + } catch (error) { + console.error("파일 업로드 실패:", error); + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다.", + }, + { status: 500 } + ); + } +} + +// 특정 질문의 첨부파일 목록 조회 +export async function GET(request: NextRequest) { + try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: "인증이 필요합니다." }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const questionId = searchParams.get("questionId"); + const evaluationId = searchParams.get("evaluationId"); + + if (!questionId || !evaluationId) { + return NextResponse.json( + { success: false, error: "필수 파라미터가 누락되었습니다." }, + { status: 400 } + ); + } + + const attachments = await db + .select({ + id: reviewerEvaluationAttachments.id, + originalFileName: reviewerEvaluationAttachments.originalFileName, + publicPath: reviewerEvaluationAttachments.publicPath, + fileSize: reviewerEvaluationAttachments.fileSize, + description: reviewerEvaluationAttachments.description, + createdAt: reviewerEvaluationAttachments.createdAt, + }) + .from(reviewerEvaluationAttachments) + .innerJoin( + reviewerEvaluationDetails, + eq(reviewerEvaluationAttachments.reviewerEvaluationDetailId, reviewerEvaluationDetails.id) + ) + .innerJoin( + reviewerEvaluations, + eq(reviewerEvaluationDetails.reviewerEvaluationId, reviewerEvaluations.id) + ) + .where(eq(reviewerEvaluations.id, parseInt(evaluationId))); + + return NextResponse.json({ + success: true, + attachments, + }); + + } catch (error) { + console.error("첨부파일 조회 실패:", error); + return NextResponse.json( + { + success: false, + error: "첨부파일 조회 중 오류가 발생했습니다.", + }, + { status: 500 } + ); + } +}
\ No newline at end of file diff --git a/app/api/vendors/route.ts b/app/api/vendors/route.ts index 7c7dbb84..760f183e 100644 --- a/app/api/vendors/route.ts +++ b/app/api/vendors/route.ts @@ -31,11 +31,14 @@ interface CreateVendorData { representativeEmail?: string representativePhone?: string corporateRegistrationNumber?: string + representativeWorkExpirence?: boolean } interface ContactData { contactName: string contactPosition?: string + contactDepartment?: string + contactTask?: string contactEmail: string contactPhone?: string isPrimary?: boolean @@ -218,12 +221,14 @@ export async function POST(request: NextRequest) { await storeVendorFiles(tx, newVendor.id, bankAccountFiles, FILE_TYPES.BANK_ACCOUNT_COPY) } - // Insert contacts + // Insert contacts with new fields for (const contact of contacts) { await tx.insert(vendorContacts).values({ vendorId: newVendor.id, contactName: contact.contactName, contactPosition: contact.contactPosition || null, + contactDepartment: contact.contactDepartment || null, + contactTask: contact.contactTask || null, contactEmail: contact.contactEmail, contactPhone: contact.contactPhone || null, isPrimary: contact.isPrimary ?? false, |
