summaryrefslogtreecommitdiff
path: root/app/api/vendors/attachments/download-all/route.ts
blob: a1e56bba12e86df2d71061187d87c758ea96143a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// /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 }
    );
  }
}