summaryrefslogtreecommitdiff
path: root/app/api
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-10 09:55:45 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-10 09:55:45 +0000
commitc657ef972feeafff16ab0e07cb4771f7dd141ba0 (patch)
treebefabd884b00d3cc632c628b3e3810f61cc9f38d /app/api
parentb8a03c9d130435a71c5d6217d06ccb0beb9697e5 (diff)
(대표님) 20250710 작업사항 - 평가 첨부, 로그인, SEDP 변경 요구사항 반영
Diffstat (limited to 'app/api')
-rw-r--r--app/api/evaluation/attachments/[id]/route.ts151
-rw-r--r--app/api/evaluation/attachments/route.ts201
-rw-r--r--app/api/vendors/route.ts7
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,