= 파일 업/다운로드

== 개요
클라이언트의 파일 업/다운로드 API 호출을 처리한다.

=== 파일 업로드
단일 파일 업로드를 처리한다. 하나의 파일만 업로드하는 경우가 아니라면 보통은 멀티 파일 업로드를 이용한다.

=== 멀티 파일 업로드

====
컨트롤러에서 파일 업로드 서비스를 호출한다.

.FileManagerController
[source,java]
----
@PostMapping("/resource/attachments/multifile-upload")
    public List<AttachFile> uploadMultiFile(
            @Parameter(description = "MultipartFile[]", required = true) @RequestParam(required = true) MultipartFile[] files,
            @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType) {

        return fileManagerService.save(downloadType, Arrays.asList(files));
    }
----
====

====
서비스에서 지정된 경로에 파일 리소스를 저장하고, 업로드된 파일 정보를 DB 에 저장한다.

.FileManagerServiceImpl
[source,java]
----
/**
 * 기본 업로드 패스 설정 값
 */
@Value("${common.upload-path}")
private String fileUploadPath;

/**
 * 업로드 루트 하위 폴더 자릿수 설정 값
 */
@Value("${common.upload.directory-name-len}")
private int directoryNameLen;

/**
 * 사용자지정 업로드 패스 설정 여부
 */
@Value("${custom.upload-path.enabled}")
private boolean customUploadPathEnabled;

/**
 * 사용자지정 업로드 패스 설정 값
 */
@Value("${custom.upload-path}")
private String customUploadPaths;

@Override
@Transactional
public List<AttachFile> save(String downloadType, List<MultipartFile> files) {
    return this.save(downloadType, null, files);
}

@Override
@Transactional
public List<AttachFile> save(String downloadType, String refId, List<MultipartFile> files) {
    List<AttachFile> attachFileList = new ArrayList<>();

    files.stream().forEach(file -> {
        AttachFile attachFile = this.save(downloadType, refId, file);
        attachFileList.add(attachFile);
    });

    return attachFileList;
}

@Override
@Transactional
public AttachFile save(String downloadType, String refId, MultipartFile file) {
    String fileId = this.store(file, downloadType); // <1>

    AttachFile attachFile = new AttachFile();
    attachFile.setDownloadType(downloadType);
    attachFile.setFilePathName(this.rootUploadPath.toString());
    attachFile.setFileExtensionName(fileId);
    attachFile.setFileName(file.getOriginalFilename());
    attachFile.setFileMimeTypeName(file.getContentType());
    attachFile.setFileSize(file.getSize());
    attachFile.setOwnerObjectPkId(refId);

    return fileManagerDao.insertFileInfo(attachFile);   // <2>
}
----
<1> 파일 리소스 저장
<2> 업로드된 파일 정보 DB저장
====

====
파일 리소스 저장 경로는 기본 업로드 패스 값(common.upload-path)과 하위 디렉토리 길이 설정 값(common.upload.directory-name-len)에 따라 결정된다. +
기본 업로드 패스외에 사용자정의 업로드 패스 설정도 가능하다.

.config.properties
[source,properties]
----
## File Attach Configuration
common.upload-path=/NAS/SDL/upload  // <1>
common.upload.directory-name-len=2  // <2>
# custom upload path 설정
custom.upload-path.enabled=false    // <3>
custom.upload-path=\                // <4>
notice=/NAS/SDL/upload/notice,\
faq=/NAS/SDL/upload/faq
----
<1> 기본 업로드 패스 설정
<2> 업로드 패스 하위 디렉토리 길이 + 
파일명(UUID)에서 이 길이 만큼 잘라서 기본 업로드 패스 하위 디렉토리가 생성된다.
<3> 사용자정의 업로드 패스 사용 여부
<4> 파일 컴포넌트별 사용자정의 업로드 패스 설정 (custom.upload-path.enabled=true 일 경우 적용됨)
====

=== 파일 다운로드
컨트롤러에서 파일 다운로드 서비스를 호출한다.

.FileManagerController
[source,java]
----
@GetMapping("/resource/attachments/file-download/{fileId}")
public ResponseEntity<Resource> downloadFile(
        @Parameter(description = "File ID", required = true) @PathVariable String fileId,
        @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType,
        HttpServletRequest request) {

    long startTime = System.nanoTime();

    if (StringUtils.isBlank(fileId)) {
        throw new FileManagerException("No File ID");
    }

    AttachFile attachFile = fileManagerService.getAttachFile(fileId, downloadType);  // <1>
    if (attachFile == null) {
        throw new FileManagerException("Cannot find file info: " + fileId);
    }
    Resource resource = fileManagerService.getResource(attachFile.getFileExtensionName(), attachFile.getFilePathName());  // <2>

    String contentType = attachFile.getFileMimeTypeName();
    if (StringUtils.isBlank(contentType)) {
        contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
    }

    String fileName = attachFile.getFileName(); // <3>
    String encodeFileName = null;
    // 다운로드 파일명 UTF-8 인코딩
    encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");

    // 다운로드 이력 로깅
    fileManagerService.saveFileDownloadLog(fileName, startTime, request);  // <4>

    return ResponseEntity.ok()
            .contentType(MediaType.parseMediaType(contentType))
            .header("Content-Transfer-Encoding", "binary")
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodeFileName + "\"")
            .body(resource);
}
----
<1> 파일 정보 조회
<2> 파일 리소스 가져오기
<3> 파일 업로드 시점의 파일명
<4> 파일 다운로드 이력 로깅

=== 멀티 파일 다운로드
여러 파일을 zip 파일 형태로 다운로드할 수 있도록 제공한다.

.FileManagerController
[source,java]
----
    @Value("${common.download.zipfilename}")
    private String zipFileName;
    
    @GetMapping("/resource/attachments/multifile-download")
    public ResponseEntity<ByteArrayResource> downloadMultiFile(
            @Parameter(description = "File IDs", required = true) @RequestParam(required = true) String[] fileIds,
            @Parameter(description = "다운로드 구분 (컴포넌트 한개 이상일 경우 구분)", required = true) @RequestParam(required = true) String downloadType,
            @Parameter(description = "zip 파일명", required = false) @RequestParam(required = false) String zipFileName,
            HttpServletRequest request) {

    	long startTime = System.nanoTime();

        if (fileIds == null || fileIds.length == 0) {
            throw new FileManagerException("No File ID(s)");
        }

        if (StringUtils.isBlank(zipFileName)) {
            zipFileName = this.zipFileName;
        }

        // 다운로드 파일명 UTF-8 인코딩
		String fileName = null;
        fileName = URLEncoder.encode(zipFileName, StandardCharsets.UTF_8).replace("+", "%20");

        // 다운로드 이력 로깅
        fileManagerService.saveFileDownloadLog(zipFileName, startTime, request);

        ByteArrayResource baResource = fileManagerService.getByteArrayResource(fileIds, downloadType);

        return ResponseEntity.ok()
                .contentLength(baResource.contentLength())
                .contentType(MediaType.parseMediaType("application/zip"))
                .header("Content-Transfer-Encoding", "binary")
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + ".zip\";")
                .body(baResource);
    }
----
