diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-04 17:30:27 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-04 17:30:27 +0900 |
| commit | a1710296dbc1881a7ed86093872904a529901430 (patch) | |
| tree | 62e4be5dabba4bf7c337769f391d11ce59154cc4 /app | |
| parent | 680da9b323db8b8d7cf27c674ab0016ec87bfe81 (diff) | |
(김준회) RFQ 테이블 POS 다운로드할 수 있도록 변경, 벤더측은 다운로드시 암호화 해제 추가, item dialog 통일처리
Diffstat (limited to 'app')
| -rw-r--r-- | app/api/pos/download-on-demand-partners/route.ts | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/app/api/pos/download-on-demand-partners/route.ts b/app/api/pos/download-on-demand-partners/route.ts new file mode 100644 index 00000000..d2941537 --- /dev/null +++ b/app/api/pos/download-on-demand-partners/route.ts @@ -0,0 +1,243 @@ +'use server'; + +/** + * POS 파일 온디맨드 다운로드 API (Partners용 - 복호화 포함) + * + * 자재코드(MATNR)로 POS 파일을 찾아서 NFS에서 다운로드 후 복호화하여 제공합니다. + * EVCP 직원용과 달리 협력사는 복호화된 파일을 다운로드합니다. + */ + +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'; +} + +/** + * DRM 복호화 함수 + * Spring Boot DRM Proxy 서버를 통해 파일 복호화 + */ +async function decryptBuffer(buffer: Buffer, fileName: string): Promise<Buffer> { + try { + console.log(`🔐 DRM 복호화 시작: ${fileName} (크기: ${buffer.length} bytes)`); + + // 파일을 Blob/File로 변환 + const blob = new Blob([buffer]); + const file = new File([blob], fileName); + + const formData = new FormData(); + formData.append('file', file); + + // DRM Proxy 서버 엔드포인트 + const backendUrl = process.env.DRM_PROXY_URL || "http://localhost:6543/api/drm-proxy/decrypt"; + + const response = await fetch(backendUrl, { + method: "POST", + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => '응답 텍스트를 가져올 수 없음'); + throw new Error(`DRM 서버 응답 오류 [${response.status}]: ${errorText}`); + } + + const arrayBuffer = await response.arrayBuffer(); + const decryptedBuffer = Buffer.from(arrayBuffer); + + console.log(`✅ DRM 복호화 완료: ${fileName} (결과 크기: ${decryptedBuffer.length} bytes)`); + + return decryptedBuffer; + } catch (error) { + console.error(`❌ DRM 복호화 실패: ${fileName}`, error); + // 복호화 실패 시 원본 반환 (폴백) + console.warn(`⚠️ 복호화 실패로 원본 파일 반환: ${fileName}`); + return buffer; + } +} + +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 파일 온디맨드 다운로드 시작 [Partners - 복호화] (자재코드: ${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; + let 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 } + ); + } + + // 4. DRM 복호화 (Partners용 추가 로직) + fileBuffer = await decryptBuffer(fileBuffer, fileName); + + // 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 파일 다운로드 성공 [Partners - 복호화됨]: ${fileName} (${fileBuffer.length} bytes)`); + + return response; + } catch (error) { + console.error('❌ POS 파일 온디맨드 다운로드 API 오류 [Partners]:', error); + return NextResponse.json( + { + error: '서버 내부 오류가 발생했습니다.', + details: error instanceof Error ? error.message : '알 수 없는 오류' + }, + { status: 500 } + ); + } +} + |
