summaryrefslogtreecommitdiff
path: root/app/api/partners
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-15 14:41:01 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-15 14:41:01 +0000
commit4ee8b24cfadf47452807fa2af801385ed60ab47c (patch)
treee1d1fb029f0cf5519c517494bf9a545505c35700 /app/api/partners
parent265859d691a01cdcaaf9154f93c38765bc34df06 (diff)
(대표님) 작업사항 - rfqLast, tbeLast, pdfTron, userAuth
Diffstat (limited to 'app/api/partners')
-rw-r--r--app/api/partners/rfq-last/[id]/response/route.ts12
-rw-r--r--app/api/partners/tbe/[sessionId]/documents/route.ts275
2 files changed, 281 insertions, 6 deletions
diff --git a/app/api/partners/rfq-last/[id]/response/route.ts b/app/api/partners/rfq-last/[id]/response/route.ts
index db320dde..1fc9d5dd 100644
--- a/app/api/partners/rfq-last/[id]/response/route.ts
+++ b/app/api/partners/rfq-last/[id]/response/route.ts
@@ -156,7 +156,10 @@ export async function POST(
const fileRecords = []
if (files.length > 0) {
- for (const file of files) {
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i]
+ const metadata = data.fileMetadata?.[i] // 인덱스로 메타데이터 매칭
+
try {
const filename = `${uuidv4()}_${file.name.replace(/[^a-zA-Z0-9.-]/g, '_')}`
const filepath = path.join(uploadDir, filename)
@@ -165,28 +168,25 @@ export async function POST(
if (file.size > 50 * 1024 * 1024) { // 50MB 이상
await saveFileStream(file, filepath)
} else {
- // 작은 파일은 기존 방식
const buffer = Buffer.from(await file.arrayBuffer())
await writeFile(filepath, buffer)
}
fileRecords.push({
vendorResponseId: result.id,
- attachmentType: (file as any).attachmentType || "기타",
+ attachmentType: metadata?.attachmentType || "기타", // 메타데이터에서 가져옴
fileName: filename,
originalFileName: file.name,
filePath: `/uploads/rfq/${rfqId}/${filename}`,
fileSize: file.size,
fileType: file.type,
- description: (file as any).description,
+ description: metadata?.description || "", // 메타데이터에서 가져옴
uploadedBy: session.user.id,
})
} catch (fileError) {
console.error(`Failed to save file ${file.name}:`, fileError)
- // 파일 저장 실패 시 계속 진행 (다른 파일들은 저장)
}
}
-
// DB에 파일 정보 저장
if (fileRecords.length > 0) {
await db.insert(rfqLastVendorAttachments).values(fileRecords)
diff --git a/app/api/partners/tbe/[sessionId]/documents/route.ts b/app/api/partners/tbe/[sessionId]/documents/route.ts
new file mode 100644
index 00000000..0045ea43
--- /dev/null
+++ b/app/api/partners/tbe/[sessionId]/documents/route.ts
@@ -0,0 +1,275 @@
+// app/api/partners/tbe/[sessionId]/documents/route.ts
+import { NextRequest, NextResponse } from "next/server"
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import db from "@/db/db"
+import {
+ rfqLastTbeDocumentReviews,
+ rfqLastTbeSessions,
+ rfqLastTbeHistory,
+ rfqLastTbeVendorDocuments
+} from "@/db/schema"
+import { eq, and } from "drizzle-orm"
+import { writeFile, mkdir } from "fs/promises"
+import { createWriteStream } from "fs"
+import { pipeline } from "stream/promises"
+import path from "path"
+import { v4 as uuidv4 } from "uuid"
+
+// 1GB 파일 지원을 위한 설정
+export const config = {
+ api: {
+ bodyParser: {
+ sizeLimit: '1gb',
+ },
+ responseLimit: false,
+ },
+}
+
+// 스트리밍으로 파일 저장
+async function saveFileStream(file: File, filepath: string) {
+ const stream = file.stream()
+ const writeStream = createWriteStream(filepath)
+ await pipeline(stream, writeStream)
+}
+
+// POST: TBE 문서 업로드
+export async function POST(request: NextRequest, { params }: { params: { sessionId: string } }) {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user || session.user.domain !== "partners") {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
+ }
+
+ const tbeSessionId = Number(params.sessionId)
+ const formData = await request.formData()
+
+ // ✅ 프런트 기frfqLastTbeVendorDocuments본값 'other' 등을 안전한 enum으로 매핑
+ const documentType = (formData.get("documentType") as string | undefined)
+ const documentName = (formData.get("documentName") as string | undefined)?.trim() || "Untitled"
+ const description = (formData.get("description") as string | undefined) || ""
+ const file = formData.get("file") as File | null
+
+ if (!file) {
+ return NextResponse.json({ error: "파일이 필요합니다" }, { status: 400 })
+ }
+
+ // 세션/권한
+ const tbeSession = await db.query.rfqLastTbeSessions.findFirst({
+ where: eq(rfqLastTbeSessions.id, tbeSessionId),
+ with: { vendor: true },
+ })
+ if (!tbeSession) return NextResponse.json({ error: "TBE 세션을 찾을 수 없습니다" }, { status: 404 })
+
+ // 권한 체크: 회사 기준으로 통일 (위/아래 GET도 동일 기준을 권장)
+ if (tbeSession.vendor?.id !== session.user.companyId) {
+ return NextResponse.json({ error: "권한이 없습니다" }, { status: 403 })
+ }
+
+ // 저장 경로
+ const isDev = process.env.NODE_ENV === "development"
+ const uploadDir = isDev
+ ? path.join(process.cwd(), "public", "uploads", "tbe", String(tbeSessionId), "vendor")
+ : path.join(process.env.NAS_PATH || "/nas", "uploads", "tbe", String(tbeSessionId), "vendor")
+
+ await mkdir(uploadDir, { recursive: true })
+
+ const safeOriginal = file.name.replace(/[^a-zA-Z0-9.\-_\s]/g, "_")
+ const filename = `${uuidv4()}_${safeOriginal}`
+ const filepath = path.join(uploadDir, filename)
+
+ try {
+ if (file.size > 50 * 1024 * 1024) {
+ await saveFileStream(file, filepath)
+ } else {
+ const buffer = Buffer.from(await file.arrayBuffer())
+ await writeFile(filepath, buffer)
+ }
+ } catch (e) {
+ console.error("파일 저장 실패:", e)
+ return NextResponse.json({ error: "파일 저장에 실패했습니다" }, { status: 500 })
+ }
+
+ // 트랜잭션
+ const result = await db.transaction(async (tx) => {
+ // 1) 벤더 업로드 문서 insert
+ const [vendorDoc] = await tx
+ .insert(rfqLastTbeVendorDocuments)
+ .values({
+ tbeSessionId,
+ documentType, // enum 매핑된 값
+ isResponseToReviewId: null, // 필요 시 formData에서 받아 세팅
+ fileName: filename,
+ originalFileName: file.name,
+ filePath: `/uploads/tbe/${tbeSessionId}/vendor/${filename}`,
+ fileSize: Number(file.size),
+ fileType: file.type || null,
+ documentNo: null,
+ revisionNo: null,
+ issueDate: null,
+ description,
+ submittalRemarks: null,
+ reviewRequired: true,
+ reviewStatus: "pending",
+ submittedBy: session.user.id,
+ submittedAt: new Date(),
+ reviewedBy: null,
+ reviewedAt: null,
+ reviewComments: null,
+ })
+ .returning()
+
+ // 2) (선택) 기존 리뷰 테이블에도 “벤더가 올린 검토대상 문서”로 남기고 싶다면 유지
+ // 필요 없다면 아래 블록은 제거 가능
+ const [documentReview] = await tx
+ .insert(rfqLastTbeDocumentReviews)
+ .values({
+ tbeSessionId,
+ vendorAttachmentId:vendorDoc.id,
+ documentSource: "vendor",
+ documentType: documentType, // 동일 매핑
+ documentName: documentName, // UX 표시용 이름
+ reviewStatus: "미검토",
+ reviewComments: description,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ .returning()
+
+ // 3) 세션 상태 전환
+ if (tbeSession.status === "준비중") {
+ await tx
+ .update(rfqLastTbeSessions)
+ .set({
+ status: "진행중",
+ actualStartDate: new Date(),
+ updatedAt: new Date(),
+ updatedBy: session.user.id,
+ })
+ .where(eq(rfqLastTbeSessions.id, tbeSessionId))
+ }
+
+ // 4) 이력
+ await tx.insert(rfqLastTbeHistory).values({
+ tbeSessionId,
+ actionType: "document_review",
+ changeDescription: `벤더 문서 업로드: ${documentName}`,
+ changeDetails: {
+ vendorDocumentId: vendorDoc.id,
+ documentReviewId: documentReview.id,
+ documentName: documentName,
+ documentType: documentType,
+ filePath: vendorDoc.filePath,
+ },
+ performedBy: session.user.id,
+ performedByType: "vendor",
+ performedAt: new Date(),
+ })
+
+ if (tbeSession.status === "준비중") {
+ await tx.insert(rfqLastTbeHistory).values({
+ tbeSessionId,
+ actionType: "status_change",
+ previousStatus: "준비중",
+ newStatus: "진행중",
+ changeDescription: "벤더 문서 업로드로 인한 상태 변경",
+ performedBy: session.user.id,
+ performedByType: "vendor",
+ performedAt: new Date(),
+ })
+ }
+
+ return {
+ vendorDoc,
+ documentReview,
+ }
+ })
+
+ return NextResponse.json({
+ success: true,
+ data: {
+ vendorDocumentId: result.vendorDoc.id,
+ filePath: result.vendorDoc.filePath,
+ originalFileName: result.vendorDoc.originalFileName,
+ fileSize: result.vendorDoc.fileSize,
+ fileType: result.vendorDoc.fileType,
+ },
+ message: "문서가 성공적으로 업로드되었습니다",
+ })
+ } catch (error) {
+ console.error("TBE 문서 업로드 오류:", error)
+ return NextResponse.json({ error: "문서 업로드에 실패했습니다" }, { status: 500 })
+ }
+}
+
+// GET: TBE 세션의 문서 목록 조회
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { sessionId: string } }
+) {
+ try {
+ const session = await getServerSession(authOptions)
+ if (!session?.user || session.user.domain !== "partners") {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
+ }
+
+ const tbeSessionId = parseInt(params.sessionId)
+
+ // TBE 세션 확인 및 권한 체크
+ const tbeSession = await db.query.rfqLastTbeSessions.findFirst({
+ where: eq(rfqLastTbeSessions.id, tbeSessionId),
+ with: {
+ vendor: true,
+ documentReviews: {
+ orderBy: (reviews, { desc }) => [desc(reviews.createdAt)],
+ }
+ }
+ })
+
+ if (!tbeSession) {
+ return NextResponse.json({ error: "TBE 세션을 찾을 수 없습니다" }, { status: 404 })
+ }
+
+ // 벤더 권한 확인
+ if (tbeSession.vendor.userId !== session.user.id) {
+ return NextResponse.json({ error: "권한이 없습니다" }, { status: 403 })
+ }
+
+ // PDFTron 코멘트 수 집계 (필요시)
+ const documentsWithDetails = await Promise.all(
+ tbeSession.documentReviews.map(async (doc) => {
+ // PDFTron 코멘트 수 조회
+ const pdftronComments = await db.query.rfqLastTbePdftronComments.findFirst({
+ where: eq(rfqLastTbePdftronComments.documentReviewId, doc.id),
+ })
+
+ return {
+ ...doc,
+ comments: pdftronComments?.commentSummary || {
+ totalCount: 0,
+ openCount: 0,
+ },
+ }
+ })
+ )
+
+ return NextResponse.json({
+ success: true,
+ session: {
+ id: tbeSession.id,
+ sessionCode: tbeSession.sessionCode,
+ sessionTitle: tbeSession.sessionTitle,
+ sessionStatus: tbeSession.status,
+ evaluationResult: tbeSession.evaluationResult,
+ },
+ documents: documentsWithDetails,
+ })
+
+ } catch (error) {
+ console.error("문서 목록 조회 오류:", error)
+ return NextResponse.json(
+ { error: "문서 목록 조회에 실패했습니다" },
+ { status: 500 }
+ )
+ }
+} \ No newline at end of file