> For the complete documentation index, see [llms.txt](https://tech.x2bee.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tech.x2bee.com/dev-guide/dev-start/markdown/undefined.md).

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

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

* 파일 업로드는 REST API와 FormData를 활용하며 다양한 저장소(WAS/NAS, FTP, AWS S3, Azure Blob Storage) 옵션을 제공합니다.
* 대용량 엑셀 다운로드는 최신 MyBatis Cursor 기능을 활용한 대용량 데이터 처리 효율성을 극대화 합니다.

***

## 파일 업로드

{% stepper %}
{% step %}

### 기본 사용 흐름

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

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

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

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

{% step %}

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

```javascript
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;
};
```

{% endstep %}

{% step %}

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

```tsx
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}
    />
  );
}
```

{% endstep %}

{% step %}

### 업로드 관련 상수 예시

```ts
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)
{% endstep %}

{% step %}

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

```yaml
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
```

{% endstep %}

{% step %}

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

```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)
{% endstep %}

{% step %}

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

```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();
    }
}
```

{% endstep %}
{% endstepper %}

***

## 대용량 엑셀 다운로드

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

{% stepper %}
{% step %}

### MyBatis Mapper 예시 (Cursor 사용)

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

{% endstep %}

{% step %}

### MyBatis XML 쿼리 예시 (Sample.xml)

```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>
```

{% endstep %}

{% step %}

### Controller 샘플

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

{% endstep %}

{% step %}

### Service 구현 예시 (ExcelUtil.createCursorExcel)

```java
/* 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()
    );
}
```

{% endstep %}
{% endstepper %}

참고: Postman 샘플파일 [\[Sample\].postman\_collection.json](https://tech.x2bee.com/download/attachments/99352577/%5BSample%5D.postman_collection.json)

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

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

```ts
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/>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://tech.x2bee.com/dev-guide/dev-start/markdown/undefined.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
