summaryrefslogtreecommitdiff
path: root/app/api/rfq
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-14 05:28:01 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-14 05:28:01 +0000
commit675b4e3d8ffcb57a041db285417d81e61284d900 (patch)
tree254f3d6a6c0ce39ae8fba35618f3810e08945f19 /app/api/rfq
parent39f12cb19f29cbc5568057e154e6adf4789ae736 (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.ts179
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