summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/api/tech-sales-rfq-download/route.ts85
-rw-r--r--app/api/tech-sales-rfqs/[rfqId]/vendors/[vendorId]/comments/route.ts259
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