diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/api/swp/download/[fileId]/route.ts | 160 | ||||
| -rw-r--r-- | app/api/swp/upload/route.ts | 138 |
2 files changed, 111 insertions, 187 deletions
diff --git a/app/api/swp/download/[fileId]/route.ts b/app/api/swp/download/[fileId]/route.ts index 3af560aa..fe422015 100644 --- a/app/api/swp/download/[fileId]/route.ts +++ b/app/api/swp/download/[fileId]/route.ts @@ -1,9 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import * as fs from "fs/promises"; -import * as path from "path"; -import { eq } from "drizzle-orm"; -import db from "@/db/db"; -import { swpDocumentFiles } from "@/db/schema/SWP/swp-documents"; +import { downloadDocumentFile } from "@/lib/swp/document-service"; import { debugLog, debugError, debugSuccess } from "@/lib/debug-utils"; // API Route 설정 @@ -11,116 +7,67 @@ export const runtime = "nodejs"; export const maxDuration = 60; // 1분 타임아웃 /** - * GET /api/swp/download/[fileId] - * 파일 다운로드 (바이너리 직접 전송) + * GET /api/swp/download/[ownDocNo]?projNo=xxx&fileName=xxx + * 파일 다운로드 (Full API 기반) + * + * Query Parameters: + * - projNo: 프로젝트 번호 + * - fileName: 파일명 */ export async function GET( request: NextRequest, { params }: { params: Promise<{ fileId: string }> } ) { try { - const { fileId: fileIdString } = await params; - const fileId = parseInt(fileIdString, 10); - - if (isNaN(fileId)) { - return NextResponse.json( - { success: false, error: "잘못된 파일 ID입니다." }, - { status: 400 } - ); - } - - debugLog(`[download] 다운로드 시작`, { fileId }); + const { fileId: ownDocNo } = await params; + const { searchParams } = new URL(request.url); + const projNo = searchParams.get("projNo"); + const fileName = searchParams.get("fileName"); - // 1. 파일 정보 조회 - const fileInfo = await db - .select({ - FILE_NM: swpDocumentFiles.FILE_NM, - FLD_PATH: swpDocumentFiles.FLD_PATH, - }) - .from(swpDocumentFiles) - .where(eq(swpDocumentFiles.id, fileId)) - .limit(1); + debugLog(`[download] 다운로드 시작`, { projNo, ownDocNo, fileName }); - if (!fileInfo || fileInfo.length === 0) { - debugError(`[download] 파일 정보 없음`, { fileId }); + // 파라미터 검증 + if (!projNo || !ownDocNo || !fileName) { + debugError(`[download] 필수 파라미터 누락`, { projNo, ownDocNo, fileName }); return NextResponse.json( - { success: false, error: "파일 정보를 찾을 수 없습니다." }, - { status: 404 } - ); - } - - const { FILE_NM, FLD_PATH } = fileInfo[0]; - debugLog(`[download] 파일 정보 조회 완료`, { FILE_NM, FLD_PATH }); - - if (!FLD_PATH || !FILE_NM) { - debugError(`[download] 파일 경로 또는 이름 없음`, { FILE_NM, FLD_PATH }); - return NextResponse.json( - { success: false, error: "파일 경로 또는 파일명이 없습니다." }, - { status: 404 } - ); - } - - // 2. NFS 마운트 경로 확인 - const nfsBasePath = process.env.SWP_MOUNT_DIR; - if (!nfsBasePath) { - debugError(`[download] SWP_MOUNT_DIR 환경변수가 설정되지 않았습니다.`); - return NextResponse.json( - { success: false, error: "서버 설정 오류: NFS 경로가 설정되지 않았습니다." }, - { status: 500 } + { + success: false, + error: "필수 파라미터가 누락되었습니다. (projNo, ownDocNo, fileName)" + }, + { status: 400 } ); } - // 3. 전체 파일 경로 생성 - const normalizedFldPath = FLD_PATH.replace(/\\/g, '/'); - const fullPath = path.join(nfsBasePath, normalizedFldPath, FILE_NM); + // document-service의 downloadDocumentFile 사용 + const result = await downloadDocumentFile(projNo, ownDocNo, fileName); - debugLog(`[download] 파일 경로`, { - fileId, - FILE_NM, - FLD_PATH, - normalizedFldPath, - fullPath, - }); - - // 4. 파일 존재 여부 확인 - try { - await fs.access(fullPath, fs.constants.R_OK); - } catch (accessError) { - debugError(`[download] 파일 접근 불가`, { fullPath, error: accessError }); + if (!result.success || !result.data) { + debugError(`[download] 다운로드 실패`, { error: result.error }); return NextResponse.json( - { success: false, error: `파일을 찾을 수 없습니다: ${FILE_NM}` }, + { + success: false, + error: result.error || "파일 다운로드 실패" + }, { status: 404 } ); } - // 5. 파일 읽기 - debugLog(`[download] 파일 읽기 시작`, { fullPath }); - const fileBuffer = await fs.readFile(fullPath); - - debugLog(`[download] 파일 Buffer 읽기 완료`, { - bufferLength: fileBuffer.length, - isBuffer: Buffer.isBuffer(fileBuffer), - bufferType: typeof fileBuffer, - constructor: fileBuffer.constructor.name, - first20Bytes: fileBuffer.slice(0, 20).toString('hex') - }); - - // 6. MIME 타입 결정 - const mimeType = getMimeType(FILE_NM); - debugSuccess(`[download] 다운로드 성공`, { - fileName: FILE_NM, - size: fileBuffer.length, - mimeType, + fileName: result.fileName, + size: result.data.length, + mimeType: result.mimeType, }); - // 7. 바이너리 응답 반환 - return new NextResponse(fileBuffer, { + // Uint8Array를 Buffer로 변환 + const buffer = Buffer.from(result.data); + + // 바이너리 응답 반환 + return new NextResponse(buffer, { status: 200, headers: { - "Content-Type": mimeType, - "Content-Disposition": `attachment; filename="${encodeURIComponent(FILE_NM)}"`, - "Content-Length": String(fileBuffer.length), + "Content-Type": result.mimeType || "application/octet-stream", + "Content-Disposition": `attachment; filename="${encodeURIComponent(result.fileName || fileName)}"`, + "Content-Length": String(buffer.length), }, }); } catch (error) { @@ -140,33 +87,4 @@ export async function GET( } } -/** - * MIME 타입 결정 - */ -function getMimeType(fileName: string): string { - const ext = path.extname(fileName).toLowerCase(); - - const mimeTypes: Record<string, string> = { - ".pdf": "application/pdf", - ".doc": "application/msword", - ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - ".xls": "application/vnd.ms-excel", - ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ".ppt": "application/vnd.ms-powerpoint", - ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", - ".txt": "text/plain", - ".csv": "text/csv", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".png": "image/png", - ".gif": "image/gif", - ".zip": "application/zip", - ".rar": "application/x-rar-compressed", - ".7z": "application/x-7z-compressed", - ".dwg": "application/acad", - ".dxf": "application/dxf", - }; - - return mimeTypes[ext] || "application/octet-stream"; -} diff --git a/app/api/swp/upload/route.ts b/app/api/swp/upload/route.ts index b38c4ff4..3e15e0a3 100644 --- a/app/api/swp/upload/route.ts +++ b/app/api/swp/upload/route.ts @@ -1,11 +1,7 @@ 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 설정 @@ -26,21 +22,27 @@ interface InBoxFileInfo { } /** - * 파일명 파싱: [DOC_NO]_[REV_NO]_[STAGE]_[자유-파일명].[확장자] - * 자유 파일명에는 언더스코어가 포함될 수 있음 + * 파일명 파싱: [DOC_NO]_[REV_NO]_[STAGE].[확장자] 또는 [DOC_NO]_[REV_NO]_[STAGE]_[자유-파일명].[확장자] + * 자유 파일명은 선택사항이며, 포함될 경우 언더스코어를 포함할 수 있음 */ 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; + + // 확장자 검증 + if (lastDotIndex === -1) { + throw new Error(`파일 확장자가 없습니다: ${fileName}`); + } + + const extension = fileName.substring(lastDotIndex + 1); + const nameWithoutExt = fileName.substring(0, lastDotIndex); const parts = nameWithoutExt.split("_"); - // 최소 4개 파트 필요: docNo, revNo, stage, fileName - if (parts.length < 4) { + // 최소 3개 파트 필요: docNo, revNo, stage (fileName은 선택사항) + if (parts.length < 3) { throw new Error( `잘못된 파일명 형식입니다: ${fileName}. ` + - `형식: [DOC_NO]_[REV_NO]_[STAGE]_[파일명].확장자 (언더스코어 최소 3개 필요)` + `형식: [DOC_NO]_[REV_NO]_[STAGE].[확장자] (언더스코어 최소 2개 필요)` ); } @@ -49,10 +51,29 @@ function parseFileName(fileName: string) { const revNo = parts[1]; const stage = parts[2]; - // 나머지는 자유 파일명 (언더스코어 포함 가능) - const customFileName = parts.slice(3).join("_"); + // 나머지는 자유 파일명 (선택사항, 언더스코어 포함 가능) + const customFileName = parts.length > 3 ? parts.slice(3).join("_") : ""; + + // 필수 항목이 비어있지 않은지 확인 + if (!ownDocNo || ownDocNo.trim() === "") { + throw new Error(`문서번호(DOC_NO)가 비어있습니다: ${fileName}`); + } + + if (!revNo || revNo.trim() === "") { + throw new Error(`리비전 번호(REV_NO)가 비어있습니다: ${fileName}`); + } + + if (!stage || stage.trim() === "") { + throw new Error(`스테이지(STAGE)가 비어있습니다: ${fileName}`); + } - return { ownDocNo, revNo, stage, fileName: customFileName, extension }; + return { + ownDocNo: ownDocNo.trim(), + revNo: revNo.trim(), + stage: stage.trim(), + fileName: customFileName.trim(), + extension + }; } /** @@ -71,22 +92,43 @@ function generateTimestamp(): string { } /** - * CPY_CD 조회 + * CPY_CD 조회 (API 기반) + * GetVDRDocumentList API를 호출하여 해당 프로젝트/벤더의 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); + try { + console.log(`[getCpyCdForVendor] API 조회 시작: projNo=${projNo}, vndrCd=${vndrCd}`); + + // GetVDRDocumentList API 호출 (벤더 필터 적용) + const documents = await fetchGetVDRDocumentList({ + proj_no: projNo, + doc_gb: "V", + vndrCd: vndrCd, + }); - if (!result || result.length === 0 || !result[0].CPY_CD) { - throw new Error( - `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 해당하는 회사 코드(CPY_CD)를 찾을 수 없습니다.` - ); - } + console.log(`[getCpyCdForVendor] API 조회 완료: ${documents.length}개 문서`); - return result[0].CPY_CD; + if (!documents || documents.length === 0) { + throw new Error( + `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 할당된 문서가 없습니다.` + ); + } + + // 첫 번째 문서에서 CPY_CD 추출 + const cpyCd = documents[0].CPY_CD; + + if (!cpyCd) { + throw new Error( + `프로젝트 ${projNo}에서 벤더 코드 ${vndrCd}에 해당하는 회사 코드(CPY_CD)를 찾을 수 없습니다.` + ); + } + + console.log(`[getCpyCdForVendor] CPY_CD 확인: ${cpyCd}`); + return cpyCd; + } catch (error) { + console.error("[getCpyCdForVendor] 오류:", error); + throw error; + } } /** @@ -308,39 +350,8 @@ export async function POST(request: NextRequest) { 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); - } - } + // ⚠️ Full API 방식으로 전환했으므로 로컬 DB 동기화는 불필요 + // 업로드 성공 시 SaveInBoxList API 호출만으로 충분 (이미 위에서 완료) // 결과 메시지 생성 let message: string; @@ -348,7 +359,7 @@ export async function POST(request: NextRequest) { if (result.failedCount === 0) { success = true; - message = `${result.successCount}개 파일이 성공적으로 업로드 및 동기화되었습니다.`; + message = `${result.successCount}개 파일이 성공적으로 업로드되었습니다.`; } else if (result.successCount === 0) { success = false; message = `모든 파일 업로드에 실패했습니다. (${result.failedCount}개)`; @@ -359,18 +370,13 @@ export async function POST(request: NextRequest) { console.log(`[upload] 완료:`, { success, message, result }); - // 동기화 완료 정보 추가 - const syncCompleted = result.successCount > 0; - const syncTimestamp = new Date().toISOString(); - return NextResponse.json({ success, message, successCount: result.successCount, failedCount: result.failedCount, details: result.details, - syncCompleted, - syncTimestamp, + uploadTimestamp: new Date().toISOString(), affectedVndrCd: vndrCd, }); } catch (error) { |
