import { NextRequest, NextResponse } from "next/server" import { writeFile } from "fs/promises" import { join } from "path" import { v4 as uuidv4 } from "uuid" import path from "path" import { revalidateTag } from "next/cache" import db from "@/db/db" import { documents, issueStages, revisions, documentAttachments, } from "@/db/schema/vendorDocu" import { and, eq } from "drizzle-orm" /* change log 유틸 */ import { logRevisionChange, logAttachmentChange, } from "@/lib/vendor-document-list/sync-service" export async function POST(request: NextRequest) { try { const formData = await request.formData() /* ------- 파라미터 파싱 ------- */ const usage = formData.get("usage") as string | null const usageType = formData.get("usageType") as string | null const revision = formData.get("revision") as string | null const docId = Number(formData.get("documentId")) const uploaderName = formData.get("uploaderName") as string | null const comment = formData.get("comment") as string | null const targetSystem = "DOLCE" const attachmentFiles = formData.getAll("attachments") as File[] /* ------- 검증 ------- */ if (!docId || Number.isNaN(docId)) return NextResponse.json({ error: "Invalid documentId" }, { status: 400 }) if (!usage || !revision) return NextResponse.json({ error: "Missing usage or revision" }, { status: 400 }) if (!attachmentFiles.length) return NextResponse.json({ error: "No files provided" }, { status: 400 }) const MAX = 50 * 1024 * 1024 // 50MB (다이얼로그 제한과 맞춤) for (const f of attachmentFiles) if (f.size > MAX) return NextResponse.json( { error: `${f.name} exceeds 50MB limit` }, { status: 400 } ) /* ------- 계약 ID 확보 ------- */ const [docInfo] = await db .select({ contractId: documents.contractId }) .from(documents) .where(eq(documents.id, docId)) .limit(1) if (!docInfo) { return NextResponse.json({ error: "Document not found" }, { status: 404 }) } /* ------- Stage 찾기 로직 ------- */ // 1. usage 값과 일치하는 stage 찾기 let targetStage = await db .select({ id: issueStages.id, stageName: issueStages.stageName }) .from(issueStages) .where(and( eq(issueStages.documentId, docId), eq(issueStages.stageName, usage) )) .limit(1) // 2. 없으면 해당 문서의 첫 번째 stage 사용 if (!targetStage.length) { targetStage = await db .select({ id: issueStages.id, stageName: issueStages.stageName }) .from(issueStages) .where(eq(issueStages.documentId, docId)) .orderBy(issueStages.id) // 첫 번째 stage .limit(1) } if (!targetStage.length) { return NextResponse.json({ error: "No stages found for this document" }, { status: 400 }) } const stage = targetStage[0].stageName const issueStageId = targetStage[0].id /* ------- 트랜잭션 ------- */ const result = await db.transaction(async (tx) => { /* Revision 생성 */ const today = new Date().toISOString().slice(0, 10) // 동일한 revision이 이미 있는지 확인 (usage, usageType도 포함) const whereConditions = [ eq(revisions.issueStageId, issueStageId), eq(revisions.revision, revision), eq(revisions.usage, usage) ] // usageType이 있는 경우에만 조건에 추가 if (usageType) { whereConditions.push(eq(revisions.usageType, usageType)) } const [existingRev] = await tx .select() .from(revisions) .where(and(...whereConditions)) .limit(1) let revisionId: number let revisionData: any if (existingRev) { // 기존 revision 업데이트 const updateData: any = { uploaderName: uploaderName ?? existingRev.uploaderName, comment: comment ?? existingRev.comment, updatedAt: new Date(), } // usage는 항상 업데이트 updateData.usage = usage // usageType이 있는 경우에만 업데이트 if (usageType) { updateData.usageType = usageType } await tx.update(revisions) .set(updateData) .where(eq(revisions.id, existingRev.id)) const [updated] = await tx .select() .from(revisions) .where(eq(revisions.id, existingRev.id)) revisionId = existingRev.id revisionData = updated await logRevisionChange( docInfo.contractId, revisionId, "UPDATE", updated, existingRev, undefined, uploaderName ?? undefined, [targetSystem] ) } else { // 새 revision 생성 const [newRev] = await tx.insert(revisions) .values({ issueStageId, revision, usage, usageType, uploaderType: "vendor", uploaderName: uploaderName ?? undefined, revisionStatus: "UPLOADED", uploadedAt: today, comment: comment ?? undefined, updatedAt: new Date(), }) .returning() revisionId = newRev.id revisionData = newRev await logRevisionChange( docInfo.contractId, revisionId, "CREATE", newRev, undefined, undefined, uploaderName ?? undefined, [targetSystem] ) } /* 첨부파일 처리 */ const uploadedFiles: any[] = [] const baseDir = join(process.cwd(), "public", "documents") for (const file of attachmentFiles) { const ext = path.extname(file.name) const fname = uuidv4() + ext const dest = join(baseDir, fname) await writeFile(dest, Buffer.from(await file.arrayBuffer())) const [att] = await tx.insert(documentAttachments) .values({ revisionId, fileName: file.name, filePath: "/documents/" + fname, fileSize: file.size, fileType: ext.slice(1).toLowerCase() || undefined, updatedAt: new Date(), }) .returning() uploadedFiles.push({ id: att.id, fileName: file.name, fileSize: file.size, filePath: att.filePath, fileType: ext.slice(1).toLowerCase() || null, // ✅ 추가 }) // change_logs: attachment CREATE await logAttachmentChange( docInfo.contractId, att.id, "CREATE", att, undefined, undefined, uploaderName ?? undefined, [targetSystem] ) } /* documents.updatedAt 업데이트 */ await tx.update(documents) .set({ updatedAt: new Date() }) .where(eq(documents.id, docId)) return { revisionId, issueStageId, // ✅ 추가 stage, revision, uploadedFiles, contractId: docInfo.contractId, usage, usageType } }) // 캐시 무효화 try { revalidateTag(`sync-status-${result.contractId}`) console.log(`✅ Cache invalidated for contract ${result.contractId}`) } catch (cacheError) { console.warn('⚠️ Cache invalidation failed:', cacheError) } return NextResponse.json({ success: true, message: `리비전 ${result.revision}이 성공적으로 업로드되었습니다`, data: { revisionId: result.revisionId, issueStageId: issueStageId, // ✅ 추가 stage: result.stage, revision: result.revision, usage: result.usage, usageType: result.usageType, uploaderName: uploaderName, // ✅ 추가 uploadedFiles: result.uploadedFiles, filesCount: result.uploadedFiles.length }, }) } catch (e) { console.error("revision-upload error:", e) return NextResponse.json( { error: "Failed to upload revision", details: String(e) }, { status: 500 }, ) } }