상세 컨텐츠

본문 제목

Ajax로 첨부파일 다운로드 구현 | 스프링 MVC

Framework/Spring | SpringBoot

by yooputer 2024. 3. 8. 17:53

본문

기존 레거시 프로젝트에서는 첨부파일 다운로드 기능이 form-submit 방식으로 구현되어있었다.

 

form-submit 방식으로 파일을 다운로드 받으면 단점은 다음과 같다.

1. 파일 다운로드가 필요한 곳에서 form이 있어야 함

2. 파일 다운로드 과정에서 오류가 발생하면 에러페이지로 이동함. 

 

위와 같은 단점때문에 ajax로 파일 다운로드를 구현해야겠다고 생각했다. 

이번 포스팅은 ajax 방식을 사용한 첨부파일 다운로드 구현 방법을 설명해보려고 한다. 


[DB] 파일 테이블

우선 나는 파일을 웹서버/파일서버에 저장한 후 저장한 경로와 원본 파일명 등의 파일 정보를 file_info 테이블에 저장한다. 

file_info는 프로젝트의 모든 파일 정보를 저장하는 테이블이어서 어떤 테이블의 어떤 게시물의 첨부파일인지 table_nm, table_seq 컬럼에 저장한다. 

사용자에게는 파일 관련 정보는 파일 시퀀스와 원본 파일명만을 공개하고 그 외의 저장 경로 등의 파일 정보는 공개하지 않는다. 


[Spring] 파일 다운 api 구현

파일 Resource를 반환하는 컨트롤러 메서드를 만든다. 

    /**
     * 첨부파일 다운로드 api
     *
     */
    @PostMapping("/download-attach-file-ajax")
    @ResponseBody
    public ResponseEntity<?> downloadAttachFile(Long fileSeq, String tableNm, Long tableSeq, HttpServletRequest request) {
        try{
            return fileService.downloadAttachFile(fileSeq, tableNm, tableSeq, request);
        }catch (Exception e){
            logger.error("Exception", e);
            throw new AjaxException(e);
        }
    }

 

1. 파일 시퀀스를 통해 file_info에서 파일 정보를 가져온다.

2. 파일 정보가 있는지 확인하고 없으면 예외를 발생시킨다. 

3. 파일 정보의 tableNm, tableSeq가 일치하는지 확인한다. 틀리면 예외를 발생시킨다. 

4. response 헤더에 파일명을 세팅한다.

5. 파일의 저장경로에서 파일을 불러오고 Resource로 변환한다.

6. Resource와 headers를 담은 ResponseEntity를 반환한다.

@Service
@RequiredArgsConstructor
public class FileService {

    Logger logger = LoggerFactory.getLogger(this.getClass());
    private final FileDao fileDao;
    private final ResourceLoader resourceLoader;

    // 첨부파일 다운로드
    public ResponseEntity<Resource> downloadAttachFile(Long fileSeq, String tableNm, Long tableSeq, HttpServletRequest request) {
        ResponseEntity<Resource> response = ResponseEntity.notFound().build();

        if (fileSeq == null) {
            throw new CustomException(ErrorCode.INVALID_VALUE);
        }

        // FileVo 조회
        FileVo fileVo = fileDao.getFileVoBySeq(fileSeq);
        
        if (fileVo == null) {
            throw new CustomException(ErrorCode.FILE_NOT_FOUND);
        }
        
        if (!fileVo.getTableNm().equals(tableNm) || !fileVo.getTableSeq().equals(tableSeq)) {
            throw new CustomException(ErrorCode.INVALID_FILE_INFO);
        }

        // header에 파일명 세팅
        HttpHeaders headers = new HttpHeaders();
        try {
            String encodedFileName = FileUtil.getEncodedFileName(request, fileVo.getOriginNm());

            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.add("Content-Transfer-Encoding", "binary");
            headers.add("Content-Disposition", "attachment; filename=" + encodedFileName + ";");
        } catch (Exception e) {
            return ResponseEntity.notFound().build();
        }

        // Resource 세팅
        // 로컬서버인경우 로컬 스토리지에 있는 파일을 찾아서 리소스로 반환
        // 개발/운영 서버인 경우 Azure Blob Storage에 있는 파일을 찾아서 리소스로 반환
        if (ServerInfoUtil.isLocalServer()) {
            if (!FileUtil.isExist(fileVo.getSavePath())) {
                return ResponseEntity.notFound().build();
            }

            Resource resource = new FileSystemResource(fileVo.getSavePath());
            response = new ResponseEntity<>(resource, headers, HttpStatus.OK);
        } else if (ServerInfoUtil.isDevServer() || ServerInfoUtil.isPrdServer()) {
            Resource storageBlobResource = resourceLoader.getResource(fileVo.getSavePath());
            response = new ResponseEntity<>(storageBlobResource, headers, HttpStatus.OK);
        } else {
            return ResponseEntity.notFound().build();
        }

        return response;
    }

 

파일 정보 조회 쿼리

    <select id="getFileVoBySeq" resultType="FileVo">
        /* 파일vo 조회 */
        SELECT seq AS file_seq,
               table_nm,
               table_seq,
               save_path,
               save_nm,
               origin_nm,
               file_type,
               file_ext,
               file_size,
               download_cnt,
               reg_date
        FROM metaesg.file_info
        WHERE seq = #{fileSeq)
            AND NOT is_del
      </select>

[JS] 파일 다운로드 함수 구현

파일 다운로드 api를 요청하고 반환받은 resource를 xhr로 다운로드 받게 하는 함수를 구현한다. 

공통 js 파일에 구현해놓으면 좋다. 

function downloadAttachFile(obj) {
  var token = $("meta[name='_csrf']").attr('content');
  var header = $("meta[name='_csrf_header']").attr('content');

  $.ajax({
    url       : obj.url,
    data      : obj.data,
    type      : obj.type,
    contentType: obj.contentType,
    xhrFields : {
      responseType: "blob",
    },
    error     : function (e) {
      commonAlert.alert("다운로드에 실패하였습니다.<br/>관리자에게 문의해주세요.");
    },
    beforeSend: function (jqXHR) {
      if (token && header) {
        jqXHR.setRequestHeader(header, token);
      }
    }
  }).done(function (blob, status, xhr) {
    var fileName = "name";
    var disposition = xhr.getResponseHeader("Content-Disposition");

    if (disposition && disposition.indexOf("attachment") !== -1) {
      var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
      var matches = filenameRegex.exec(disposition);

      if (matches != null && matches[1]) {
        fileName = decodeURI(matches[1].replace(/['"]/g, ""));
      }
    }

    // for IE
    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
      window.navigator.msSaveOrOpenBlob(blob, fileName);
    } else {
      var URL = window.URL || window.webkitURL;
      var downloadUrl = URL.createObjectURL(blob);

      if (fileName) {
        var a = document.createElement("a");

        // for safari
        if (a.download === undefined) {
          window.location.href = downloadUrl;
        } else {
          a.href = downloadUrl;
          a.download = fileName;
          document.body.appendChild(a);
          a.click();
        }
      } else {
        window.location.href = downloadUrl;
      }
    }
  });
}

commonAlert은 미리 구현한 공통Alert을 위한 객체이다.

 

위에서 구현한 함수를 사용해 편하게 첨부파일을 다운로드 받을 수 있다. 

 

      downloadAttachFile({
        url      : "/file/download-attach-file-ajax",
        type   : "POST",
        data : {
          fileSeq: fileSeq,
          tableNm: "project_info",
          tableSeq: [[${projectInfo.projectSeq}]]
        }
      });

 

관련글 더보기