diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 05:28:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-09-14 05:28:01 +0000 |
| commit | 675b4e3d8ffcb57a041db285417d81e61284d900 (patch) | |
| tree | 254f3d6a6c0ce39ae8fba35618f3810e08945f19 /app/api/rfq | |
| parent | 39f12cb19f29cbc5568057e154e6adf4789ae736 (diff) | |
(대표님) RFQ-last, tbe-last, 기본계약 템플릿 내 견적,입찰,계약 추가, env.dev NAS_PATH 수정
Diffstat (limited to 'app/api/rfq')
| -rw-r--r-- | app/api/rfq/attachments/download-all/route.ts | 179 |
1 files changed, 179 insertions, 0 deletions
diff --git a/app/api/rfq/attachments/download-all/route.ts b/app/api/rfq/attachments/download-all/route.ts new file mode 100644 index 00000000..0ff6a071 --- /dev/null +++ b/app/api/rfq/attachments/download-all/route.ts @@ -0,0 +1,179 @@ +import { NextRequest, NextResponse } from 'next/server'; +import db from '@/db/db'; +import { sql } from 'drizzle-orm'; +import JSZip from 'jszip'; +import fs from 'fs/promises'; +import path from 'path'; + +export async function POST(request: NextRequest) { + try { + // 1. 요청 데이터 파싱 + const body = await request.json(); + const { rfqId, files } = body; + + // 2. 입력값 검증 + if (!rfqId || !Array.isArray(files) || files.length === 0) { + return NextResponse.json( + { error: '유효하지 않은 요청입니다' }, + { status: 400 } + ); + } + + // 파일 개수 제한 (보안) + if (files.length > 100) { + return NextResponse.json( + { error: '한 번에 다운로드할 수 있는 최대 파일 수는 100개입니다' }, + { status: 400 } + ); + } + + // 3. DB에서 RFQ 정보 확인 (권한 검증용) + const rfqResult = await db.execute(sql` + SELECT rfq_code, rfq_title + FROM rfqs_last_view + WHERE id = ${rfqId} + `); + + if (rfqResult.rows.length === 0) { + return NextResponse.json( + { error: 'RFQ를 찾을 수 없습니다' }, + { status: 404 } + ); + } + + const rfqInfo = rfqResult.rows[0] as any; + + // 4. NAS 경로 설정 + const nasPath = process.env.NAS_PATH; + if (!nasPath) { + console.error('NAS_PATH 환경 변수가 설정되지 않았습니다'); + return NextResponse.json( + { error: '서버 설정 오류' }, + { status: 500 } + ); + } + + // 5. ZIP 객체 생성 + const zip = new JSZip(); + let addedFiles = 0; + const errors: string[] = []; + + // 6. 파일들을 ZIP에 추가 + for (const file of files) { + if (!file.path || !file.name) { + errors.push(`잘못된 파일 정보: ${file.name || 'unknown'}`); + continue; + } + + try { + // 경로 정규화 및 보안 검증 + const filePath = file.path.startsWith('/') + ? path.join(nasPath, file.path) + : path.join(nasPath, '/', file.path); + + // 경로 탐색 공격 방지 + // const normalizedPath = path.normalize(filePath); + // if (!normalizedPath.startsWith(nasPath)) { + // console.error(`보안 위반: 허용되지 않은 경로 접근 시도 - ${file.path}`); + // errors.push(`보안 오류: ${file.name}`); + // continue; + // } + + // 파일 읽기 + const fileContent = await fs.readFile(filePath); + + // 중복 파일명 처리 + let fileName = file.name; + let counter = 1; + while (zip.file(fileName)) { + const ext = path.extname(file.name); + const baseName = path.basename(file.name, ext); + fileName = `${baseName}_${counter}${ext}`; + counter++; + } + + // ZIP에 파일 추가 + zip.file(fileName, fileContent, { + binary: true, + createFolders: false, + date: new Date(), + comment: `RFQ: ${rfqId}` + }); + + addedFiles++; + + } catch (error) { + console.error(`파일 처리 실패: ${file.name}`, error); + errors.push(`파일 읽기 실패: ${file.name}`); + } + } + + // 7. 추가된 파일이 없으면 오류 + if (addedFiles === 0) { + return NextResponse.json( + { + error: '다운로드할 수 있는 파일이 없습니다', + details: errors + }, + { status: 404 } + ); + } + + // 8. ZIP 파일 생성 (압축 레벨 설정) + const zipBuffer = await zip.generateAsync({ + type: 'nodebuffer', + compression: 'DEFLATE', + compressionOptions: { + level: 9 // 최대 압축 + }, + comment: `RFQ ${rfqInfo.rfq_code} 첨부파일`, + platform: 'UNIX' // 파일 권한 보존 + }); + + // 9. 파일명 생성 + const fileName = `RFQ_${rfqInfo.rfq_code}_attachments_${new Date().toISOString().slice(0, 10)}.zip`; + + // 10. 로그 기록 + console.log(`ZIP 생성 완료: ${fileName}, 파일 수: ${addedFiles}/${files.length}, 크기: ${(zipBuffer.length / 1024).toFixed(2)}KB`); + + if (errors.length > 0) { + console.warn('처리 중 오류 발생:', errors); + } + + // 11. 응답 반환 + return new NextResponse(zipBuffer, { + status: 200, + headers: { + 'Content-Type': 'application/zip', + 'Content-Disposition': `attachment; filename="${fileName}"`, + 'Content-Length': zipBuffer.length.toString(), + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'X-Files-Count': addedFiles.toString(), + 'X-Total-Files': files.length.toString(), + 'X-Errors-Count': errors.length.toString(), + }, + }); + + } catch (error) { + console.error('전체 다운로드 오류:', error); + return NextResponse.json( + { + error: '파일 다운로드 중 오류가 발생했습니다', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} + +// OPTIONS 요청 처리 (CORS) +export async function OPTIONS(request: NextRequest) { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +}
\ No newline at end of file |
