콘텐츠로 이동

HERA-V4 업로드 가이드

목차

  1. 개요
  2. 빠른 시작
  3. 단계별 적용 가이드
  4. 핵심 패턴 & 안티패턴
  5. 트러블슈팅
  6. 레퍼런스 링크

1. 개요

HERA-V4의 업로드 시스템은 TUS 재개 가능 프로토콜 위에 4가지 처리 모드를 제공한다. 프론트엔드는 filepond 라이브러리를 통해 일관된 사용자 경험을 제공하고, 백엔드는 TransferMode 별로 분기 처리한다.

1.1 지원 업로드 모드

Mode 사용 사례 저장 위치
NORMAL 일반 파일 업로드 (첨부파일, 이미지 등) 로컬 디스크 (resources.dir.upload)
OBJECT_STORAGE 영속 저장이 필요한 파일 (회사 자료 등) S3 호환 스토리지
EXCEL 엑셀 데이터 파싱 → DB 일괄 적재 처리 후 _temp/excel/
EXCEL_ASYNC 대용량 엑셀 비동기 처리 (미사용 — 향후 확장)

1.2 핵심 컴포넌트

컴포넌트 역할
FileTransfer (interface) 업로드 처리 인터페이스
WebFileTransfer NORMAL/OBJECT_STORAGE 처리
TusTransferService TUS 프로토콜 (재개 가능 업로드)
TransferParams mode + 파일 형식 화이트리스트 + Object Storage 메타
UploadResult 결과 DTO (saved file path 등)
cleanup.directory.scheduler 임시 파일 정리 (cron 기반)

1.3 적용 가능한 시나리오

  • 사용자가 파일을 업로드해야 하는 모든 메뉴 (첨부파일, 이미지, 엑셀, 양식 등)
  • 대용량 파일 재개 가능 업로드 필요 (네트워크 단절 시 이어 올리기)
  • 운영자가 파일 형식/크기 정책을 메뉴별로 다르게 적용해야 하는 경우

1.4 적용 부적절한 시나리오

  • 외부 API에서 파일을 전송받는 경우 (서버-서버 통신은 별도 프로토콜)
  • 1KB 이하 매우 작은 파일의 대량 처리 (오버헤드가 더 큼 — JSON 등으로 처리)
  • 권한 분리가 매우 복잡한 경우 (권한 모델을 매뉴얼 외부에서 별도 검토 필요)

2. 빠른 시작

신규 메뉴에 일반 파일 업로드를 5분 안에 적용하는 최소 예시이다.

2.1 백엔드 endpoint

hera-webapp/src/main/java/.../web/controller/api/<menu>/<Menu>ApiController.java:

@RestController
@RequestMapping(SYS_API)
public class MyMenuApiController extends WebApiRouter {

    @ResponseBody
    @RequestMapping(value = {"/my-menu/upload", "/my-menu/upload/**"})
    public ResponseEntity upload(HttpServletRequest req, HttpServletResponse res) throws Exception {
        TransferParams params = TransferParams.builder(NORMAL)
            .bucket(getBucketName(req))
            .path(getFileMiddlePath(false))
            .build();

        UploadResult result = fileTransfer.transfer(req, res, params);
        return Responses.ok(result);
    }
}

2.2 프론트엔드 modal + fragment

업로드 모달은 file-upload fragment를 재사용한다.

<menu>.html:

<th:block layout:insert="~{fragments/modal/modal-file-upload :: content('파일 업로드', ${@apiUrl}+'/my-menu/upload')}"/>

→ 모달 내부에서 file-upload.html (hera-webapp/src/main/resources/templates/fragments/file-upload.html) fragment가 filepond를 초기화하고 TUS 청크 업로드를 수행한다.

2.3 결과 확인

  1. 메뉴 화면에서 "업로드" 버튼 클릭 → modal 표시
  2. 파일 드래그 앤 드롭 또는 클릭으로 선택
  3. filepond가 TUS 프로토콜로 청크 단위 업로드
  4. 백엔드 WebFileTransfer가 파일을 _temp/upload/{uuid}/에 저장
  5. UploadResult JSON 응답 반환 (저장 경로 포함)
  6. cleanup scheduler가 7일 후 자동 정리

3. 단계별 적용 가이드

신규 메뉴에 업로드 기능을 추가하는 표준 절차이다.

Step 1: 경로 설정 점검

hera-webapp/src/main/resources/application-<profile>.yml에 표준 디렉토리가 정의되어 있는지 확인한다.

resources.dir:
  root: ${user.dir}/_temp                        # 모든 임시 파일의 루트
  upload: ${resources.dir.root}/upload           # TUS 업로드 임시 (NORMAL)
  excel: ${resources.dir.root}/excel             # 엑셀 처리
  excel-template: ${resources.dir.excel}/template
  excel-result-set: ${resources.dir.excel}/result-set
  excel-error: ${resources.dir.excel}/error
  user-avatar: ${resources.dir.root}/user-avatar
  file-storage: ${resources.dir.root}/file-storage
  dataset: ${resources.dir.root}/dataset

