Skip to content

Firo — 파일 업로드 플랫폼

Firo 는 iflex 에 내장된 파일 첨부 플랫폼입니다. 파일이 실제로 어디에 저장되든 (서버 디스크 · AWS S3 · Azure Blob · FTP · SFTP) 개발자는 항상 같은 코드로 첨부 기능을 만듭니다.

이 문서를 처음 읽는다면

핵심 개념 을 훑고 바로 Quick Start 로 가세요. 30분이면 새 화면에 첨부 기능을 붙일 수 있습니다.

왜 Firo 를 쓰나

첨부 기능을 직접 만들면 보통 이런 코드를 매번 다시 씁니다.

  • 업로드 API, 저장 디렉토리 정하기, 파일명 충돌 방지
  • 첨부 테이블 만들기, 엔터티와 연결하기, 수정 화면에서 기존 파일 불러오기
  • 이미지 리사이즈 · EXIF 회전 보정 · 확장자 검증 · 용량 제한
  • 나중에 "S3 로 옮기자" 가 되면 전부 다시 작성

Firo 는 이 전부를 대신합니다. 개발자가 하는 일은 다음 세 가지뿐입니다.

하는 일코드
① 엔터티에 "나는 첨부를 가진다" 고 표시@FiroRef("product")
② 저장 API 에 "첨부도 같이 확정해줘" 라고 표시@FiroUpload
③ 화면에 업로드 컴포넌트 배치<nv-file-upload ref-domain="product" ... />

나머지(임시 저장, 필터링, 물리 저장, DB 레코드, 조회, 삭제, 뷰 URL)는 Firo 가 처리합니다.

왜 별도 SQL 없이 첨부를 가져올 수 있나

첨부 정보는 nv_attach 테이블 한 곳에 (refDomain, refKey, refCategory) 키로 모입니다. 업무 테이블에 image_path 같은 컬럼을 만들 필요가 없고, 업무 쿼리에 조인을 추가하지 않아도 /assets/firo/attach/view/{도메인}/{키}/{카테고리} URL 만으로 이미지를 바로 표시할 수 있습니다.

핵심 개념 (3분)

Domain(도메인)과 Category(카테고리)

Firo 는 첨부를 2단계로 분류합니다.

refDomain "product"        ← 업무 엔터티 단위 (보통 테이블 1개 = 도메인 1개)
   ├── refCategory "main"  ← 용도별 구분 (대표 이미지)
   ├── refCategory "list"  ← 목록용 이미지 (여러 장)
   └── refCategory "manual"← 사용설명서 PDF

여기에 refKey(엔터티의 PK)가 더해져 첨부 하나가 유일하게 식별됩니다.

product 도메인의 35번 상품의 main 카테고리 첨부 → product / 35 / main

카테고리를 생략하면

카테고리를 지정하지 않으면 default 라는 이름의 카테고리로 들어갑니다. 첨부 용도가 한 가지뿐이면 카테고리를 신경 쓰지 않아도 됩니다.

예시 — 이 문서 전체에서 사용할 가상의 애플리케이션입니다.

refDomainrefCategory설명
boarddefault게시판 첨부파일. 용도가 하나뿐이라 기본 카테고리만 사용
productmain상품 대표 이미지. 1장만
list상품 상세 이미지. 여러 장, 순서 있음

Store(저장소)

Store 는 "이름 붙인 저장소" 입니다. 접속 정보 + 디렉토리 정책 + CDN 주소를 하나로 묶은 것으로, 도메인/카테고리는 store 를 이름으로 참조합니다.

yaml
firo:
  default-store: local
  stores:
    local:    { type: local }
    s3-main:  { type: s3, bucket: my-images, region: ap-northeast-2 }
    s3-docs:  { type: s3, bucket: my-documents, region: ap-northeast-2 }
  domains:
    product:  { store: s3-main }     # 상품 이미지는 이미지 버킷에
    contract: { store: s3-docs }     # 계약서는 문서 버킷에

같은 타입(S3)을 여러 개 선언할 수 있는 것이 핵심입니다 — "S3 어댑터는 하나뿐" 같은 제약이 없습니다.

첨부의 일생 (temp 2단계 커밋)

Firo 의 가장 중요한 동작 원리입니다. 파일 선택 시점과 폼 저장 시점이 다르다는 현실을 다룹니다.

① 사용자가 파일 선택

        ├─► POST /api/firo/attach/tmp        (즉시 서버로 전송)
        │      · 로컬 스테이징에 저장
        │      · 필터 체인 실행 (리사이즈 · 용량검증 · EXIF 보정 …)
        │      · 원격 store 면 store 의 temp 영역에도 복사

        ◄── uuid.ext 반환 → 화면의 model.attachContainer 에 보관
                             (아직 DB 레코드 없음 · 미리보기만 가능)

