diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/api/tech-sales-rfq-download/route.ts | 85 | ||||
| -rw-r--r-- | app/api/tech-sales-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts | 259 |
2 files changed, 344 insertions, 0 deletions
diff --git a/app/api/tech-sales-rfq-download/route.ts b/app/api/tech-sales-rfq-download/route.ts new file mode 100644 index 00000000..b9dd14d1 --- /dev/null +++ b/app/api/tech-sales-rfq-download/route.ts @@ -0,0 +1,85 @@ +import { NextRequest } from "next/server" +import { join } from "path" +import { readFile } from "fs/promises" + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams + const filePath = searchParams.get("path") + + if (!filePath) { + return new Response("File path is required", { status: 400 }) + } + + // 보안: 경로 조작 방지 + if (filePath.includes("..") || !filePath.startsWith("/techsales-rfq/")) { + return new Response("Invalid file path", { status: 400 }) + } + + // 파일 경로 구성 (public 폴더 기준) + const fullPath = join(process.cwd(), "public", filePath) + + try { + // 파일 읽기 + const fileBuffer = await readFile(fullPath) + + // 파일명 추출 + const fileName = filePath.split("/").pop() || "download" + + // MIME 타입 결정 + const ext = fileName.split(".").pop()?.toLowerCase() + let contentType = "application/octet-stream" + + switch (ext) { + case "pdf": + contentType = "application/pdf" + break + case "doc": + contentType = "application/msword" + break + case "docx": + contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + break + case "xls": + contentType = "application/vnd.ms-excel" + break + case "xlsx": + contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + break + case "jpg": + case "jpeg": + contentType = "image/jpeg" + break + case "png": + contentType = "image/png" + break + case "gif": + contentType = "image/gif" + break + case "txt": + contentType = "text/plain" + break + case "zip": + contentType = "application/zip" + break + default: + contentType = "application/octet-stream" + } + + // 응답 헤더 설정 + const headers = new Headers({ + "Content-Type": contentType, + "Content-Disposition": `attachment; filename="${encodeURIComponent(fileName)}"`, + "Content-Length": fileBuffer.length.toString(), + }) + + return new Response(fileBuffer, { headers }) + } catch (fileError) { + console.error("File read error:", fileError) + return new Response("File not found", { status: 404 }) + } + } catch (error) { + console.error("Download error:", error) + return new Response("Internal server error", { status: 500 }) + } +}
\ No newline at end of file diff --git a/app/api/tech-sales-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts b/app/api/tech-sales-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts new file mode 100644 index 00000000..187e4e4f --- /dev/null +++ b/app/api/tech-sales-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts @@ -0,0 +1,259 @@ +import { NextRequest, NextResponse } from "next/server" + +import db from '@/db/db'; +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" + +import { techSalesRfqComments, techSalesRfqCommentAttachments, users } from "@/db/schema" +import { revalidateTag } from "next/cache" +import { eq, and } from "drizzle-orm" + +// 파일 저장을 위한 유틸리티 +import { writeFile, mkdir } from 'fs/promises' +import { join } from 'path' +import crypto from 'crypto' + +/** + * 코멘트 조회 API 엔드포인트 + */ +export async function GET( + request: NextRequest, + { params }: { params: { rfqId: string; vendorId: string } } +) { + try { + // 인증 확인 + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { success: false, message: "인증이 필요합니다" }, + { status: 401 } + ) + } + + const rfqId = parseInt(params.rfqId) + const vendorId = parseInt(params.vendorId) + + // 유효성 검사 + if (isNaN(rfqId) || isNaN(vendorId)) { + return NextResponse.json( + { success: false, message: "유효하지 않은 매개변수입니다" }, + { status: 400 } + ) + } + + // 코멘트 조회 (첨부파일 별도 조회) + const comments = await db + .select({ + id: techSalesRfqComments.id, + rfqId: techSalesRfqComments.rfqId, + vendorId: techSalesRfqComments.vendorId, + userId: techSalesRfqComments.userId, + content: techSalesRfqComments.content, + isVendorComment: techSalesRfqComments.isVendorComment, + createdAt: techSalesRfqComments.createdAt, + updatedAt: techSalesRfqComments.updatedAt, + isRead: techSalesRfqComments.isRead, + userName: users.name, + }) + .from(techSalesRfqComments) + .leftJoin(users, eq(techSalesRfqComments.userId, users.id)) + .where( + and( + eq(techSalesRfqComments.rfqId, rfqId), + eq(techSalesRfqComments.vendorId, vendorId) + ) + ) + .orderBy(techSalesRfqComments.createdAt); + + // 각 코멘트의 첨부파일 조회 + const formattedComments = await Promise.all( + comments.map(async (comment) => { + const attachments = await db + .select({ + id: techSalesRfqCommentAttachments.id, + fileName: techSalesRfqCommentAttachments.fileName, + fileSize: techSalesRfqCommentAttachments.fileSize, + fileType: techSalesRfqCommentAttachments.fileType, + filePath: techSalesRfqCommentAttachments.filePath, + uploadedAt: techSalesRfqCommentAttachments.uploadedAt, + }) + .from(techSalesRfqCommentAttachments) + .where(eq(techSalesRfqCommentAttachments.commentId, comment.id)); + + return { + ...comment, + attachments, + }; + }) + ); + + return NextResponse.json({ + success: true, + data: formattedComments + }) + } catch (error) { + console.error("techSales 코멘트 조회 오류:", error) + return NextResponse.json( + { success: false, message: "코멘트 조회 중 오류가 발생했습니다" }, + { status: 500 } + ) + } +} + +/** + * 코멘트 생성 API 엔드포인트 + */ +export async function POST( + request: NextRequest, + { params }: { params: { rfqId: string; vendorId: string } } +) { + try { + // 인증 확인 + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json( + { success: false, message: "인증이 필요합니다" }, + { status: 401 } + ) + } + + const rfqId = parseInt(params.rfqId) + const vendorId = parseInt(params.vendorId) + + // 유효성 검사 + if (isNaN(rfqId) || isNaN(vendorId)) { + return NextResponse.json( + { success: false, message: "유효하지 않은 매개변수입니다" }, + { status: 400 } + ) + } + + // FormData 파싱 + const formData = await request.formData() + const content = formData.get("content") as string + const isVendorComment = formData.get("isVendorComment") === "true" + const files = formData.getAll("attachments") as File[] + + console.log("POST 요청 파라미터:", { rfqId, vendorId, content, isVendorComment, filesCount: files.length }); + + if (!content && files.length === 0) { + return NextResponse.json( + { success: false, message: "내용이나 첨부파일이 필요합니다" }, + { status: 400 } + ) + } + + // 세션 사용자 ID 확인 + if (!session.user.id) { + return NextResponse.json( + { success: false, message: "사용자 ID를 찾을 수 없습니다" }, + { status: 400 } + ) + } + + // 코멘트 생성 + console.log("코멘트 생성 시도:", { + rfqId, + vendorId, + userId: parseInt(session.user.id), + content, + isVendorComment, + }); + + const [comment] = await db + .insert(techSalesRfqComments) + .values({ + rfqId, + vendorId, + userId: parseInt(session.user.id), + content, + isVendorComment, + isRead: !isVendorComment, // 본인 메시지는 읽음 처리 + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning() + + console.log("코멘트 생성 성공:", comment); + + // 첨부파일 처리 + const attachments = [] + if (files.length > 0) { + console.log("첨부파일 처리 시작:", files.length); + + // 디렉토리 생성 + const uploadDir = join(process.cwd(), "public", `tech-sales-rfq-${rfqId}`, `vendor-${vendorId}`, `comment-${comment.id}`) + await mkdir(uploadDir, { recursive: true }) + + // 각 파일 저장 + for (const file of files) { + const buffer = Buffer.from(await file.arrayBuffer()) + const filename = `${Date.now()}-${crypto.randomBytes(8).toString("hex")}-${file.name.replace(/[^a-zA-Z0-9.-]/g, "_")}` + const filePath = join(uploadDir, filename) + + // 파일 쓰기 + await writeFile(filePath, buffer) + + // DB에 첨부파일 정보 저장 + const [attachment] = await db + .insert(techSalesRfqCommentAttachments) + .values({ + rfqId, + commentId: comment.id, + fileName: file.name, + fileSize: file.size, + fileType: file.type, + filePath: `/tech-sales-rfq-${rfqId}/vendor-${vendorId}/comment-${comment.id}/${filename}`, + isVendorUpload: isVendorComment, + uploadedBy: parseInt(session.user.id), + vendorId, + uploadedAt: new Date(), + }) + .returning() + + attachments.push({ + id: attachment.id, + fileName: attachment.fileName, + fileSize: attachment.fileSize, + fileType: attachment.fileType, + filePath: attachment.filePath, + uploadedAt: attachment.uploadedAt + }) + } + + console.log("첨부파일 처리 완료:", attachments.length); + } + + // 캐시 무효화 + revalidateTag(`tech-sales-rfq-${rfqId}-comments`) + + // 응답 데이터 구성 + const responseData = { + id: comment.id, + rfqId: comment.rfqId, + vendorId: comment.vendorId, + userId: comment.userId, + content: comment.content, + isVendorComment: comment.isVendorComment, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + userName: session.user.name, + attachments, + isRead: comment.isRead + } + + console.log("응답 데이터:", responseData); + + return NextResponse.json({ + success: true, + data: { comment: responseData } + }) + } catch (error) { + console.error("techSales 코멘트 생성 오류:", error) + console.error("Error stack:", error instanceof Error ? error.stack : "Unknown error"); + return NextResponse.json( + { success: false, message: "코멘트 생성 중 오류가 발생했습니다", error: error instanceof Error ? error.message : "Unknown error" }, + { status: 500 } + ) + } +}
\ No newline at end of file |