원칙: - resources.dir.root 하나로 모든 임시 파일을 관리한다 - 새 모듈/메뉴는 기존 디렉토리 재사용 — 임의로 새 경로 만들지 않는다 - 절대 경로 하드코딩 금지 — 모두 resources.dir.* 변수 사용

Step 2: Cleanup Scheduler 설정

장기 누적되는 임시 파일을 정기 삭제한다.

cleanup.directory.scheduler:
  enabled: true
  options:
    - dir: ${resources.dir.upload}
      cron: 0 0 1 * * ?            # 매일 새벽 1시
      cutoff-timestamp: -7
      cutoff-timestamp-unit: day   # 7일 경과 파일 삭제
    - dir: ${resources.dir.excel}/error
      cron: 0 0 2 * * ?
      cutoff-timestamp: -30
      cutoff-timestamp-unit: day

추천 정책:

디렉토리 보관 기간
upload (TUS 임시) 7일
excel/error (오류 엑셀) 30일 (사용자가 다운로드할 시간 보장)
dataset (.dat 청크) 7일
user-avatar 미적용 (영속 저장)
file-storage 미적용 (영속 저장)

Step 3: 백엔드 Controller endpoint

업로드 endpoint를 작성한다. WebFileTransfer가 TUS 프로토콜을 처리하므로 Controller는 TransferParams만 구성하면 된다.

@Resource
private FileTransfer fileTransfer;

@Value("${resources.dir.upload}")
private String uploadDir;

@ResponseBody
@RequestMapping(value = {"/my-menu/upload", "/my-menu/upload/**"})
public ResponseEntity upload(HttpServletRequest req, HttpServletResponse res) throws Exception {
    TransferParams params = TransferParams.builder(NORMAL)
        .bucket(getBucketName(req))
        .path(getFileMiddlePath(false))
        .enableFileFormats(List.of("xlsx", "pdf", "png", "jpg"))   // 화이트리스트
        .build();

    UploadResult result = fileTransfer.transfer(req, res, params);
    return Responses.ok(result);
}

핵심 옵션:

옵션 의미 예시
TransferMode 처리 모드 NORMAL, OBJECT_STORAGE, EXCEL
bucket 저장 버킷 (또는 디렉토리) getBucketName(req)
path 하위 경로 prefix getFileMiddlePath(false)
enableFileFormats 허용 파일 확장자 List.of("xlsx", "csv")

Step 4: 프론트엔드 Modal + Fragment

업로드 UI는 fragment를 조합하여 구성한다. 자체 multipart form 작성 금지 — 항상 fragment를 재사용한다.

4-1. 일반 파일 업로드

<!-- 메뉴 페이지 어딘가에 모달 삽입 -->
<th:block layout:insert="~{fragments/modal/modal-file-upload :: content(${@apiUrl}+'/my-menu/upload')}"/>

<!-- 트리거 버튼 -->
<button type="button" data-bs-toggle="modal" data-bs-target="#fileUploadModal">
    <i class="ri-upload-line"></i> 업로드
</button>

4-2. Object Storage 첨부파일

<th:block layout:insert="~{fragments/file-upload-object-storage :: content('attachmentIds')}"/>

attachmentIds는 form에 포함될 hidden input 이름. 업로드 완료 시 파일 ID 배열이 자동 채워진다.

4-3. 엑셀 업로드 (룰 엔진 연동)

<th:block layout:insert="~{fragments/modal/modal-excel-upload :: content(${@apiUrl}+'/my-menu/excel-upload', 'sys0102')}"/>

→ 두 번째 인자('sys0102')는 menuId. Coordinator가 이 ID로 ExcelAdapter를 dispatch한다.

4-4. 사용 가능한 fragment 목록

Fragment 용도
file-upload 표준 filepond + TUS (모달 내부)
file-upload-object-storage Object Storage 모드 (form에 임베드)
file-uploader 단일 파일 업로더 (간소화)
modal-file-upload 일반 파일 업로드 모달
modal-excel-upload 엑셀 업로드 모달 (탭 + 검증 그리드)
modal-layout-upload 엑셀 레이아웃 업로드 (sys0402 전용)
file-list, file-list2 업로드된 파일 목록 표시
file-list-object-storage Object Storage 파일 목록

Step 5: Object Storage 모드 (선택)

대용량/영속 저장이 필요하면 NORMAL 대신 OBJECT_STORAGE 모드를 사용한다.

