// /app/api/vendors/attachments/download-all/route.js import { NextResponse,NextRequest } from 'next/server'; import fs from 'fs'; import path from 'path'; import JSZip from 'jszip'; import db from '@/db/db'; import { eq } from 'drizzle-orm'; import { vendorAttachments, vendors } from '@/db/schema'; export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); const vendorId = searchParams.get('vendorId'); if (!vendorId) { return NextResponse.json( { error: "필수 파라미터가 누락되었습니다." }, { status: 400 } ); } // 협력업체 정보 조회 const vendor = await db.query.vendors.findFirst({ where: eq(vendors.id, parseInt(vendorId, 10)) }); if (!vendor) { return NextResponse.json( { error: `협력업체 정보를 찾을 수 없습니다. (ID: ${vendorId})` }, { status: 404 } ); } // 첨부파일 조회 const attachments = await db.select() .from(vendorAttachments) .where(eq(vendorAttachments.vendorId, parseInt(vendorId, 10))); if (!attachments.length) { return NextResponse.json( { error: '다운로드할 첨부파일이 없습니다.' }, { status: 404 } ); } // 환경에 따른 기본 경로 설정 (/api/files/ 와 동일한 로직) const nasPath = process.env.NAS_PATH || "/evcp_nas"; const basePath = process.env.NODE_ENV === 'production' ? nasPath : path.join(process.cwd(), 'public'); // ZIP 생성 const zip = new JSZip(); // 파일 읽기 및 ZIP에 추가 await Promise.all( attachments.map(async (attachment) => { try { // 필수 필드 검증 if (!attachment.filePath || !attachment.fileName) { console.warn(`첨부파일 정보가 불완전합니다:`, attachment); return; } // DB에 저장된 경로가 /api/files/vendors/[id]/[filename] 형태인 경우 처리 let normalizedPath: string; if (attachment.filePath.startsWith('/api/files/')) { // /api/files/vendors/[id]/[filename] -> vendors/[id]/[filename] normalizedPath = attachment.filePath.replace('/api/files/', ''); } else if (attachment.filePath.startsWith('/')) { // 기존 방식: /vendors/[id]/[filename] -> vendors/[id]/[filename] normalizedPath = attachment.filePath.slice(1); } else { // 상대 경로 그대로 사용 normalizedPath = attachment.filePath; } // 보안 검증: 경로 탐색 공격 방지 if (normalizedPath.includes('..') || normalizedPath.includes('~')) { console.warn(`위험한 경로 패턴: ${normalizedPath}`); return; } const filePath = path.join(basePath, normalizedPath); console.log(`파일 경로 확인: ${attachment.filePath} -> ${filePath}`); // 파일 존재 확인 try { await fs.promises.access(filePath, fs.constants.F_OK); } catch { console.warn(`파일이 존재하지 않습니다: ${filePath}`); return; // 파일이 없으면 건너뜀 } // 파일 읽기 const fileData = await fs.promises.readFile(filePath); // ZIP에 파일 추가 (파일명 중복 방지) const safeFileName = `${attachment.id}_${attachment.fileName}`; zip.file(safeFileName, fileData); console.log(`ZIP에 파일 추가됨: ${safeFileName}`); } catch (error) { console.warn(`파일을 처리할 수 없습니다: ${attachment.filePath}`, error); // 오류가 있더라도 계속 진행 } }) ); // ZIP에 추가된 파일이 있는지 확인 const zipFiles = Object.keys(zip.files); if (zipFiles.length === 0) { return NextResponse.json( { error: '다운로드 가능한 파일이 없습니다.' }, { status: 404 } ); } console.log(`ZIP 파일 생성 시작: ${zipFiles.length}개 파일`); // ZIP 생성 const zipContent = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE', compressionOptions: { level: 9 } }); // 파일명 생성 const fileName = `${vendor.vendorName || `vendor-${vendorId}`}-attachments.zip`; // 응답 헤더 설정 const headers = new Headers(); headers.set('Content-Disposition', `attachment; filename="${fileName}"`); headers.set('Content-Type', 'application/zip'); headers.set('Content-Length', zipContent.length.toString()); // ZIP 파일 데이터와 함께 응답 return new Response(zipContent, { status: 200, headers }); } catch (error) { console.error('첨부파일 다운로드 오류:', error); return NextResponse.json( { error: "첨부파일 다운로드 준비 중 오류가 발생했습니다." }, { status: 500 } ); } }