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 { 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 { 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 } ); } }