포시코딩

[NestJS] How can I respond with a file and delete with interceptor 본문

JavaScript

[NestJS] How can I respond with a file and delete with interceptor

포시 2024. 2. 27. 11:09
728x90

https://4sii.tistory.com/691

 

[NestJS] JSON to Excel

npm install xlsx fs import * as fs from 'fs'; import * as XLSX from 'xlsx'; // ... 생략 jsonToExcel(res) { const originalJSON = { "AD": { "name": "Andorra", "phone": "376", "flag": "🇦🇩" }, "AE": { "name": "United Arab Emirates", "phone": "97", "fla

4sii.tistory.com

 

이번 포스팅에선

 

1. DB에서 typeorm을 통해 데이터 stream 받기

2. 데이터 엑셀 파일로 임시 저장 (위 링크 참고)

3. 엑셀 파일 요청한 클라이언트로 전달

4. 전달 후 interceptor를 통해 서버에 저장된 임시 엑셀 파일 제거

 

위 네 가지 과정에 대해 알아보겠다. 

 

excel.service

import { Injectable } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { join } from 'path';
import * as fs from 'fs';

@Injectable()
export class ExcelService {
  async createExcelFile(prefix: string, data: Array<string | Buffer>) {
    // 파일 작명
    const currentDate = new Date();
    const formattedDate = currentDate.toISOString().slice(0, 10).replace(/-/g, '');
    const filename = `${prefix}_${formattedDate}.xlsx`;

    // temp 임시 폴더 없다면 생성, 있다면 무시
    fs.mkdirSync(join(process.cwd(), `temp`), { recursive: true });
    const filePath = join(process.cwd(), `temp/${filename}`);

    // filePath 위치에 엑셀 다운로드
    const wb = XLSX.utils.book_new();
    const newWorksheet = XLSX.utils.json_to_sheet(data);
    XLSX.utils.book_append_sheet(wb, newWorksheet, 'Sheet1');
    const wbOptions: any = { bookType: 'xlsx', type: 'binary' };
    XLSX.writeFile(wb, filePath, wbOptions);
    
    return { filename, filePath };
  }
}

 

target.service

async getTargetsByExcel() {
    // typeorm stream()을 통해 데이터 받기
    const queryStream = await this.targetRepository
      .createQueryBuilder('t')
      .select(*)
      .stream();

    let data: Array<string | Buffer> = [];

    await new Promise<void>((resolve, reject) => {
      queryStream.on('data', (chunk) => {
        // stream을 통해 받는 데이터 data array에 밀어넣기
        data.push(chunk);
      });

      queryStream.on('end', () => {
        // stream 종료 시 resolve()
        resolve();
      });

      queryStream.on('error', (err) => {
        reject(err);
      });
    });

    return this.excelService.createExcelFile('target', data);
  }

 

위 코드에선 stream으로 받는 데이터가 모두 받아진 후 엑셀로 만들기 때문에 굳이 stream을 쓸 필요가 없다고 생각한다. 

 

target.controller

  @Get('excel')
  @UseInterceptors(ExcelFileCleanupInterceptor)
  async getTargetsByExcel(@Res() response: Response) {
    const { filename, filePath } = await this.targetService.getTargetsByExcel();
    const file = fs.createReadStream(filePath);
    
    // 파일 전달 후 interceptor에서 서버에 저장된 임시 엑셀 파일을 지우기 위해 filePath를 response.locals에 저장
    response.locals.filePathToDelete = filePath;

    // setHeader에서 세팅하는 filename이 client에서 다운로드 했을 때의 파일명이 된다.
    response.setHeader(
      'Content-Disposition',
      `attachment; filename=${filename}`,
    );

    return file.pipe(response);
  }

 

excel.interceptor

import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import * as fs from 'fs';

@Injectable()
export class ExcelFileCleanupInterceptor implements NestInterceptor {
  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
  
    // nestjs의 생명 주기에서 request가 아닌 response에 대해서만 동작하게
    const response = context.switchToHttp().getResponse();
    
    const responseFinished = new Promise<void>((resolve) => {
      response.once('finish', resolve);
    });

    return next.handle().pipe(
      tap(async () => {
        // 위 responseFinished를 통해 controller의 return이 된 후 동작하게 만든다. 
        await responseFinished;

        const filePath = response.locals?.filePathToDelete;
        
        if (filePath) {
          try {
            // fs.unlinkSync를 통해 해당 위치의 파일 제거
            fs.unlinkSync(filePath);
          } catch (err) {
            console.error('Error deleting file:', err);
          }
        }
      }),
    );
  }
}

 

주석으로 설명 다 달아놓음

728x90