② 사용자가 [저장] 버튼 클릭

        ├─► POST /api/products  (본문에 attachContainer 가 함께 실려감)
        │      · 컨트롤러가 상품 저장 → id 채번
        │      · @FiroUpload aspect 가 attachContainer 를 읽음
        │      · temp → 최종 경로로 copy + nv_attach 레코드 생성

        ◄── 저장 완료. 이제 첨부에 id 가 생기고 조회 · 뷰 URL 사용 가능

왜 2단계인가

① 만 하고 폼을 저장하지 않으면 그 파일은 확정되지 않습니다. 즉 사용자가 파일을 골랐다가 저장하지 않고 화면을 떠나도 DB 에는 아무 흔적이 남지 않습니다. 반대로 말하면, 폼을 저장해야 첨부가 실제로 남습니다.

저장 파일명과 경로

저장되는 물리 파일명은 {uuid}.{확장자} 로, 한 번 정해지면 절대 바뀌지 않습니다.

{base-dir}/{refDomain}/{yyyy}/{MM}/{refKey}/{uuid}.{ext}

예) /data/attach/product/2026/07/35/6f1c0f2a-....jpg
특성의미
확장자 항상 유지브라우저가 Content-Type 을 올바르게 인식 (.jpg 는 이미지로 인라인 표시)
파일명 불변파일을 교체하면 항상 새 이름 → CDN 캐시 무효화가 필요 없음
순서는 파일명이 아님표시 순서는 nv_attach.attach_sort 컬럼이 담당. 재정렬해도 물리 파일은 그대로

direct 업로드

에디터 본문 이미지처럼 "폼 저장" 개념이 없는 첨부는 temp 단계 없이 즉시 영구 저장합니다. 경로는 {base-dir}/{refDomain}/{yyyy}/{MM}/_/{refCategory}/{uuid}.{ext} 로, _ 디렉토리가 direct 업로드 표식입니다.

용어 정리

용어
refDomain업무 엔터티 단위 코드. @FiroRef("product") 의 값
refKey엔터티의 PK. 기본적으로 id 필드를 봅니다
refCategory도메인 안의 용도 구분. 미지정 시 default
AttachBag카테고리 → 파일 목록 Map. { "main": [파일…], "list": [파일…] }
AttachContainer도메인 → AttachBag Map. 한 요청에 여러 도메인 첨부가 있을 때 사용
attachContainer (프론트)위 구조를 그대로 담는 모델 속성. 업로드 컴포넌트가 자동 생성
savedName실제 저장 파일명 (uuid.ext). temp 업로드 응답의 name 이 이 값
displayName사용자가 올린 원본 파일명. 다운로드 시 이 이름으로 내려갑니다
store이름 붙인 저장소 (접속정보 + 경로정책 + CDN)

설정 상속 규칙

설정은 명시한 값만 저장하고, 빠진 값은 조회 시점에 상위에서 가져옵니다. 그래서 도메인 설정을 나중에 바꿔도 하위 카테고리에 바로 반영됩니다.

항목상속 순서
store(어댑터) / 경로 정책 / 접근 제어categorydomain → 글로벌 기본값(default-store)
cdnUrlcategory해석된 storedomain → 글로벌 기본값

cdnUrl 만 순서가 다른 이유

파일이 실제로 저장된 store 의 CDN 이 도메인 상속값보다 우선해야 합니다. 그렇지 않으면 "S3 버킷 A 에 저장했는데 URL 은 버킷 B 의 CDN" 같은 불일치가 생깁니다.

문서 안내

문서언제 읽나
Quick Start처음 시작. 새 엔터티에 첨부를 붙이는 전 과정
실전 예제 모음다중 파일 · 순서 변경 · 이미지 리사이즈 · 서버에서 직접 다루기 등 시나리오별 코드
설정 가이드S3/Azure 연결, 필터, 접근 제어, 환경별 설정
프론트엔드 연동nv-file-upload / v-firo-upload / firo-img 상세 레퍼런스
REST API 레퍼런스직접 API 를 호출할 때 (모바일 앱, 외부 연동 등)
트러블슈팅안 될 때
내부 구조가 궁금하다면 (기여자용)

저장소 어댑터 SPI, 부팅 조립 순서, 해석 규칙 등 내부 설계는 리포지토리 문서를 참고하세요.

구버전 문서 주의

과거 별도 라이브러리(com.unvus.firo) 및 멀티모듈(firo-core/firo-jpa/firo-mybatis) 시절의 문서·블로그 글은 현재 코드와 맞지 않습니다. 현재 Firo 는 iflex core 에 내장되어 있고 (core/platform/firo), DB 계층은 MyBatis 전용이며, 별도 의존성 추가가 필요 없습니다.