diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/api/pos/download-on-demand/route.ts | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/app/api/pos/download-on-demand/route.ts b/app/api/pos/download-on-demand/route.ts new file mode 100644 index 00000000..f88e8858 --- /dev/null +++ b/app/api/pos/download-on-demand/route.ts @@ -0,0 +1,196 @@ +/** + * 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<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; 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 } + ); + } +} + |