TransferParams params = TransferParams.builder(OBJECT_STORAGE)
    .bucket("my-company-attachments")
    .path("/board/" + boardId)
    .objectId(UUID.randomUUID().toString())
    .build();

주의: - Object Storage 자격 증명은 application-<profile>.yml의 별도 섹션에서 관리 - 로컬 개발 시 NORMAL 모드 사용 권장 (인프라 의존성 회피)

Step 6: 엑셀 업로드 적용 (선택)

엑셀 데이터를 DB에 일괄 적재하는 메뉴는 별도 처리가 필요하다.

  1. Adapter 작성: ExcelUploadMenuAdapter 인터페이스 구현 (또는 AbstractExcelUploadMenuAdapter<V> 상속)
  2. DB 메타데이터 등록: sys0401(템플릿) → sys0402(레이아웃) → sys0403(검증룰) → sys0404(변환룰)
  3. Frontend: modal-excel-upload fragment 사용 (위 §4-3 참조)

상세 절차는 PDCA archive 참조:

핵심 코드 참조:

  • AbstractExcelUploadMenuAdapter.java (hera-commons/src/main/java/kr/co/dandisoft/hera/excel/upload/AbstractExcelUploadMenuAdapter.java) — Adapter base class (V 제네릭)
  • CommonCodeExcelAdapter.java (hera-webapp/src/main/java/kr/co/dandisoft/hera/domain/sys/excel/sys0101/CommonCodeExcelAdapter.java) — 참조 구현

Step 7: 동작 검증

  1. 업로드 모달에서 파일 선택 → 청크 업로드 진행률 확인
  2. 네트워크 차단 후 재연결 → 자동 이어 올리기 동작 (TUS resume)
  3. _temp/upload/{uuid}/ 디렉토리에 파일 저장됨 확인
  4. cleanup scheduler 시뮬레이션: cutoff 기간을 짧게 설정 후 cron 실행

4. 핵심 패턴 & 안티패턴

4.1 권장 패턴

파일 형식 화이트리스트 사용

TransferParams.builder(NORMAL)
    .enableFileFormats(List.of("xlsx", "pdf", "png", "jpg"))   // ✅ 명시적 허용
    .build();

비허용 확장자는 자동 거부된다. 메뉴별로 다른 정책 적용 가능.

디렉토리 변수 일관 사용

@Value("${resources.dir.upload}")   // ✅ application.yml 변수
private String uploadDir;

Fragment 재사용

<th:block layout:insert="~{fragments/modal/modal-file-upload :: content(${url})}"/>   <!-- ✅ -->

메뉴 ID로 Adapter dispatch (엑셀)

<th:block layout:insert="~{fragments/modal/modal-excel-upload :: content(${url}, 'sys0102')}"/>

→ Coordinator가 menuId로 적절한 Adapter를 자동 선택한다.

4.2 안티패턴 (해서는 안 되는 것)

❌ 자체 multipart 파싱

@PostMapping("/my-menu/upload")
public ResponseEntity upload(@RequestParam("file") MultipartFile file) { ... }   // ❌

TUS 청크 업로드와 비호환. WebFileTransfer를 사용한다.

❌ 절대 경로 하드코딩

private String uploadDir = "/var/www/upload";   // ❌ 환경별 이식 불가

@Value("${resources.dir.upload}")로 외부화한다.

❌ 파일 형식 검증 누락

TransferParams.builder(NORMAL).build();   // ❌ enableFileFormats 비어있음 → 모든 파일 허용

악성 파일 업로드 위험. 메뉴별 화이트리스트 명시.

❌ Cleanup 미설정

cleanup.directory.scheduler:
  enabled: false   # ❌ 임시 파일이 영구 누적되어 디스크 고갈

enabled: true로 두고 메뉴별 cutoff 기간 정의.

❌ 임시 파일을 영속 저장 용도로 사용

// ❌ _temp/upload는 cleanup 대상
return "/_temp/upload/" + filename;

영속 저장은 file-storage 또는 OBJECT_STORAGE 사용.

❌ 엑셀 Adapter에서 직접 EasyExcel 호출

// ❌ Coordinator가 이미 파싱했다
EasyExcel.read(filePath).sheet(0).doRead();

상세는 엑셀 업로드 PDCA 참조.


5. 트러블슈팅

5.1 자주 발생하는 오류

