summaryrefslogtreecommitdiff
path: root/app/api
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-02 20:58:37 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-02 20:58:37 +0900
commit3cc0b07f39c0e8dcbd8962865557dd4d9e323d0f (patch)
tree5975615e67597d1a2c10e1cc652c628cace1a7bb /app/api
parent624cfcf4edb106e6cf0b041d9437ceaa94b6a46d (diff)
(대표님) dolce serialNo별 revision 별도 저장 처리
Diffstat (limited to 'app/api')
-rw-r--r--app/api/revision-upload-ship/route.ts294
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
+}