/** * POS 파일 온디맨드 다운로드 API * * 자재코드(MATNR)로 POS 파일을 찾아서 NFS에서 직접 스트리밍합니다. * 서버 스토리지에 저장하지 않고 실시간으로 다운로드합니다. */ import { NextRequest, NextResponse } from 'next/server'; import { getDcmtmIdByMaterialCode, getEncryptDocumentumFile, downloadPosFile } from '@/lib/pos'; // 허용된 파일 확장자 const ALLOWED_EXTENSIONS = new Set([ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'dwg', 'dxf', 'zip', 'rar', '7z' ]); // 최대 파일 크기 (10240MB) const MAX_FILE_SIZE = 10240 * 1024 * 1024; // 파일 확장자 검증 function validateFileExtension(fileName: string): boolean { const extension = fileName.split('.').pop()?.toLowerCase() || ''; return ALLOWED_EXTENSIONS.has(extension); } // MIME 타입 결정 function getMimeType(fileName: string): string { const fileExtension = fileName.split('.').pop()?.toLowerCase() || ''; const mimeTypes: Record = { '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; charset=utf-8', 'csv': 'text/csv; charset=utf-8', 'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'bmp': 'image/bmp', 'svg': 'image/svg+xml', 'dwg': 'application/acad', 'dxf': 'application/dxf', 'zip': 'application/zip', 'rar': 'application/x-rar-compressed', '7z': 'application/x-7z-compressed', }; return mimeTypes[fileExtension] || 'application/octet-stream'; } export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams; const materialCode = searchParams.get('materialCode'); const fileIndex = parseInt(searchParams.get('fileIndex') || '0'); // 파라미터 검증 if (!materialCode || materialCode.trim() === '') { return NextResponse.json( { error: '자재코드(materialCode)가 제공되지 않았습니다.' }, { status: 400 } ); } console.log(`🔍 POS 파일 온디맨드 다운로드 시작 (자재코드: ${materialCode})`); // 1. 자재코드로 DCMTM_ID 조회 console.log(`📋 DCMTM_ID 조회 중... (자재코드: ${materialCode})`); const dcmtmResult = await getDcmtmIdByMaterialCode({ materialCode }); if (!dcmtmResult.success || !dcmtmResult.files || dcmtmResult.files.length === 0) { console.warn(`⚠️ POS 파일을 찾을 수 없음 (자재코드: ${materialCode})`); return NextResponse.json( { error: dcmtmResult.error || '해당 자재코드에 대한 POS 파일을 찾을 수 없습니다.', materialCode }, { status: 404 } ); } // 파일 인덱스 범위 검증 if (fileIndex >= dcmtmResult.files.length) { return NextResponse.json( { error: `파일 인덱스가 범위를 벗어났습니다. 사용 가능한 파일 수: ${dcmtmResult.files.length}`, availableFilesCount: dcmtmResult.files.length }, { status: 400 } ); } const posFile = dcmtmResult.files[fileIndex]; console.log(`✅ DCMTM_ID 조회 완료:`, { materialCode, dcmtmId: posFile.dcmtmId, fileName: posFile.fileName, totalFiles: dcmtmResult.files.length }); // 2. POS API로 파일 경로 가져오기 console.log(`🌐 POS API 호출 중... (DCMTM_ID: ${posFile.dcmtmId})`); const posResult = await getEncryptDocumentumFile({ objectID: posFile.dcmtmId }); if (!posResult.success || !posResult.result) { console.error(`❌ POS API 호출 실패:`, posResult.error); return NextResponse.json( { error: posResult.error || 'POS 파일 경로를 가져올 수 없습니다.', materialCode, dcmtmId: posFile.dcmtmId }, { status: 500 } ); } console.log(`✅ POS API 호출 완료 (경로: ${posResult.result})`); // 3. NFS에서 파일 다운로드 console.log(`⬇️ 파일 다운로드 중... (NFS 경로: ${posResult.result})`); const downloadResult = await downloadPosFile({ relativePath: posResult.result }); if (!downloadResult.success || !downloadResult.fileBuffer) { console.error(`❌ 파일 다운로드 실패:`, downloadResult.error); return NextResponse.json( { error: downloadResult.error || '파일 다운로드에 실패했습니다.', materialCode, nfsPath: posResult.result }, { status: 500 } ); } const fileName = downloadResult.fileName || posFile.fileName; const fileBuffer = downloadResult.fileBuffer; console.log(`✅ 파일 다운로드 완료 (크기: ${fileBuffer.length} bytes)`); // 파일 확장자 검증 if (!validateFileExtension(fileName)) { console.warn(`🚨 허용되지 않은 파일 확장자: ${fileName}`); return NextResponse.json( { error: '지원하지 않는 파일 형식입니다.', fileName }, { status: 403 } ); } // 파일 크기 검증 if (fileBuffer.length > MAX_FILE_SIZE) { console.warn(`🚨 파일 크기 초과: ${fileBuffer.length} bytes`); return NextResponse.json( { error: '파일 크기가 너무 큽니다.', fileSize: fileBuffer.length }, { status: 413 } ); } // MIME 타입 결정 const contentType = downloadResult.mimeType || getMimeType(fileName); // 파일 스트리밍 응답 생성 const response = new NextResponse(fileBuffer); response.headers.set('Content-Type', contentType); response.headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`); response.headers.set('Content-Length', fileBuffer.length.toString()); // 보안 헤더 response.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); response.headers.set('Pragma', 'no-cache'); response.headers.set('Expires', '0'); response.headers.set('X-Content-Type-Options', 'nosniff'); console.log(`✅ POS 파일 다운로드 성공: ${fileName} (${fileBuffer.length} bytes)`); return response; } catch (error) { console.error('❌ POS 파일 온디맨드 다운로드 API 오류:', error); return NextResponse.json( { error: '서버 내부 오류가 발생했습니다.', details: error instanceof Error ? error.message : '알 수 없는 오류' }, { status: 500 } ); } }