'use server'; import fs from 'fs/promises'; import path from 'path'; import type { DownloadPosFileParams, DownloadPosFileResult } from './types'; import { DOCUMENTUM_NFS_PATH } from './types'; import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils'; // 레거시: Windows 네트워크 경로 (하위 호환성을 위해 유지) const POS_BASE_PATH_LEGACY = '\\\\60.100.99.123\\ECM_NAS_PRM\\Download'; // NFS 마운트 경로 사용 const POS_BASE_PATH = path.posix.join(DOCUMENTUM_NFS_PATH, 'Download'); /** * POS API에서 반환된 경로의 파일을 NFS 마운트를 통해 읽어서 클라이언트에게 전달 * NFS 마운트된 Documentum 저장소에서 파일을 직접 접근하여 제공 */ export async function downloadPosFile( params: DownloadPosFileParams ): Promise { try { const { relativePath } = params; debugLog(`⬇️ POS 파일 다운로드 시작 (NFS)`, { relativePath, nfsBasePath: DOCUMENTUM_NFS_PATH, basePath: POS_BASE_PATH }); if (!relativePath) { debugError(`❌ 파일 경로가 제공되지 않음`); return { success: false, error: '파일 경로가 제공되지 않았습니다.', }; } // Windows 경로 구분자를 Unix 스타일로 정규화 (NFS용) const normalizedRelativePath = relativePath.replace(/\\/g, '/'); debugLog(`📁 경로 정규화 (NFS)`, { original: relativePath, normalized: normalizedRelativePath }); // NFS 마운트 경로와 상대 경로를 결합 const fullPath = path.posix.join(POS_BASE_PATH, normalizedRelativePath); debugLog(`📍 NFS 전체 파일 경로 구성`, { fullPath }); // 경로 보안 검증 (디렉토리 탈출 방지) const resolvedPath = path.resolve(fullPath); const resolvedBasePath = path.resolve(DOCUMENTUM_NFS_PATH); debugLog(`🔒 경로 보안 검증 (NFS)`, { resolvedPath, resolvedBasePath, isValid: resolvedPath.startsWith(resolvedBasePath) }); if (!resolvedPath.startsWith(resolvedBasePath)) { debugError(`❌ 디렉토리 탈출 시도 감지 (NFS)`, { resolvedPath, resolvedBasePath }); return { success: false, error: '잘못된 파일 경로입니다.', }; } // 파일 존재 여부 확인 debugProcess(`🔍 NFS 파일 존재 여부 확인 중...`); try { await fs.access(resolvedPath); debugSuccess(`✅ 파일 존재 확인됨 (${resolvedPath})`); } catch (accessError) { debugError(`❌ 파일을 찾을 수 없음`, { resolvedPath, error: accessError }); return { success: false, error: '파일을 찾을 수 없습니다.', }; } // 파일 정보 가져오기 debugProcess(`📊 파일 정보 조회 중...`); const stats = await fs.stat(resolvedPath); debugLog(`📊 파일 정보`, { resolvedPath, size: stats.size, isFile: stats.isFile(), isDirectory: stats.isDirectory(), mtime: stats.mtime }); if (!stats.isFile()) { debugError(`❌ 대상이 파일이 아님 (디렉토리)`, { resolvedPath }); return { success: false, error: '디렉토리는 다운로드할 수 없습니다.', }; } // 파일 읽기 debugProcess(`📖 파일 읽기 시작 (크기: ${stats.size} bytes)...`); const fileBuffer = await fs.readFile(resolvedPath); const fileName = path.basename(resolvedPath); debugLog(`📖 파일 읽기 완료`, { fileName, bufferSize: fileBuffer.length, expectedSize: stats.size, sizeMatch: fileBuffer.length === stats.size }); // MIME 타입 추정 debugProcess(`🔍 MIME 타입 추정 중...`); const mimeType = await getMimeType(fileName); debugLog(`🔍 MIME 타입 추정 완료`, { fileName, mimeType }); debugSuccess(`✅ NFS를 통한 POS 파일 다운로드 성공`, { fileName, fileSize: fileBuffer.length, mimeType, nfsPath: resolvedPath }); return { success: true, fileName, fileBuffer, mimeType, }; } catch (error) { debugError('❌ NFS를 통한 POS 파일 다운로드 실패', { relativePath: params.relativePath, nfsBasePath: DOCUMENTUM_NFS_PATH, error: error instanceof Error ? error.message : '알 수 없는 오류', stack: error instanceof Error ? error.stack : undefined }); return { success: false, error: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.', }; } } /** * 파일 확장자를 기반으로 MIME 타입 추정 */ export async function getMimeType(fileName: string): Promise { const ext = path.extname(fileName).toLowerCase(); const mimeTypes: Record = { '.pdf': 'application/pdf', '.tif': 'image/tiff', '.tiff': 'image/tiff', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.bmp': 'image/bmp', '.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', '.dwg': 'application/acad', '.dxf': 'application/dxf', '.zip': 'application/zip', '.rar': 'application/x-rar-compressed', '.7z': 'application/x-7z-compressed', '.txt': 'text/plain', }; return mimeTypes[ext] || 'application/octet-stream'; } /** * 클라이언트에서 사용할 수 있는 다운로드 URL을 생성하는 헬퍼 함수 * 실제 다운로드는 별도의 API 엔드포인트에서 처리 */ export async function createDownloadUrl(relativePath: string): Promise { const encodedPath = encodeURIComponent(relativePath); return `/api/pos/download?path=${encodedPath}`; }