Skip to content

REST API 레퍼런스

cms/channel 프론트엔드를 쓴다면 이 API 를 직접 호출할 일은 거의 없습니다. 모바일 앱 · 외부 시스템 연동 · 커스텀 업로더를 만들 때 참고하세요.

Firo 는 두 개의 URL 그룹을 제공합니다.

그룹용도인증
/api/firo/**업로드 · 조회 · 수정 · 삭제 (JSON)프로젝트의 일반 API 와 동일
/assets/firo/**파일 뷰 · 다운로드 (바이너리 스트리밍)시큐리티 필터 밖 — 보호는 secureAccessFunc

날짜 포맷

모든 일시는 ISO-8601 UTC(2026-07-25T04:12:33.123Z) 입니다.


업로드

POST /api/firo/attach/tmp — 임시 업로드 (2단계 커밋 1단계)

사용자가 파일을 고른 즉시 호출합니다. 필터 체인이 실행되고 파일은 temp 영역에 대기합니다. 아직 DB 레코드는 생기지 않습니다.

Content-Type: multipart/form-data

파라미터필수설명
refDomain도메인 코드
refCategory카테고리 (미지정 시 default)
file 또는 fileMap단일 파일 / 여러 파일
filters요청별 필터 JSON. 등록된 키만 동작 (기본: size, dimension)
resultKey응답의 목록 키 이름 (기본 files)
useTempFileExtension(폐기) 수신만 하고 무시 — 확장자는 항상 유지됩니다
bash
curl -X POST http://localhost:8080/api/firo/attach/tmp \
  -H "Authorization: Bearer $TOKEN" \
  -F 'refDomain=product' \
  -F 'refCategory=main' \
  -F 'filters={"size":{"maxSize":5}}' \
  -F 'file=@photo.jpg'
json
{
  "files": [
    {
      "name": "6f1c0f2a-2b71-4a0e-9d3e-8f2b1c0a5e77.jpg",
      "displayName": "photo.jpg",
      "size": 204812,
      "type": "image/jpeg",
      "url": null,
      "thumbnailUrl": null,
      "deleteUrl": null,
      "deleteType": "DELETE"
    }
  ]
}

name 을 잘 보관하세요

namesavedName 입니다. 다음 단계(확정 저장)에서 이 값을 그대로 넘겨야 합니다.

필터가 거부하면

HTTP 는 200 이지만 해당 파일 항목에 error 가 들어옵니다 — 응답 코드만 보지 말고 항목별 error 를 확인하세요.

json
{ "files": [ { "error": { "code": "size", "message": "File size exceeds ..." } } ] }

임시 파일 미리보기는 다음 URL 로 가능합니다.

GET /assets/firo/attach-temp/view/{refDomain}/{refCategory}/{savedName}

POST /api/firo/attach/{refDomain}/{refKey} — 확정 저장 (2단계)

temp 파일을 최종 경로로 옮기고 nv_attach 레코드를 만듭니다.

보통은 직접 호출하지 않습니다

백엔드에 @FiroUpload 를 쓰면 본체 저장 API 가 이 일을 대신합니다. 이 엔드포인트는 첨부만 따로 저장해야 할 때 사용합니다.

저장 디렉토리 기준은 Instant.now() 입니다

이 엔드포인트는 엔터티를 모르므로 yyyy/MM호출 시각으로 정합니다. 엔터티의 createdDt 기준으로 묶고 싶다면 @FiroUpload aspect 경로(본체 저장 API)를 쓰세요.

요청 본문은 AttachBag카테고리 → 파일 목록 Map 입니다.

bash
curl -X POST http://localhost:8080/api/firo/attach/product/35 \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "refDomain": "product",
        "main": [
          { "savedName": "6f1c0f2a-....jpg", "displayName": "photo.jpg",
            "fileSize": 204812, "fileType": "image/jpeg", "ext": "대표 이미지" }
        ],
        "_deleted": [ { "id": 10 } ]
      }'
필드설명
savedNametemp 업로드 응답의 name
displayName원본 파일명. 다운로드 시 이 이름으로 내려갑니다
fileSize / fileTypetemp 업로드 응답의 size / type
ext자유 부가 정보 (nv_attach.attach_ext). 설명 텍스트 등
id있으면 기존 첨부 — 순서 변경만 반영합니다
savedDir있으면 물리 파일 재사용 — 파일을 복사하지 않고 DB 레코드만 새로 만듭니다
_deleted삭제할 첨부 목록. 물리 파일까지 영구 삭제됩니다

배열 순서 = 표시 순서

각 항목의 배열 index 가 attach_sort 로 저장됩니다. 순서를 바꿔 다시 저장하면 물리 파일·파일명은 그대로 두고 sort 값만 갱신됩니다.

성공 응답:

json
{ "success": true, "attachList": [ { "id": 1024, "savedName": "...", "sort": 0, ... } ] }

부분 실패 응답 (500):

json
{
  "success": false,
  "code": "attach-save-failure",
  "message": "첨부 저장 실패 1건 (성공 2건)",
  "failures": [
    { "refCategory": "main", "index": 2, "displayName": "broken.png", "reason": "..." }
  ]
}

부분 실패는 재시도로 복구합니다

성공한 첨부는 이미 저장돼 있습니다. failures 에 나온 항목만 다시 업로드하세요. 전체를 재전송하면 성공분이 중복 저장됩니다.

POST /api/firo/attach/direct — 즉시 확정 업로드

temp 단계 없이 바로 영구 경로에 저장합니다. 에디터 본문 이미지처럼 "폼 저장" 개념이 없는 첨부용입니다.

파라미터는 tmp 와 동일합니다(file 대신 files 로 여러 개 전송 가능. useFileExtension 은 폐기·무시).

json
{
  "files": [
    {
      "name": "9a2c....png",
      "savedDir": "/ckupload/2026/07/_/default/",
      "url": "/assets/firo/attach-direct/view/ckupload/default?path=%2Fckupload%2F...",
      "displayName": "screenshot.png",
      "size": 88123,
      "type": "image/png"
    }
  ]
}

저장 경로는 {refDomain}/{yyyy}/{MM}/_/{refCategory}/{uuid}.{ext} 이며, CDN 이 설정돼 있으면 url 이 CDN 주소로 내려옵니다.


조회 · 수정 · 삭제

GET /api/firo/attach/{refDomain}/{refKey}[/{refCategory}]

해당 참조의 첨부를 AttachBag(카테고리 → 목록) 으로 반환합니다. attach_sort 입니다.

bash
curl http://localhost:8080/api/firo/attach/product/35/main
json
{
  "refDomain": "product",
  "main": [
    {
      "id": 1024,
      "refDomain": "product",
      "refKey": 35,
      "refCategory": "main",
      "displayName": "photo.jpg",
      "savedName": "6f1c0f2a-....jpg",
      "savedDir": "/product/2026/07/35/",
      "fileType": "image/jpeg",
      "fileSize": 204812,
      "ext": "대표 이미지",
      "sort": 0,
      "createdBy": 1,
      "createdDt": "2026-07-25T04:12:33.123Z",
      "url": "https://cdn.example.com/product/2026/07/35/6f1c0f2a-....jpg"
    }
  ]
}

url 은 서버가 채워줍니다.

상황url
CDN 설정 있음cdnUrl + savedDir + savedName
CDN 없음 (일반 업로드)/assets/firo/attach/view/{refDomain}/{refKey}/{refCategory}/{index}
CDN 없음 (direct 업로드)/assets/firo/attach-direct/view/{refDomain}/{refCategory}?path=...

그 밖의 엔드포인트

엔드포인트설명
GET /api/firo/attach/{refDomain}/{refKey}/{refCategory}/_count첨부 건수 (숫자 하나 반환)
GET /api/firo/attach?...조건 목록 조회 (refKeyList, refCategoryList 등)
PUT /api/firo/attach/첨부 메타 수정. body: List<FiroFile> (URL 끝의 / 필수)
POST /api/firo/attach영구 삭제 (물리 파일 포함). body: List<FiroFile>
GET /api/firo/config등록된 도메인/카테고리와 CDN URL 조회

POST /api/firo/attach 는 삭제입니다

업로드용 엔드포인트(/attach/tmp, /attach/{refDomain}/{refKey})와 경로가 비슷하지만 카테고리 없는 POST /api/firo/attach 는 영구 삭제입니다. 물리 파일까지 지워지며 복구되지 않습니다.

POST /api/firo/direct-url — CDN 직접 접근 URL 조회

(refDomain, refCategory, refKey)sort 순 index 번째 첨부의 실제 URL 을 반환합니다.

bash
curl -X POST http://localhost:8080/api/firo/direct-url \
  -H 'Content-Type: application/json' \
  -d '{ "refDomain": "product", "refCategory": "main", "refKey": "35", "index": 0 }'
"https://cdn.example.com/product/2026/07/35/6f1c0f2a-....jpg"

조회형입니다 — 저장된 첨부만 존재합니다

과거처럼 파일명을 계산하지 않고 DB 를 조회합니다. 따라서

  • 아직 저장하지 않은(temp) 파일에는 URL 이 없습니다 → 폼 저장 후에 생깁니다
  • 해당 참조에 첨부가 없거나 index 가 범위를 벗어나면 404

대신 실제 저장명 기반이라 확장자가 항상 정확하고, 파일명이 불변이라 캐시버스터 파라미터가 필요 없습니다.

createdDt 파라미터는 하위호환을 위해 받기만 하고 사용하지 않습니다.

CKEditor 전용

엔드포인트설명
POST /api/firo/ckupload/dnd드래그&드롭 업로드
POST /api/firo/ckupload/modal다이얼로그 업로드
GET /assets/firo/editor/image/**에디터 이미지 서빙

파일 보기 · 다운로드 (/assets/firo)

경로의 {action} 자리에 view(브라우저 인라인 표시 + 캐시 헤더) 또는 download(Content-Disposition: attachment) 를 넣습니다.

엔드포인트설명
GET /attach/{action}/{id}첨부 ID 로 단건
GET /attach/{action}/{refDomain}/{refKey}/{refCategory}[/{idx}]참조 기준 (idx0부터, 생략 시 0)
GET /attach-temp/{action}/{refDomain}/{refCategory}/{savedName}저장 전 temp 파일 미리보기
GET /attach-direct/{action}/{refDomain}/{refCategory}?path=direct 업로드 파일
POST /attach/downloadZIP 일괄 다운로드
html
<!-- 상품 35번 main 첫 번째 이미지 -->
<img src="/assets/firo/attach/view/product/35/main">

<!-- 두 번째 이미지를 가로 300px 로 -->
<img src="/assets/firo/attach/view/product/35/main/1?w=300">

<!-- 원본 파일명으로 다운로드 -->
<a href="/assets/firo/attach/download/1024">내려받기</a>

서버 리사이즈 (w / h)

?w=300 또는 ?h=200 으로 축소본을 요청합니다. 한쪽만 주면 비율을 유지합니다.

  • 생성된 축소본은 store 에 캐시되므로 두 번째 요청부터는 즉시 응답합니다.
  • 원본이 요청 크기보다 작으면 확대하지 않고 원본을 그대로 줍니다.

temp / direct 뷰는 리사이즈되지 않습니다

attach-temp, attach-direct 경로는 DB 레코드(id·savedDir)가 없어 캐시본을 만들 수 없습니다. w/h 를 줘도 원본이 반환됩니다. 정식 저장 후 /attach/view/{id}?w=... 를 쓰세요.

필터 파라미터 (참조 기준 조회 시)

파라미터설명
ext첨부의 ext 값이 일치하는 것만
extAlt: 로 구분한 우선순위 목록. 앞에서부터 찾고 없으면 첫 번째 첨부 (extAlt=webp:jpg)
fmetaValue메타 값으로 필터

ZIP 일괄 다운로드

bash
curl -X POST http://localhost:8080/assets/firo/attach/download \
  -H 'Content-Type: application/json' \
  -d '{ "title": "상품35_이미지", "ids": [1024, 1025, 1026] }' \
  -o images.zip
  • title.zip 이 없으면 자동으로 붙습니다. 생략하면 {도메인}_{카테고리}_{timestamp}.zip
  • 파일명이 겹치면 photo.jpg, photo_1.jpg 처럼 자동 구분됩니다
  • 접근 권한이 없거나 없는 파일은 건너뛰고 나머지를 압축합니다 (전부 실패하면 404)

접근 제어와 응답 특성

  • 카테고리에 secureAccessFunc 가 설정돼 있으면 모든 경로에서 검사하고, 거부 시 403
  • 스트리밍 응답은 Content-Length 를 생략할 수 있습니다(chunked) — 필터로 인해 실제 파일 크기가 DB 값과 다를 수 있어 신뢰하지 않기 때문입니다
  • viewIf-Modified-Since 를 지원해 변경 없으면 304 를 반환합니다

상태 코드 정리

코드의미
200성공. 단, temp 업로드는 항목별 error 를 확인해야 합니다
400파일이 없음 (file/fileMap 누락)
403secureAccessFunc 가 거부
404첨부 없음 / direct-url 의 index 범위 초과
413요청 크기 초과 — spring.servlet.multipart.max-* 설정 확인
500 + attach-save-failure확정 저장 부분 실패. failures 항목만 재시도