파일 업로드 및 대용량 엑셀 다운로드

프로젝트에서 파일 업로드 및 대용량 엑셀 다운로드를 구현하기 위한 가이드로 각 기능의 사용 방법과 코드 예제 및 설정 방법을 제공합니다.

  • 파일 업로드는 REST API와 FormData를 활용하며 다양한 저장소(WAS/NAS, FTP, AWS S3, Azure Blob Storage) 옵션을 제공합니다.

  • 대용량 엑셀 다운로드는 최신 MyBatis Cursor 기능을 활용한 대용량 데이터 처리 효율성을 극대화 합니다.


파일 업로드

1

기본 사용 흐름

Plugin으로 제공하는 restApi.ts에서 uploadPost를 이용해 파일 업로드를 처리하고, 업로드할 파일을 FormData에 추가하여 서버로 전송합니다.

필드 컴포넌트(fields.tsx)에서 Upload, UploadBox를 사용해 파일의 확장자와 용량 제한을 검증하여 안정성을 확보할 수 있습니다.

저장소 옵션은 WAS/NAS, FTP, AWS S3, Azure Blob Storage 중 선택하여 저장소를 구성합니다.

@Configuration @Bean으로 저장소 옵션에 설정한 정보로 upload 메소드를 정의하여 @Qualifier("uploader") 으로 사용합니다.

2

파일 업로드 (클라이언트) 예시

const fetchUploadFile = async (files: File[]) => {
  const formData = new FormData();
  files.forEach((file) => {
    if (file) formData.append('files', file);
  });
  const response = (await restApi.uploadPost(
    `/api/bo/...`,
    { form: formData }
  )) as ResponseEntity;
  return response;
};
3

Component를 사용한 유효성 검증 예시

export default function Upload() {
  const { setValue } = useFormContext();

  const validator = <T extends File>(file: T) => {
    const fileType = file.type;
    const fileSize = file.size;
    const validTypes = Object.keys(UPLOAD_IMAGE_ACCEPT);

    if (!validTypes.includes(fileType)) {
      return { message: '확장자가 올바르지 않습니다.', code: 'ERR_TYPE' };
    }
    if (GOODS_UPLOAD.MAX_IMAGE_SIZE < fileSize) {
      return { message: '최대 용량을 초과하였습니다.', code: 'ERR_SIZE' };
    }
    return null;
  };

  const onUpload = (files: File[]) => {
    setValue('File', files);
  };

  const onDelete = () => {
    setValue('File', null);
  };

  return (
    <Field.Upload
      name="fileUpload"
      accept={UPLOAD_IMAGE_ACCEPT}
      validator={validator}
      onDelete={onDelete}
      onDrop={onUpload}
    />
  );
}
4

업로드 관련 상수 예시

export const UPLOAD_IMAGE_ACCEPT = {
  'image/png': [],
  'image/jpg': [],
  'image/jpeg': [],
  'image/gif': []
};

export const UPLOAD_VIDEO_ACCEPT = {
  'video/mp4': [],
  'video/avi': [],
  'video/mov': []
};

// 전시
export const DISPLAY_UPLOAD = {
  MAX_IMAGE_SIZE: 10485760,
  MAX_VIDEO_SIZE: 104857600
};

// 상품
export const GOODS_UPLOAD = {
  MAX_IMAGE_SIZE: 10485760,
  MAX_VIDEO_SIZE: 104857600
};

(원본: upload-constants.ts)

5

API 저장소 설정 (Spring Boot applcation.yml 예시)

upload:
  type: s3 # file : WAS / NAS, ftp : FTP, s3 : AWS S3, azure : Azure Blob Storage
  root-path: files/ # 파일업로드 시 저장될 경로를 설정합니다.
  file:
    base-path: /data/ # was의 기본 업로드 경로 또는 mount 되는 nas의 기본 경로를 설정합니다.
    base-path-video: /data/video
  ftp:
    host: 192.168.2.247
    user: test
    password: test
  azure:
    connection: DefaultEndpointsProtocol=https;AccountName=testblob;AccountKey=....
    container: test

# aws의 경우에는 스프링에서 정한 설정 변수를 사용함.
cloud:
  aws:
    s3:
      bucket: x2bee-stg-pri-attachment-s3
    s3-video-origin:
      bucket: x2bee-stg-pri-attachment-s3
region:
  static: ap-northeast-2
stack:
  auto: false
6

Spring Uploader Bean 설정 예시 (UploaderConfig.java)

package com.x2bee.api.common.base.config;

import com.x2bee.common.base.upload.UploaderCommonImpl;
import com.x2bee.common.base.upload.Uploader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class UploaderConfig {

    @Autowired
    private Environment env;

    @Value("${upload.file.base-path:#{null}}")
    private String basePath;
    @Value("${upload.file.base-path-video:#{null}}")
    private String videoBasePath;

    @Value("${upload.ftp.host:}")
    private String ftpHost;
    @Value("${upload.ftp.user:}")
    private String ftpUser;
    @Value("${upload.ftp.password:}")
    private String ftpPassword;

    @Value("${cloud.aws.region.static:#{null}}")
    private String awsRegion;
    @Value("${cloud.aws.s3.bucket:#{null}}")
    private String bucket;
    @Value("${cloud.aws.s3-video-origin.bucket:#{null}}")
    private String videoBucket;

    @Value("${upload.azure.connection:#{null}}")
    private String azureConnection;
    @Value("${upload.azure.container:#{null}}")
    private String azureContainer;

    @Bean(name = "uploader")
    // apllication.yml 설정값으로 bean 모듈을 생성하여 파일업로드를 합니다.
    public Uploader uploader() {
        return new UploaderCommonImpl.Builder(env)
            .basePath(basePath)
            .ftpHost(ftpHost)
            .ftpUser(ftpUser)
            .ftpPassword(ftpPassword)
            .awsRegion(awsRegion)
            .bucket(bucket)
            .azureConnection(azureConnection)
            .azureContainer(azureContainer)
            .build();
    }

    @Bean(name = "videoUploader")
    public Uploader videoUploader() {
        return new UploaderCommonImpl.Builder(env)
            .basePath(videoBasePath)
            .ftpHost(ftpHost)
            .ftpUser(ftpUser)
            .ftpPassword(ftpPassword)
            .awsRegion(awsRegion)
            .bucket(videoBucket)
            .build();
    }
}

