summaryrefslogtreecommitdiff
path: root/app/api/swp/upload
diff options
context:
space:
mode:
Diffstat (limited to 'app/api/swp/upload')
-rw-r--r--app/api/swp/upload/route.ts364
1 files changed, 364 insertions, 0 deletions
diff --git a/app/api/swp/upload/route.ts b/app/api/swp/upload/route.ts
new file mode 100644
index 00000000..d17fcff7
--- /dev/null
+++ b/app/api/swp/upload/route.ts
@@ -0,0 +1,364 @@
+import { NextRequest, NextResponse } from "next/server";
+import * as fs from "fs/promises";
+import * as path from "path";
+import { eq, and } from "drizzle-orm";
+import db from "@/db/db";
+import { swpDocuments } from "@/db/schema/SWP/swp-documents";
+import { fetchGetVDRDocumentList, fetchGetExternalInboxList } from "@/lib/swp/api-client";
+import { syncSwpProject } from "@/lib/swp/sync-service";
+import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils";
+
+// API Route 설정
+export const runtime = "nodejs";
+export const maxDuration = 300; // 5분 타임아웃 (대용량 파일 업로드 대응)
+
+interface InBoxFileInfo {
+ CPY_CD: string;
+ FILE_NM: string;
+ OFDC_NO: string | null;
+ PROJ_NO: string;
+ OWN_DOC_NO: string;
+ REV_NO: string;
+ STAGE: string;
+ STAT: string;
+ FILE_SZ: string;
+ FLD_PATH: string;
+}
+
+/**
+ * 파일명 파싱: [OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDDhhmmss].[확장자]
+ */
+function parseFileName(fileName: string) {
+ const lastDotIndex = fileName.lastIndexOf(".");
+ const extension = lastDotIndex !== -1 ? fileName.substring(lastDotIndex + 1) : "";
+ const nameWithoutExt = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName;
+
+ const parts = nameWithoutExt.split("_");
+
+ if (parts.length !== 4) {
+ throw new Error(
+ `잘못된 파일명 형식입니다: ${fileName}. ` +
+ `형식: [OWN_DOC_NO]_[REV_NO]_[STAGE]_[YYYYMMDDhhmmss].확장자`
+ );
+ }
+
+ const [ownDocNo, revNo, stage, timestamp] = parts;
+
+ if (!/^\d{14}$/.test(timestamp)) {
+ throw new Error(
+ `잘못된 타임스탬프 형식입니다: ${timestamp}. ` +
+ `YYYYMMDDhhmmss 형식이어야 합니다.`
+ );
+ }
+
+ return { ownDocNo, revNo, stage, timestamp, extension };
+}
+
+/**
+ * CPY_CD 조회
+ */
+async function getCpyCdForVendor(projNo: string, vndrCd: string): Promise<string> {
+ const result = await db
+ .select({ CPY_CD: swpDocuments.CPY_CD })
+ .from(swpDocuments)
+ .where(and(eq(swpDocuments.PROJ_NO, projNo), eq(swpDocuments.VNDR_CD, vndrCd)))
+ .limit(1);
+
+ if (!result || result.length === 0 || !result[0].CPY_CD) {
+ throw new Error(
+ `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 해당하는 회사 코드(CPY_CD)를 찾을 수 없습니다.`
+ );
+ }
+
+ return result[0].CPY_CD;
+}
+
+/**
+ * SaveInBoxList API 호출
+ */
+async function callSaveInBoxList(fileInfos: InBoxFileInfo[]): Promise<void> {
+ const ddcUrl = process.env.DDC_BASE_URL || "http://60.100.99.217/DDC/Services/WebService.svc";
+ const url = `${ddcUrl}/SaveInBoxList`;
+
+ const request = { externalInboxLists: fileInfos };
+
+ console.log("[callSaveInBoxList] 요청:", JSON.stringify(request, null, 2));
+
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ body: JSON.stringify(request),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`SaveInBoxList API 호출 실패: ${response.statusText} - ${errorText}`);
+ }
+
+ const data = await response.json();
+ console.log("[callSaveInBoxList] 응답:", JSON.stringify(data, null, 2));
+
+ // SaveInBoxListResult는 성공한 FLD_PATH들을 쉼표로 구분한 문자열
+ // 예: "\\\\ProjNo\\\\CpyCd\\\\YYYYMMDDhhmmss, \\\\ProjNo\\\\CpyCd\\\\YYYYMMDDhhmmss"
+ if (!data.SaveInBoxListResult) {
+ throw new Error("SaveInBoxList API 실패: 응답에 SaveInBoxListResult가 없습니다.");
+ }
+
+ const result = data.SaveInBoxListResult;
+
+ // 문자열 응답인 경우 (정상)
+ if (typeof result === "string") {
+ if (result.trim().length === 0) {
+ throw new Error("SaveInBoxList API 실패: 빈 응답이 반환되었습니다.");
+ }
+ // 성공한 FLD_PATH 개수 로깅
+ const successPaths = result.split(",").map(p => p.trim()).filter(p => p.length > 0);
+ console.log(`[callSaveInBoxList] 성공: ${successPaths.length}개 파일 등록 완료`);
+ return;
+ }
+
+ // 객체 응답인 경우 (레거시 또는 에러)
+ if (typeof result === "object" && result !== null) {
+ const objResult = result as { success?: boolean; message?: string };
+ if (objResult.success === false) {
+ throw new Error(
+ `SaveInBoxList API 실패: ${objResult.message || "알 수 없는 오류"}`
+ );
+ }
+ }
+}
+
+/**
+ * POST /api/swp/upload
+ * FormData로 파일 업로드
+ */
+export async function POST(request: NextRequest) {
+ try {
+ const formData = await request.formData();
+
+ const projNo = formData.get("projNo") as string;
+ const vndrCd = formData.get("vndrCd") as string;
+
+ if (!projNo || !vndrCd) {
+ return NextResponse.json(
+ { success: false, message: "projNo와 vndrCd는 필수입니다." },
+ { status: 400 }
+ );
+ }
+
+ // CPY_CD 조회
+ console.log(`[upload] CPY_CD 조회: projNo=${projNo}, vndrCd=${vndrCd}`);
+ const cpyCd = await getCpyCdForVendor(projNo, vndrCd);
+ console.log(`[upload] CPY_CD: ${cpyCd}`);
+
+ const files = formData.getAll("files") as File[];
+
+ if (!files || files.length === 0) {
+ return NextResponse.json(
+ { success: false, message: "업로드할 파일이 없습니다." },
+ { status: 400 }
+ );
+ }
+
+ const result = {
+ successCount: 0,
+ failedCount: 0,
+ details: [] as Array<{ fileName: string; success: boolean; error?: string; networkPath?: string }>,
+ };
+
+ const inBoxFileInfos: InBoxFileInfo[] = [];
+ const swpMountDir = process.env.SWP_MOUNT_DIR || "/mnt/swp-smb-dir/";
+
+ for (const file of files) {
+ try {
+ // 파일명 파싱
+ const parsed = parseFileName(file.name);
+ console.log(`[upload] 파일명 파싱:`, parsed);
+
+ // 네트워크 경로 생성
+ const networkPath = path.join(swpMountDir, projNo, cpyCd, parsed.timestamp, file.name);
+
+ // 파일 중복 체크
+ try {
+ await fs.access(networkPath, fs.constants.F_OK);
+ result.failedCount++;
+ result.details.push({
+ fileName: file.name,
+ success: false,
+ error: "파일이 이미 존재합니다.",
+ });
+ console.warn(`[upload] 파일 중복: ${networkPath}`);
+ continue;
+ } catch {
+ // 파일이 존재하지 않음 (정상)
+ }
+
+ // 디렉토리 생성
+ const directory = path.dirname(networkPath);
+ await fs.mkdir(directory, { recursive: true });
+
+ // 파일 저장 (스트리밍 방식)
+ const arrayBuffer = await file.arrayBuffer();
+ debugLog(`[upload] ArrayBuffer 변환 완료: ${file.name}`, {
+ arrayBufferSize: arrayBuffer.byteLength,
+ fileType: file.type,
+ originalSize: file.size
+ });
+
+ const buffer = Buffer.from(arrayBuffer);
+ debugLog(`[upload] Buffer 생성 완료: ${file.name}`, {
+ bufferLength: buffer.length,
+ bufferType: typeof buffer,
+ isBuffer: Buffer.isBuffer(buffer),
+ first20Bytes: buffer.slice(0, 20).toString('hex')
+ });
+
+ console.log(`[upload] 파일 저장: ${file.name} (${buffer.length} bytes)`);
+
+ // 저장 전 buffer 상태 확인
+ debugLog(`[upload] 저장 직전 buffer 상태`, {
+ constructor: buffer.constructor.name,
+ isBuffer: Buffer.isBuffer(buffer),
+ jsonStringified: JSON.stringify(buffer).substring(0, 100) + '...'
+ });
+
+ await fs.writeFile(networkPath, buffer);
+ debugSuccess(`[upload] 파일 저장 완료: ${networkPath}`);
+
+ // 저장된 파일 검증
+ const savedFileStats = await fs.stat(networkPath);
+ debugLog(`[upload] 저장된 파일 정보`, {
+ size: savedFileStats.size,
+ expectedSize: buffer.length,
+ sizeMatch: savedFileStats.size === buffer.length
+ });
+
+ // 저장된 파일 첫 부분 읽어서 검증
+ const verifyBuffer = await fs.readFile(networkPath);
+ debugLog(`[upload] 저장된 파일 검증`, {
+ readSize: verifyBuffer.length,
+ first20Bytes: verifyBuffer.slice(0, 20).toString('hex'),
+ isBuffer: Buffer.isBuffer(verifyBuffer),
+ matchesOriginal: buffer.slice(0, 20).equals(verifyBuffer.slice(0, 20))
+ });
+
+ // InBox 파일 정보 준비
+ const fldPath = `\\\\${projNo}\\\\${cpyCd}\\\\${parsed.timestamp}`;
+
+ inBoxFileInfos.push({
+ CPY_CD: cpyCd,
+ FILE_NM: file.name,
+ OFDC_NO: null,
+ PROJ_NO: projNo,
+ OWN_DOC_NO: parsed.ownDocNo,
+ REV_NO: parsed.revNo,
+ STAGE: parsed.stage,
+ STAT: "SCW01",
+ FILE_SZ: String(buffer.length),
+ FLD_PATH: fldPath,
+ });
+
+ result.successCount++;
+ result.details.push({
+ fileName: file.name,
+ success: true,
+ networkPath,
+ });
+ } catch (error) {
+ result.failedCount++;
+ result.details.push({
+ fileName: file.name,
+ success: false,
+ error: error instanceof Error ? error.message : "알 수 없는 오류",
+ });
+ console.error(`[upload] 파일 처리 실패: ${file.name}`, error);
+ debugError(`[upload] 파일 처리 실패: ${file.name}`, {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined
+ });
+ }
+ }
+
+ // SaveInBoxList API 호출
+ if (inBoxFileInfos.length > 0) {
+ console.log(`[upload] SaveInBoxList API 호출: ${inBoxFileInfos.length}개 파일`);
+ await callSaveInBoxList(inBoxFileInfos);
+ }
+
+ // 업로드 성공 후 동기화 처리 (현재 벤더의 변경사항만)
+ if (result.successCount > 0) {
+ try {
+ console.log(`[upload] 동기화 시작: projNo=${projNo}, vndrCd=${vndrCd}`);
+
+ // GetVDRDocumentList 및 GetExternalInboxList API 호출 (벤더 필터 적용)
+ const [documents, files] = await Promise.all([
+ fetchGetVDRDocumentList({
+ proj_no: projNo,
+ doc_gb: "V",
+ vndrCd: vndrCd, // 현재 벤더만 필터링
+ }),
+ fetchGetExternalInboxList({
+ projNo: projNo,
+ vndrCd: vndrCd, // 현재 벤더만 필터링
+ }),
+ ]);
+
+ console.log(`[upload] API 조회 완료: 문서 ${documents.length}개, 파일 ${files.length}개`);
+
+ // 동기화 실행
+ const syncResult = await syncSwpProject(projNo, documents, files);
+
+ if (syncResult.success) {
+ console.log(`[upload] 동기화 완료:`, syncResult.stats);
+ } else {
+ console.warn(`[upload] 동기화 경고:`, syncResult.errors);
+ }
+ } catch (syncError) {
+ // 동기화 실패는 경고로만 처리 (업로드 자체는 성공)
+ console.error("[upload] 동기화 실패 (업로드는 성공):", syncError);
+ }
+ }
+
+ // 결과 메시지 생성
+ let message: string;
+ let success: boolean;
+
+ if (result.failedCount === 0) {
+ success = true;
+ message = `${result.successCount}개 파일이 성공적으로 업로드 및 동기화되었습니다.`;
+ } else if (result.successCount === 0) {
+ success = false;
+ message = `모든 파일 업로드에 실패했습니다. (${result.failedCount}개)`;
+ } else {
+ success = true;
+ message = `${result.successCount}개 파일 업로드 성공, ${result.failedCount}개 실패`;
+ }
+
+ console.log(`[upload] 완료:`, { success, message, result });
+
+ return NextResponse.json({
+ success,
+ message,
+ successCount: result.successCount,
+ failedCount: result.failedCount,
+ details: result.details,
+ });
+ } catch (error) {
+ console.error("[upload] 오류:", error);
+ debugError(`[upload] 전체 프로세스 실패`, {
+ error: error instanceof Error ? error.message : String(error),
+ stack: error instanceof Error ? error.stack : undefined
+ });
+ return NextResponse.json(
+ {
+ success: false,
+ message: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.",
+ },
+ { status: 500 }
+ );
+ }
+}
+