diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-02 20:58:37 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-02 20:58:37 +0900 |
| commit | 3cc0b07f39c0e8dcbd8962865557dd4d9e323d0f (patch) | |
| tree | 5975615e67597d1a2c10e1cc652c628cace1a7bb /app/api | |
| parent | 624cfcf4edb106e6cf0b041d9437ceaa94b6a46d (diff) | |
(대표님) dolce serialNo별 revision 별도 저장 처리
Diffstat (limited to 'app/api')
| -rw-r--r-- | app/api/revision-upload-ship/route.ts | 294 |
1 files changed, 165 insertions, 129 deletions
diff --git a/app/api/revision-upload-ship/route.ts b/app/api/revision-upload-ship/route.ts index 26105efd..180378f3 100644 --- a/app/api/revision-upload-ship/route.ts +++ b/app/api/revision-upload-ship/route.ts @@ -1,90 +1,102 @@ -import { NextRequest, NextResponse } from "next/server" -import { revalidateTag } from "next/cache" -import { getServerSession } from "next-auth/next" -import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { NextRequest, NextResponse } from 'next/server'; +import { revalidateTag } from 'next/cache'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import db from "@/db/db" +import db from '@/db/db'; import { documents, issueStages, revisions, documentAttachments, -} from "@/db/schema/vendorDocu" -import { and, eq } from "drizzle-orm" +} from '@/db/schema/vendorDocu'; +import { and, eq } from 'drizzle-orm'; /* 보안 강화된 파일 저장 유틸리티 */ -import { saveFile, SaveFileResult, saveFileStream } from "@/lib/file-stroage" +import { saveFile, SaveFileResult, saveFileStream } from '@/lib/file-stroage'; /* change log 유틸 */ import { logRevisionChange, logAttachmentChange, -} from "@/lib/vendor-document-list/sync-service" +} from '@/lib/vendor-document-list/sync-service'; export async function POST(request: NextRequest) { try { - // 세션 정보 가져오기 - const session = await getServerSession(authOptions) + const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + return NextResponse.json({ error: 'unauthorized' }); } - const currentUserId = Number(session.user.id) - const currentUserName = session.user.name || session.user.email || "unknown" + const currentUserId = Number(session.user.id); + const currentUserName = + session.user.name || session.user.email || 'no name & email'; - const formData = await request.formData() + 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[] + 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[]; // const issueStageId = formData.get("issueStageId") as string - const serialNo = formData.get("serialNo") as string + const serialNo = formData.get('serialNo') as string; /* ------- 검증 ------- */ if (!docId || Number.isNaN(docId)) - return NextResponse.json({ error: "Invalid documentId" }, { status: 400 }) + return NextResponse.json( + { error: 'Invalid documentId' }, + { status: 400 } + ); if (!usage || !revision) - return NextResponse.json({ error: "Missing usage or revision" }, { status: 400 }) + return NextResponse.json( + { error: 'Missing usage or revision' }, + { status: 400 } + ); if (!attachmentFiles.length) - return NextResponse.json({ error: "No files provided" }, { status: 400 }) + return NextResponse.json({ error: 'No files provided' }, { status: 400 }); // 기본 파일 크기 검증 (보안 함수에서도 검증하지만 조기 체크) - const MAX = 1024 * 1024 * 1024 + const MAX = 1024 * 1024 * 1024; for (const f of attachmentFiles) { if (f.size > MAX) { return NextResponse.json( { error: `${f.name} exceeds 1GB limit` }, { status: 400 } - ) + ); } } /* ------- 계약 ID 확보 ------- */ const [docInfo] = await db - .select({ + .select({ contractId: documents.contractId, - projectId: documents.projectId , - vendorId: documents.vendorId , + projectId: documents.projectId, + vendorId: documents.vendorId, }) .from(documents) .where(eq(documents.id, docId)) - .limit(1) + .limit(1); if (!docInfo) { - return NextResponse.json({ error: "Document not found" }, { status: 404 }) + return NextResponse.json( + { error: 'Document not found' }, + { status: 404 } + ); } // projectId가 null인 경우 처리 if (!docInfo.vendorId) { - return NextResponse.json({ - error: "Document must have a valid project ID for synchronization" - }, { status: 400 }) + return NextResponse.json( + { + error: 'Document must have a valid project ID for synchronization', + }, + { status: 400 } + ); } /* ------- Stage 찾기 로직 ------- */ @@ -92,11 +104,10 @@ export async function POST(request: NextRequest) { 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) + .where( + and(eq(issueStages.documentId, docId), eq(issueStages.stageName, usage)) + ) + .limit(1); // 2. 없으면 해당 문서의 첫 번째 stage 사용 if (!targetStage.length) { @@ -105,43 +116,47 @@ export async function POST(request: NextRequest) { .from(issueStages) .where(eq(issueStages.documentId, docId)) .orderBy(issueStages.id) // 첫 번째 stage - .limit(1) + .limit(1); } if (!targetStage.length) { - return NextResponse.json({ - error: "No stages found for this document" - }, { status: 400 }) + return NextResponse.json( + { + error: 'No stages found for this document', + }, + { status: 400 } + ); } - const stage = targetStage[0].stageName - const issueStageId = targetStage[0].id + 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) - + 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) - ] - + eq(revisions.serialNo, serialNo), + eq(revisions.usage, usage), + ]; + // usageType이 있는 경우에만 조건에 추가 if (usageType) { - whereConditions.push(eq(revisions.usageType, usageType)) + whereConditions.push(eq(revisions.usageType, usageType)); } - + const [existingRev] = await tx .select() .from(revisions) .where(and(...whereConditions)) - .limit(1) + .limit(1); - let revisionId: number - let revisionData: any + let revisionId: number; + let revisionData: any; if (existingRev) { // 기존 revision 업데이트 @@ -149,166 +164,187 @@ export async function POST(request: NextRequest) { uploaderName: uploaderName ?? existingRev.uploaderName, comment: comment ?? existingRev.comment, updatedAt: new Date(), - } - + }; + // usage는 항상 업데이트 - updateData.usage = usage - + updateData.usage = usage; + // usageType이 있는 경우에만 업데이트 if (usageType) { - updateData.usageType = usageType + updateData.usageType = usageType; } - - await tx.update(revisions) + + await tx + .update(revisions) .set(updateData) - .where(eq(revisions.id, existingRev.id)) + .where(eq(revisions.id, existingRev.id)); const [updated] = await tx .select() .from(revisions) - .where(eq(revisions.id, existingRev.id)) + .where(eq(revisions.id, existingRev.id)); - revisionId = existingRev.id - revisionData = updated + revisionId = existingRev.id; + revisionData = updated; await logRevisionChange( docInfo.vendorId!, // null 체크 후이므로 non-null assertion 사용 revisionId, - "UPDATE", + 'UPDATE', updated, existingRev, - currentUserId, // 세션에서 가져온 실제 user ID - currentUserName, // 세션에서 가져온 실제 user name + currentUserId, + currentUserName, [targetSystem] - ) + ); } else { // 새 revision 생성 - const [newRev] = await tx.insert(revisions) + const [newRev] = await tx + .insert(revisions) .values({ issueStageId, serialNo: serialNo, revision, usage, usageType, - uploaderType: "vendor", + uploaderType: 'vendor', uploaderName: uploaderName ?? undefined, - revisionStatus: "UPLOADED", + revisionStatus: 'UPLOADED', uploadedAt: today, comment: comment ?? undefined, updatedAt: new Date(), }) - .returning() + .returning(); - revisionId = newRev.id - revisionData = newRev + revisionId = newRev.id; + revisionData = newRev; await logRevisionChange( docInfo.vendorId!, // null 체크 후이므로 non-null assertion 사용 revisionId, - "CREATE", + 'CREATE', newRev, undefined, - currentUserId, // 세션에서 가져온 실제 user ID - currentUserName, // 세션에서 가져온 실제 user name + currentUserId, + currentUserName, [targetSystem] - ) + ); } /* ------- 보안 강화된 첨부파일 처리 ------- */ - const uploadedFiles: any[] = [] - const securityFailures: string[] = [] + const uploadedFiles: any[] = []; + const securityFailures: string[] = []; for (const file of attachmentFiles) { - console.log(`🔐 보안 검증 시작: ${file.name}`) - + console.log(`🔐 보안 검증 시작: ${file.name}`); + // 보안 강화된 파일 저장 - const saveResult = file.size > 100 * 1024 * 1024 - ? await saveFileStream({ file, directory: "documents", userId: uploaderName ||""}) - : await saveFile({ file, directory: "documents", userId: uploaderName ||"" }) + const saveResult = + file.size > 100 * 1024 * 1024 + ? await saveFileStream({ + file, + directory: 'documents', + userId: uploaderName || '', + }) + : await saveFile({ + file, + directory: 'documents', + userId: uploaderName || '', + }); if (!saveResult.success) { - console.error(`❌ 파일 보안 검증 실패: ${file.name} - ${saveResult.error}`) - securityFailures.push(`${file.name}: ${saveResult.error}`) - continue // 실패한 파일은 건너뛰고 계속 진행 + console.error( + `❌ 파일 보안 검증 실패: ${file.name} - ${saveResult.error}` + ); + securityFailures.push(`${file.name}: ${saveResult.error}`); + continue; // 실패한 파일은 건너뛰고 계속 진행 } - console.log(`✅ 파일 보안 검증 통과: ${file.name}`) - console.log(`📁 저장된 경로: ${saveResult.publicPath}`) + console.log(`✅ 파일 보안 검증 통과: ${file.name}`); + console.log(`📁 저장된 경로: ${saveResult.publicPath}`); // DB에 첨부파일 정보 저장 - const [att] = await tx.insert(documentAttachments) + const [att] = await tx + .insert(documentAttachments) .values({ revisionId, fileName: saveResult.originalName!, // 원본 파일명 - filePath: saveResult.publicPath!, // 웹 접근 경로 + filePath: saveResult.publicPath!, // 웹 접근 경로 fileSize: saveResult.fileSize!, - fileType: saveResult.fileName!.split('.').pop()?.toLowerCase() || undefined, + fileType: + saveResult.fileName!.split('.').pop()?.toLowerCase() || undefined, updatedAt: new Date(), }) - .returning() + .returning(); uploadedFiles.push({ id: att.id, fileName: saveResult.originalName, fileSize: saveResult.fileSize, filePath: saveResult.publicPath, - fileType: saveResult.fileName!.split('.').pop()?.toLowerCase() || null, + fileType: + saveResult.fileName!.split('.').pop()?.toLowerCase() || null, securityChecks: saveResult.securityChecks, // 보안 검증 결과 - }) + }); // change_logs: attachment CREATE await logAttachmentChange( docInfo.vendorId!, att.id, - "CREATE", + 'CREATE', att, undefined, - currentUserId, // 세션에서 가져온 실제 user ID - currentUserName, // 세션에서 가져온 실제 user name + currentUserId, + currentUserName, [targetSystem] - ) + ); } // 보안 검증 실패한 파일이 있으면 경고 반환 if (securityFailures.length > 0) { - console.warn(`⚠️ 일부 파일의 보안 검증 실패:`, securityFailures) - + console.warn(`⚠️ 일부 파일의 보안 검증 실패:`, securityFailures); + // 모든 파일이 실패한 경우 에러 반환 if (uploadedFiles.length === 0) { - throw new Error(`모든 파일의 보안 검증이 실패했습니다: ${securityFailures.join(', ')}`) + throw new Error( + `모든 파일의 보안 검증이 실패했습니다: ${securityFailures.join( + ', ' + )}` + ); } } /* documents.updatedAt 업데이트 */ - await tx.update(documents) + await tx + .update(documents) .set({ updatedAt: new Date() }) - .where(eq(documents.id, docId)) + .where(eq(documents.id, docId)); - return { - revisionId, + return { + revisionId, issueStageId, - stage, - revision, - uploadedFiles, + stage, + revision, + uploadedFiles, vendorId: docInfo.vendorId, usage, usageType, - securityFailures // 보안 실패 정보 포함 - } - }) + securityFailures, // 보안 실패 정보 포함 + }; + }); // 캐시 무효화 try { - revalidateTag(`sync-status-${result.vendorId}`) - console.log(`✅ Cache invalidated for contract ${result.vendorId}`) + revalidateTag(`sync-status-${result.vendorId}`); + console.log(`✅ Cache invalidated for contract ${result.vendorId}`); } catch (cacheError) { - console.warn('⚠️ Cache invalidation failed:', cacheError) + console.warn('⚠️ Cache invalidation failed:', cacheError); } // 응답 메시지 구성 - let message = `리비전 ${result.revision}이 성공적으로 업로드되었습니다` + let message = `리비전 ${result.revision}이 성공적으로 업로드되었습니다`; if (result.securityFailures.length > 0) { - message += ` (일부 파일 보안 검증 실패: ${result.securityFailures.length}개)` + message += ` (일부 파일 보안 검증 실패: ${result.securityFailures.length}개)`; } return NextResponse.json({ @@ -327,12 +363,12 @@ export async function POST(request: NextRequest) { filesCount: result.uploadedFiles.length, securityFailures: result.securityFailures, // 클라이언트에 보안 실패 정보 전달 }, - }) + }); } catch (e) { - console.error("revision-upload error:", e) + console.error('revision-upload error:', e); return NextResponse.json( - { error: "Failed to upload revision", details: String(e) }, - { status: 500 }, - ) + { error: 'Failed to upload revision', details: String(e) }, + { status: 500 } + ); } -}
\ No newline at end of file +} |
