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 | (폐기) 수신만 하고 무시 — 확장자는 항상 유지됩니다 |
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'{
"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 을 잘 보관하세요
name 이 savedName 입니다. 다음 단계(확정 저장)에서 이 값을 그대로 넘겨야 합니다.
필터가 거부하면
HTTP 는 200 이지만 해당 파일 항목에 error 가 들어옵니다 — 응답 코드만 보지 말고 항목별 error 를 확인하세요.
{ "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 입니다.
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 } ]
}'| 필드 | 설명 |
|---|---|
savedName | temp 업로드 응답의 name |
displayName | 원본 파일명. 다운로드 시 이 이름으로 내려갑니다 |
fileSize / fileType | temp 업로드 응답의 size / type |
ext | 자유 부가 정보 (nv_attach.attach_ext). 설명 텍스트 등 |
id | 있으면 기존 첨부 — 순서 변경만 반영합니다 |
savedDir | 있으면 물리 파일 재사용 — 파일을 복사하지 않고 DB 레코드만 새로 만듭니다 |
_deleted | 삭제할 첨부 목록. 물리 파일까지 영구 삭제됩니다 |
배열 순서 = 표시 순서
각 항목의 배열 index 가 attach_sort 로 저장됩니다. 순서를 바꿔 다시 저장하면 물리 파일·파일명은 그대로 두고 sort 값만 갱신됩니다.
성공 응답:
{ "success": true, "attachList": [ { "id": 1024, "savedName": "...", "sort": 0, ... } ] }부분 실패 응답 (500):
{
"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 은 폐기·무시).
{
"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 순 입니다.
curl http://localhost:8080/api/firo/attach/product/35/main{
"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 을 반환합니다.
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}] | 참조 기준 (idx 는 0부터, 생략 시 0) |
GET /attach-temp/{action}/{refDomain}/{refCategory}/{savedName} | 저장 전 temp 파일 미리보기 |
GET /attach-direct/{action}/{refDomain}/{refCategory}?path= | direct 업로드 파일 |
POST /attach/download | ZIP 일괄 다운로드 |
<!-- 상품 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 일괄 다운로드
curl -X POST http://localhost:8080/assets/firo/attach/download \
-H 'Content-Type: application/json' \
-d '{ "title": "상품35_이미지", "ids": [1024, 1025, 1026] }' \
-o images.ziptitle에.zip이 없으면 자동으로 붙습니다. 생략하면{도메인}_{카테고리}_{timestamp}.zip- 파일명이 겹치면
photo.jpg,photo_1.jpg처럼 자동 구분됩니다 - 접근 권한이 없거나 없는 파일은 건너뛰고 나머지를 압축합니다 (전부 실패하면
404)
접근 제어와 응답 특성
- 카테고리에
secureAccessFunc가 설정돼 있으면 모든 경로에서 검사하고, 거부 시403 - 스트리밍 응답은
Content-Length를 생략할 수 있습니다(chunked) — 필터로 인해 실제 파일 크기가 DB 값과 다를 수 있어 신뢰하지 않기 때문입니다 view는If-Modified-Since를 지원해 변경 없으면304를 반환합니다
상태 코드 정리
| 코드 | 의미 |
|---|---|
200 | 성공. 단, temp 업로드는 항목별 error 를 확인해야 합니다 |
400 | 파일이 없음 (file/fileMap 누락) |
403 | secureAccessFunc 가 거부 |
404 | 첨부 없음 / direct-url 의 index 범위 초과 |
413 | 요청 크기 초과 — spring.servlet.multipart.max-* 설정 확인 |
500 + attach-save-failure | 확정 저장 부분 실패. failures 항목만 재시도 |