'use server'; import { XMLBuilder, XMLParser } from 'fast-xml-parser'; import { withSoapLogging } from '@/lib/soap/utils'; import fs from 'fs/promises'; import path from 'path'; import type { GetEncryptDocumentumFileParams, AccessNfsFileParams, AccessNfsFileResult } from './types'; import { POS_SOAP_ENDPOINT, DOCUMENTUM_NFS_PATH } from './types'; import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils'; /** * 문서 암호화 파일을 서버에 다운로드하고 경로를 반환하는 POS(Documentum) SOAP 액션 * 반환값은 서버 내 파일 다운로드 경로입니다. (예: "asd_as_2509131735233768_OP02\asd_as_2509131735233768_OP02.tif") * * 실제 파일은 DOCUMENTUM_NFS 환경변수로 지정된 NFS 마운트 경로에서 접근할 수 있습니다. * - 환경변수: DOCUMENTUM_NFS (기본값: "/mnt/nfs-documentum/") * - 실제 파일 경로: ${DOCUMENTUM_NFS}/Download/${반환된_상대_경로} * * 예시: * - SOAP 반환값: "asd_as_2509131735233768_OP02\asd_as_2509131735233768_OP02.tif" * - 실제 NFS 파일 경로: "/mnt/nfs-documentum/Download/asd_as_2509131735233768_OP02/asd_as_2509131735233768_OP02.tif" */ export async function getEncryptDocumentumFile( params: GetEncryptDocumentumFileParams ): Promise<{ success: boolean; result?: string; error?: string; }> { try { const { objectID, sabun = 'EVM0236', // context2.txt에 따라 기본값 설정 appCode = process.env.POS_APP_CODE || 'SO13', // 환경변수 사용 fileCreateMode = 1, // context2.txt에 따라 기본값 변경 securityLevel = 'SedamsClassID', isDesign = true, // context2.txt에 따라 기본값 변경 } = params; debugLog(`🌐 POS SOAP API 호출 시작`, { objectID, sabun, appCode, fileCreateMode, securityLevel, isDesign, endpoint: POS_SOAP_ENDPOINT }); // 1. SOAP Envelope 생성 (SOAP 1.2) debugProcess(`📄 SOAP Envelope 생성 중...`); const envelopeObj = { 'soap12:Envelope': { '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', '@_xmlns:xsd': 'http://www.w3.org/2001/XMLSchema', '@_xmlns:soap12': 'http://www.w3.org/2003/05/soap-envelope', 'soap12:Body': { GetEncryptDocumentumFile: { '@_xmlns': 'Sedams.WebServices.Documentum', objectID, sabun, appCode, fileCreateMode: String(fileCreateMode), securityLevel, isDesign: String(isDesign), }, }, }, }; const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, suppressEmptyNode: true, }); const xmlBody = builder.build(envelopeObj); debugLog(`📄 생성된 SOAP XML`, { objectID, xmlLength: xmlBody.length, envelope: envelopeObj['soap12:Envelope']['soap12:Body'] }); // 2. SOAP 호출 (로그 포함) debugProcess(`🌐 SOAP 요청 전송 중... (${POS_SOAP_ENDPOINT})`); const responseText = await withSoapLogging( 'OUTBOUND', 'SEDAMS Documentum', 'GetEncryptDocumentumFile', xmlBody, async () => { // SOAP 1.1 방식으로 송신 const res = await fetch(POS_SOAP_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '"Sedams.WebServices.Documentum/GetEncryptDocumentumFile"', }, body: xmlBody, }); const text = await res.text(); debugLog(`🌐 HTTP 응답 수신`, { objectID, status: res.status, statusText: res.statusText, responseLength: text.length, headers: Object.fromEntries(res.headers.entries()) }); if (!res.ok) { debugError(`❌ HTTP 오류 응답`, { objectID, status: res.status, statusText: res.statusText, responseBody: text }); throw new Error(`HTTP ${res.status}: ${res.statusText}`); } return text; } ); debugSuccess(`✅ SOAP 응답 수신 완료 (응답 길이: ${responseText.length})`); // 3. 응답 XML 파싱 debugProcess(`📝 XML 응답 파싱 중...`); const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', }); const parsed = parser.parse(responseText); debugLog(`📝 파싱된 XML 구조`, { objectID, parsedKeys: Object.keys(parsed), parsed: parsed }); let result: string | undefined; try { // SOAP 1.2 규격 debugProcess(`🔍 SOAP 1.2 구조에서 결과 추출 시도...`); result = parsed['soap:Envelope']['soap:Body'][ 'GetEncryptDocumentumFileResponse' ]['GetEncryptDocumentumFileResult']; debugLog(`🎯 SOAP 1.2에서 결과 추출 성공`, { objectID, result }); } catch (e1) { debugLog(`⚠️ SOAP 1.2 구조 추출 실패, Fallback 시도...`, { objectID, error: e1 }); // Fallback: SOAP 1.1 또는 다른 응답 형태 try { debugProcess(`🔍 SOAP 1.1 구조에서 결과 추출 시도...`); result = parsed['soap:Envelope']['soap:Body'][ 'GetEncryptDocumentumFileResponse' ]['GetEncryptDocumentumFileResult']; debugLog(`🎯 SOAP 1.1에서 결과 추출 성공`, { objectID, result }); } catch (e2) { debugLog(`⚠️ SOAP 1.1 구조 추출 실패, 단순 string 시도...`, { objectID, error: e2 }); // HTTP POST 응답 형태 (단순 string) try { debugProcess(`🔍 단순 string 구조에서 결과 추출 시도...`); result = parsed.string; debugLog(`🎯 단순 string에서 결과 추출 성공`, { objectID, result }); } catch (e3) { debugError(`❌ 모든 파싱 방법 실패`, { objectID, errors: [e1, e2, e3], parsedStructure: parsed }); } } } debugSuccess(`✅ POS API 호출 성공`, { objectID, result }); return { success: true, result }; } catch (error) { debugError('❌ POS SOAP 호출 실패', { objectID: params.objectID, error: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : undefined }); return { success: false, error: error instanceof Error ? error.message : 'Unknown error', }; } } /** * NFS 마운트를 통해 POS 파일에 직접 접근하는 함수 * POS API에서 반환된 상대 경로를 사용하여 NFS 마운트된 경로에서 파일을 읽습니다 */ export async function accessNfsFile( params: AccessNfsFileParams ): Promise { try { const { relativePath } = params; debugLog(`📁 NFS를 통한 POS 파일 접근 시작`, { relativePath, nfsBasePath: DOCUMENTUM_NFS_PATH }); if (!relativePath) { debugError(`❌ 파일 경로가 제공되지 않음`); return { success: false, error: '파일 경로가 제공되지 않았습니다.', }; } // Windows 경로 구분자를 Unix 스타일로 정규화 const normalizedRelativePath = relativePath.replace(/\\/g, '/'); debugLog(`📁 경로 정규화`, { original: relativePath, normalized: normalizedRelativePath }); // NFS 마운트 경로와 상대 경로를 결합 // NFS 마운트 경로(/mnt/nfs-documentum)가 이미 Download 폴더 내용을 포함하므로 Download 추가하지 않음 const fullPath = path.posix.join(DOCUMENTUM_NFS_PATH, normalizedRelativePath); debugLog(`📍 NFS 전체 파일 경로 구성`, { fullPath }); // 경로 보안 검증 (디렉토리 탈출 방지) const resolvedPath = path.resolve(fullPath); const resolvedBasePath = path.resolve(DOCUMENTUM_NFS_PATH); debugLog(`🔒 경로 보안 검증`, { resolvedPath, resolvedBasePath, isValid: resolvedPath.startsWith(resolvedBasePath) }); if (!resolvedPath.startsWith(resolvedBasePath)) { debugError(`❌ 디렉토리 탈출 시도 감지`, { resolvedPath, resolvedBasePath }); return { success: false, error: '잘못된 파일 경로입니다.', }; } // 파일 존재 여부 확인 debugProcess(`🔍 NFS 파일 존재 여부 확인 중...`); try { await fs.access(resolvedPath); debugSuccess(`✅ NFS 파일 접근 가능 확인`); } catch (accessError) { debugError(`❌ NFS 파일 접근 불가`, { path: resolvedPath, error: accessError instanceof Error ? accessError.message : 'Unknown error' }); return { success: false, error: `파일을 찾을 수 없습니다: ${resolvedPath}`, }; } // 파일 정보 확인 debugProcess(`📊 NFS 파일 정보 확인 중...`); const stats = await fs.stat(resolvedPath); debugLog(`📊 NFS 파일 정보`, { size: stats.size, isFile: stats.isFile(), modified: stats.mtime }); if (!stats.isFile()) { debugError(`❌ 대상이 파일이 아님`, { path: resolvedPath }); return { success: false, error: '대상이 파일이 아닙니다.', }; } // 파일 크기 제한 (10000MB) const maxSize = 10000 * 1024 * 1024; // 10000MB if (stats.size > maxSize) { debugError(`❌ 파일 크기 초과`, { fileSize: stats.size, maxSize, path: resolvedPath }); return { success: false, error: `파일 크기가 너무 큽니다 (최대 10000MB). 현재 크기: ${Math.round(stats.size / 1024 / 1024)}MB`, }; } // 파일 읽기 debugProcess(`📖 NFS 파일 읽기 중... (크기: ${stats.size} bytes)`); const fileBuffer = await fs.readFile(resolvedPath); debugSuccess(`✅ NFS 파일 읽기 완료 (${fileBuffer.length} bytes)`); // 파일명과 MIME 타입 추출 const fileName = path.basename(resolvedPath); const fileExtension = path.extname(fileName).toLowerCase(); // 확장자별 MIME 타입 설정 let mimeType = 'application/octet-stream'; // 기본값 switch (fileExtension) { case '.pdf': mimeType = 'application/pdf'; break; case '.tif': case '.tiff': mimeType = 'image/tiff'; break; case '.png': mimeType = 'image/png'; break; case '.jpg': case '.jpeg': mimeType = 'image/jpeg'; break; case '.doc': mimeType = 'application/msword'; break; case '.docx': mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; break; case '.xls': mimeType = 'application/vnd.ms-excel'; break; case '.xlsx': mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; break; case '.dwg': mimeType = 'application/acad'; break; case '.zip': mimeType = 'application/zip'; break; } debugSuccess(`✅ NFS를 통한 POS 파일 접근 성공`, { fileName, mimeType, fileSize: fileBuffer.length, fullPath: resolvedPath }); return { success: true, fileName, fileBuffer, mimeType, fullPath: resolvedPath, }; } catch (error) { debugError('❌ NFS를 통한 POS 파일 접근 실패', { relativePath: params.relativePath, error: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : undefined }); return { success: false, error: error instanceof Error ? error.message : 'Unknown error', }; } } /** * POS 파일 접근 방법을 테스트하는 함수 * 개발/테스트 환경에서 NFS 마운트가 제대로 작동하는지 확인하는 용도 */ export async function testNfsAccess(): Promise<{ success: boolean; nfsPath: string; accessible: boolean; error?: string; }> { try { debugLog(`🧪 NFS 접근 테스트 시작`, { nfsPath: DOCUMENTUM_NFS_PATH }); // NFS 마운트 기본 경로 접근 테스트 // NFS 마운트 경로(/mnt/nfs-documentum)가 이미 Download 폴더 내용을 포함하므로 Download 추가하지 않음 const testPath = DOCUMENTUM_NFS_PATH; debugLog(`🔍 테스트 경로 확인`, { testPath }); try { await fs.access(testPath); debugSuccess(`✅ NFS 마운트 경로 접근 성공`, { testPath }); return { success: true, nfsPath: DOCUMENTUM_NFS_PATH, accessible: true, }; } catch (accessError) { debugError(`❌ NFS 마운트 경로 접근 실패`, { testPath, error: accessError instanceof Error ? accessError.message : 'Unknown error' }); return { success: false, nfsPath: DOCUMENTUM_NFS_PATH, accessible: false, error: `NFS 마운트 경로에 접근할 수 없습니다: ${testPath}`, }; } } catch (error) { debugError('❌ NFS 접근 테스트 실패', { error: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : undefined }); return { success: false, nfsPath: DOCUMENTUM_NFS_PATH, accessible: false, error: error instanceof Error ? error.message : 'Unknown error', }; } }