오류 메시지 / 증상 원인 해결
업로드 진행률 0%에서 멈춤 TUS endpoint 미응답 Controller @RequestMapping/upload/** 와일드카드 포함 확인
Disk space full cleanup scheduler 미가동 cleanup.directory.scheduler.enabled: true, cron 표현식 검증
엑셀 업로드 후 _temp/upload/{uuid} 잔존 EXCEL 모드가 정리하지 않은 상태 처리 완료 후 Coordinator가 cleanup 호출하는지 확인
허용되지 않은 파일 형식 enableFileFormats 위반 메뉴별 화이트리스트 점검
네트워크 단절 후 처음부터 재업로드 filepond 설정 누락 chunkUploads: true, tus 옵션 확인
Object Storage 401 Unauthorized 자격 증명 누락 application-<profile>.yml의 Object Storage 섹션 확인
/file-upload :: content 에러 Fragment 호출 시그니처 불일치 fragment 인자 개수 확인 (위 §4-4 표)

5.2 디버깅 절차

  1. 브라우저 DevTools Network: TUS HTTP 요청 흐름(POST /upload, PATCH /upload/{id}) 확인
  2. 백엔드 로그: _temp/logs/hera-webapp/에서 WebFileTransfer/TusTransferService 검색
  3. 임시 파일: _temp/upload/{uuid}/에 청크 파일이 생성되는지 확인
  4. filepond 콘솔: 브라우저 콘솔의 filepond 에러 메시지

5.3 로그 위치

로그 경로
webapp 로그 _temp/logs/hera-webapp/hera-webapp-yyyy-MM-dd.log
TUS 업로드 임시 _temp/upload/{uuid}/
엑셀 .dat 청크 _temp/excel/upload/ (엑셀 모드 한정)
오류 엑셀 출력 _temp/excel/error/
엑셀 결과 셋 _temp/excel/result-set/

6. 레퍼런스 링크

6.1 백엔드 핵심 코드

  • FileTransfer.java (hera-webapp/src/main/java/kr/co/dandisoft/hera/web/transfer/file/FileTransfer.java) — Transfer 인터페이스
  • WebFileTransfer.java (hera-webapp/src/main/java/kr/co/dandisoft/hera/web/transfer/file/WebFileTransfer.java) — NORMAL/OBJECT_STORAGE 처리
  • TusTransferService.java (hera-webapp/src/main/java/kr/co/dandisoft/hera/web/transfer/file/TusTransferService.java) — TUS 프로토콜
  • TransferMode.java (hera-webapp/src/main/java/kr/co/dandisoft/hera/web/transfer/file/TransferMode.java) — 4가지 모드
  • TransferParams.java (hera-webapp/src/main/java/kr/co/dandisoft/hera/web/transfer/file/TransferParams.java) — 빌더 패턴
  • UploadResult.java (hera-webapp/src/main/java/kr/co/dandisoft/hera/web/transfer/file/result/UploadResult.java) — 결과 DTO

6.2 엑셀 업로드 (별도)

  • AbstractExcelUploadMenuAdapter.java (hera-commons/src/main/java/kr/co/dandisoft/hera/excel/upload/AbstractExcelUploadMenuAdapter.java) — Adapter base
  • ExcelUploadCoordinator.java (hera-commons/src/main/java/kr/co/dandisoft/hera/excel/upload/ExcelUploadCoordinator.java) — 멀티시트 라이프사이클
  • DbColumnGuard.java (hera-commons/src/main/java/kr/co/dandisoft/hera/mapper/guard/DbColumnGuard.java) — SQL Injection 방어

6.3 프론트엔드 Fragment

  • file-upload.html (hera-webapp/src/main/resources/templates/fragments/file-upload.html) — filepond + TUS
  • file-upload-object-storage.html (hera-webapp/src/main/resources/templates/fragments/file-upload-object-storage.html) — Object Storage 변형
  • modal-file-upload.html (hera-webapp/src/main/resources/templates/fragments/modal/modal-file-upload.html) — 일반 모달
  • modal-excel-upload.html (hera-webapp/src/main/resources/templates/fragments/modal/modal-excel-upload.html) — 엑셀 모달

6.4 관련 화면 (운영자용 / 엑셀 업로드 한정)

화면 ID 용도
sys0401 엑셀 업로드 템플릿 등록
sys0402 엑셀 컬럼 레이아웃 (자동 매핑)
sys0403 검증룰 등록 (notNull/regex/unique)
sys0404 변환룰 등록 (trim/replace)

6.5 관련 PDCA archive

6.6 외부 라이브러리 의존성

라이브러리 버전 용도
TUS Java Server 0.x 재개 가능 업로드 프로토콜
filepond 4.x 프론트엔드 업로더 (TUS 플러그인)
EasyExcel 4.x 엑셀 파싱/생성 (스트리밍)
Kryo 5.x .dat 청크 직렬화
MyBatis-Flex 1.11.6 DB 매핑
PostgreSQL 14+ UNNEST/array, information_schema

6.7 변경 이력

날짜 버전 변경 요약 작성자
2026-04-29 1.0 Guide-only 형식 초안 (엑셀 중심) 김영훈
2026-04-29 2.0 일반 업로드 전반 (TUS/filepond/Object Storage) 포함, 엑셀은 별도 섹션 + PDCA 위임 김영훈