(원본: UploaderConfig.java)

7

SampleServiceImpl.java 예시 (서버 업로드 처리)

@Service
@Slf4j
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService {

    @Qualifier("uploader")
    private final Uploader uploader;

    @Qualifier("videoUploader")
    private final Uploader videoUploader;

    @Qualifier("fileuUploader")
    private final Uploader fileuUploader;

    @Override
    public String fileUpload(HttpServletRequest httpServletRequest) throws Exception {
        AtomicReference<String> result = new AtomicReference<>("");
        MultipartHelper.handle(httpServletRequest, (fieldName, fileName, fileSize, inputStream, multipartFile) -> {
            UploadReqDto uploadReqDto = new UploadReqDto();
            uploadReqDto.setAttacheFileKind(AttacheFileKind.SYSTEM);
            uploadReqDto.setTempPathYn(false);
            uploadReqDto.setCustomPath("");
            uploadReqDto.setTypeCd("10");

            // 기존 AWS에 업로드
            Map<String, Object> retMap = uploader.upload(multipartFile, uploadReqDto); // UploaderConfig Bean 모듈 호출
            log.debug("fileUpload : {}", retMap);
            UploadResDto uploadResDto = (UploadResDto)((Map<String, Object>)retMap.get("data")).get("data");
            result.set(uploadResDto.getUrl());
            log.debug("url : {}", uploadResDto.getUrl());

            // 기존 AWS 비디오 공간에 업로드
            videoUploader.upload(multipartFile, uploadReqDto);

            // 파일 시스템에 업로드
            fileUploader.upload(multipartFile, uploadReqDto);
        });
        return result.get();
    }
}

대용량 엑셀 다운로드

대용량 엑셀 다운로드 구현 시 메모리 효율성을 위해 MyBatis(3.2.4 이상)의 Cursor를 활용하고, @ExcelDownLoad 어노테이션과 ExcelUtil.createCursorExcel 메서드를 사용합니다. Postman을 활용해 테스트를 지원하며, TypeScript 유틸도 제공합니다.

1

MyBatis Mapper 예시 (Cursor 사용)

/* SampleMapper.java */
public interface SampleMapper {
    Cursor<SampleZipNoMgmtResponse> getZipNoList(SampleZipNoMgmtRequest sampleZipNoMgmtRequest);
}
2

MyBatis XML 쿼리 예시 (Sample.xml)

<!--Dto, Xml 예제-->
<select id="getZipNoList" parameterType="SampleZipNoMgmtRequest" resultType="SampleZipNoMgmtResponse" >
WITH TMP1 AS (
  SELECT ZIP_NO_SEQ, ZIP_NO, CTP_NM, SIG_NM, HEMD_NM, LNBR_MNNM, LNBR_SLNO, ROAD_NM
  FROM ST_ZIP_NO
  WHERE USE_YN = 'Y'
  <if test="ctpNmParam != null and ctpNmParam != ''">
    AND CTP_NM = #{ctpNmParam}
  </if>
  <if test="sigNmParam != null and sigNmParam != ''">
    AND SIG_NM LIKE '%' || #{sigNmParam}
  </if>
)
SELECT ZIP_NO_SEQ, ZIP_NO, CTP_NM, SIG_NM, HEMD_NM, LNBR_MNNM, LNBR_SLNO, ROAD_NM
FROM TMP1
ORDER BY 1
LIMIT 1000000
</select>
3

Controller 샘플

/* 대용량 엑셀다운로드 샘플 */
@GetMapping("/exceldown")
public void exceldown(SampleZipNoMgmtRequest sampleZipNoMgmtRequest) throws Exception {
    sampleService.exceldown(sampleZipNoMgmtRequest);
}
4

Service 구현 예시 (ExcelUtil.createCursorExcel)

/* SampleServiceImpl.java */
@ExcelDownLoad
public void exceldown(SampleZipNoMgmtRequest sampleZipNoMgmtRequest) {

    // sample 1
    ExcelUtil.createCursorExcel(() -> sampleMapper.getZipNoList(sampleZipNoMgmtRequest));

    // sample 2 (추가 옵션)
    ExcelUtil.createCursorExcel(
        () -> sampleMapper.getZipNoList(zipNoMgmtRequest),
        ExcelEntity.builder()
            .fileName("TRGMN_LIST")
            .sheetName("SHEET1")
            .build()
    );
}

참고: Postman 샘플파일 [Sample].postman_collection.jsonarrow-up-right

Postman에서 Excel 다운로드 테스트 시 Send and Download 옵션을 사용합니다.

대용량 엑셀 다운로드 시, 제공되는 download-utils.ts의 downloadStaticFile을 사용합니다:

downloadStaticFile({
  method: 'get',
  fileUrl: '/api/bo/v1/sample/exceldown',
  downloadedFileName: '파일명'
});


Attachments

Document generated by Confluence on 2025-12-18 03:31 오전

Atlassian: http://www.atlassian.com/

마지막 업데이트