Firo API 사용 가이드
Service API 사용법
FiroService 주요 메소드
1. 파일 업로드
임시 파일 업로드
java
@Autowired
private FiroService firoService;
// 기본 임시 업로드
String tempId = firoService.uploadTemp(
multipartFile, // MultipartFile
category, // FiroCategory
filterChain, // FiroFilterChain (null 가능)
true // 확장자 포함 여부
);
// 사용 예시
FiroCategory category = FiroRegistry.get("user", "profile");
String tempFileId = firoService.uploadTemp(file, category, null, true);
직접 업로드 (임시 과정 없이 바로 저장)
java
String savedPath = firoService.uploadDirect(
multipartFile, // MultipartFile
"user", // refDomain
"profile", // refCategory
null, // FiroFilterChain (null 가능)
LocalDateTime.now(), // 업로드 날짜
true // 확장자 포함 여부
);
2. AttachBag을 통한 파일 저장
java
// AttachBag 생성 및 파일 정보 추가
AttachBag bag = new AttachBag("user");
FiroFile file = new FiroFile();
file.setDisplayName("profile.jpg");
file.setSavedName("temp-uuid-123"); // uploadTemp에서 반환된 tempId
file.setFileType("image/jpeg");
file.setFileSize(102400L);
bag.add("profile", file);
// DB에 저장 및 실제 파일 이동
List<FiroFile> savedFiles = firoService.save(
userId, // refKey (사용자 ID)
bag, // AttachBag
LocalDateTime.now(), // 생성 날짜
false, // cleanDomain (기존 파일 삭제 여부)
false // keepTemp (임시 파일 보관 여부)
);
3. 파일 조회
ID로 단일 파일 조회
java
FiroFile file = firoService.getAttach(fileId);
참조 정보로 파일 목록 조회
java
// 도메인, 키, 카테고리로 조회
List<? extends FiroFile> files = firoService.listAttachByRef(
"user", // refDomain
123L, // refKey
"profile" // refCategory
);
// 확장자 필터 추가
List<? extends FiroFile> imageFiles = firoService.listAttachByRef(
"user", 123L, "profile", "jpg"
);
AttachBag으로 카테고리별 그룹 조회
java
AttachBag bag = firoService.getAttachBagByRef(
"user", // refDomain
123L, // refKey
null // refCategory (null이면 모든 카테고리)
);
// 특정 카테고리의 파일들 접근
List<FiroFile> profileFiles = bag.get("profile");
List<FiroFile> avatarFiles = bag.get("avatar");
여러 키에 대한 AttachBag 맵 조회
java
List<Long> userIds = Arrays.asList(1L, 2L, 3L);
Map<Long, AttachBag> bagMap = firoService.getAttachBagMapByRef(
"user", userIds, null
);
// 각 사용자별 파일 접근
AttachBag user1Files = bagMap.get(1L);
List<FiroFile> user1Profiles = user1Files.get("profile");
4. 파일 삭제
java
// 단일 파일 삭제
firoService.deleteAttach(fileId);
// 참조 정보로 파일 삭제
firoService.deleteAttachByRef("user", 123L, "profile");
// 전체 도메인 파일 삭제
firoService.deleteAttachByRef("user", 123L, null);
REST API 사용법
1. 임시 파일 업로드 API
http
POST /api/firo/attach/tmp
Content-Type: multipart/form-data
Parameters:
- file: MultipartFile (필수)
- refDomain: String (필수)
- refCategory: String (필수)
Response:
{
"success": true,
"tempId": "temp-uuid-abc123",
"originalName": "profile.jpg",
"fileSize": 102400,
"contentType": "image/jpeg"
}
2. AttachBag 저장 API
http
POST /api/firo/attach/{refDomain}/{refKey}
Content-Type: application/json
Request Body:
{
"categories": {
"profile": [{
"displayName": "profile.jpg",
"savedName": "temp-uuid-abc123",
"fileType": "image/jpeg",
"fileSize": 102400
}]
},
"cleanDomain": false,
"keepTemp": false
}
Response:
{
"success": true,
"data": {
"profile": [{
"id": 123,
"refDomain": "user",
"refKey": 456,
"refCategory": "profile",
"displayName": "profile.jpg",
"savedName": "secure-name.jpg",
"savedDir": "/user/2024/01/456/profile/",
"fileType": "image/jpeg",
"fileSize": 102400,
"createdDt": "2024-01-15T10:30:00",
"cdnUrl": "https://cdn.example.com/user/2024/01/456/profile/secure-name.jpg"
}]
}
}
3. AttachBag 조회 API
http
GET /api/firo/attach/{refDomain}/{refKey}
GET /api/firo/attach/{refDomain}/{refKey}?refCategory=profile
Response:
{
"success": true,
"data": {
"categories": {
"profile": [{
"id": 123,
"displayName": "profile.jpg",
"savedDir": "/user/2024/01/456/profile/",
"cdnUrl": "https://cdn.example.com/user/2024/01/456/profile/secure-name.jpg",
"fileSize": 102400,
"createdDt": "2024-01-15T10:30:00"
}]
}
}
}
4. 시스템 설정 조회 API
http
GET /api/firo/config
Response:
{
"success": true,
"data": {
"enabled": true,
"dbType": "jpa",
"cdnUrl": "https://cdn.example.com/",
"apiPrefix": "/api/firo",
"adapters": {
"local": {
"baseDir": "./uploads/",
"tmpDir": "./uploads/temp/"
}
},
"maxFileSize": "100MB",
"maxRequestSize": "100MB"
}
}
5. 파일 삭제 API
http
DELETE /api/firo/attach/{refDomain}/{refKey}/{refCategory}
DELETE /api/firo/attach/{refDomain}/{refKey}
Response:
{
"success": true,
"message": "파일이 성공적으로 삭제되었습니다."
}
JavaScript/TypeScript 클라이언트 예시
TypeScript 인터페이스 정의
typescript
interface FiroFile {
id: number;
refDomain: string;
refKey: number;
refCategory: string;
displayName: string;
savedName: string;
savedDir: string;
fileType: string;
fileSize: number;
deleted: boolean;
ext: string;
createdBy?: number;
createdDt: string;
cdnUrl: string;
}
interface AttachBag {
categories: Record<string, FiroFile[]>;
}
interface FiroConfig {
enabled: boolean;
dbType: string;
cdnUrl: string;
apiPrefix: string;
maxFileSize: string;
maxRequestSize: string;
}
JavaScript 클라이언트 함수
javascript
class FiroClient {
constructor(baseUrl = '', apiPrefix = '/api/firo') {
this.baseUrl = baseUrl;
this.apiPrefix = apiPrefix;
}
// 임시 파일 업로드
async uploadTemp(file, refDomain, refCategory) {
const formData = new FormData();
formData.append('file', file);
formData.append('refDomain', refDomain);
formData.append('refCategory', refCategory);
const response = await fetch(`${this.baseUrl}${this.apiPrefix}/attach/tmp`, {
method: 'POST',
body: formData
});
return await response.json();
}
// AttachBag 저장
async saveAttachBag(refDomain, refKey, attachBag) {
const response = await fetch(`${this.baseUrl}${this.apiPrefix}/attach/${refDomain}/${refKey}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(attachBag)
});
return await response.json();
}
// AttachBag 조회
async getAttachBag(refDomain, refKey, refCategory = null) {
const url = refCategory
? `${this.baseUrl}${this.apiPrefix}/attach/${refDomain}/${refKey}?refCategory=${refCategory}`
: `${this.baseUrl}${this.apiPrefix}/attach/${refDomain}/${refKey}`;
const response = await fetch(url);
return await response.json();
}
// 시스템 설정 조회
async getConfig() {
const response = await fetch(`${this.baseUrl}${this.apiPrefix}/config`);
return await response.json();
}
// 파일 삭제
async deleteAttach(refDomain, refKey, refCategory = null) {
const url = refCategory
? `${this.baseUrl}${this.apiPrefix}/attach/${refDomain}/${refKey}/${refCategory}`
: `${this.baseUrl}${this.apiPrefix}/attach/${refDomain}/${refKey}`;
const response = await fetch(url, {
method: 'DELETE'
});
return await response.json();
}
}
// 사용 예시
const firoClient = new FiroClient();
// 파일 업로드 워크플로우
async function uploadUserProfile(userId, file) {
try {
// 1. 임시 업로드
const tempResult = await firoClient.uploadTemp(file, 'user', 'profile');
// 2. AttachBag 구성
const attachBag = {
categories: {
profile: [{
displayName: file.name,
savedName: tempResult.tempId,
fileType: file.type,
fileSize: file.size
}]
},
cleanDomain: false,
keepTemp: false
};
// 3. 실제 저장
const saveResult = await firoClient.saveAttachBag('user', userId, attachBag);
return saveResult.data.profile[0].cdnUrl;
} catch (error) {
console.error('파일 업로드 실패:', error);
throw error;
}
}
Vue.js 3 Composition API 예시
파일 업로드 컴포넌트
vue
<template>
<div class="file-upload">
<div class="upload-area" @drop="onDrop" @dragover.prevent>
<input
ref="fileInput"
type="file"
@change="onFileSelect"
multiple
accept="image/*"
style="display: none"
/>
<button @click="$refs.fileInput.click()" :disabled="uploading">
{{ uploading ? '업로드 중...' : '파일 선택' }}
</button>
</div>
<div v-if="files.length" class="file-list">
<div v-for="file in files" :key="file.id" class="file-item">
<img :src="file.cdnUrl" :alt="file.displayName" />
<span>{{ file.displayName }}</span>
<button @click="removeFile(file.id)">삭제</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
refDomain: { type: String, required: true },
refKey: { type: Number, required: true },
refCategory: { type: String, required: true }
})
const files = ref([])
const uploading = ref(false)
const fileInput = ref()
const firoClient = new FiroClient()
onMounted(async () => {
await loadFiles()
})
async function loadFiles() {
try {
const result = await firoClient.getAttachBag(
props.refDomain,
props.refKey,
props.refCategory
)
if (result.success && result.data.categories[props.refCategory]) {
files.value = result.data.categories[props.refCategory]
}
} catch (error) {
console.error('파일 로드 실패:', error)
}
}
async function onFileSelect(event) {
const selectedFiles = Array.from(event.target.files)
await uploadFiles(selectedFiles)
}
async function onDrop(event) {
event.preventDefault()
const droppedFiles = Array.from(event.dataTransfer.files)
await uploadFiles(droppedFiles)
}
async function uploadFiles(fileList) {
uploading.value = true
try {
for (const file of fileList) {
// 1. 임시 업로드
const tempResult = await firoClient.uploadTemp(
file,
props.refDomain,
props.refCategory
)
// 2. AttachBag 저장
const attachBag = {
categories: {
[props.refCategory]: [{
displayName: file.name,
savedName: tempResult.tempId,
fileType: file.type,
fileSize: file.size
}]
},
cleanDomain: false,
keepTemp: false
}
const saveResult = await firoClient.saveAttachBag(
props.refDomain,
props.refKey,
attachBag
)
if (saveResult.success) {
files.value.push(...saveResult.data[props.refCategory])
}
}
} catch (error) {
console.error('업로드 실패:', error)
alert('업로드에 실패했습니다.')
} finally {
uploading.value = false
fileInput.value.value = ''
}
}
async function removeFile(fileId) {
try {
await firoClient.deleteAttach(
props.refDomain,
props.refKey,
props.refCategory
)
files.value = files.value.filter(file => file.id !== fileId)
} catch (error) {
console.error('삭제 실패:', error)
alert('파일 삭제에 실패했습니다.')
}
}
</script>
React Hooks 예시
파일 업로드 훅
tsx
import { useState, useEffect, useCallback } from 'react'
interface UseFileUploadProps {
refDomain: string
refKey: number
refCategory: string
}
export function useFileUpload({ refDomain, refKey, refCategory }: UseFileUploadProps) {
const [files, setFiles] = useState<FiroFile[]>([])
const [uploading, setUploading] = useState(false)
const [loading, setLoading] = useState(false)
const firoClient = new FiroClient()
const loadFiles = useCallback(async () => {
setLoading(true)
try {
const result = await firoClient.getAttachBag(refDomain, refKey, refCategory)
if (result.success && result.data.categories[refCategory]) {
setFiles(result.data.categories[refCategory])
}
} catch (error) {
console.error('파일 로드 실패:', error)
} finally {
setLoading(false)
}
}, [refDomain, refKey, refCategory])
useEffect(() => {
loadFiles()
}, [loadFiles])
const uploadFiles = useCallback(async (fileList: File[]) => {
setUploading(true)
try {
const newFiles: FiroFile[] = []
for (const file of fileList) {
const tempResult = await firoClient.uploadTemp(file, refDomain, refCategory)
const attachBag = {
categories: {
[refCategory]: [{
displayName: file.name,
savedName: tempResult.tempId,
fileType: file.type,
fileSize: file.size
}]
},
cleanDomain: false,
keepTemp: false
}
const saveResult = await firoClient.saveAttachBag(refDomain, refKey, attachBag)
if (saveResult.success) {
newFiles.push(...saveResult.data[refCategory])
}
}
setFiles(prev => [...prev, ...newFiles])
} catch (error) {
console.error('업로드 실패:', error)
throw error
} finally {
setUploading(false)
}
}, [refDomain, refKey, refCategory])
const removeFile = useCallback(async (fileId: number) => {
try {
await firoClient.deleteAttach(refDomain, refKey, refCategory)
setFiles(prev => prev.filter(file => file.id !== fileId))
} catch (error) {
console.error('삭제 실패:', error)
throw error
}
}, [refDomain, refKey, refCategory])
return {
files,
uploading,
loading,
uploadFiles,
removeFile,
reloadFiles: loadFiles
}
}
React 컴포넌트
tsx
import React from 'react'
import { useFileUpload } from './useFileUpload'
interface FileUploadProps {
refDomain: string
refKey: number
refCategory: string
}
export function FileUpload({ refDomain, refKey, refCategory }: FileUploadProps) {
const { files, uploading, uploadFiles, removeFile } = useFileUpload({
refDomain,
refKey,
refCategory
})
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const fileList = Array.from(event.target.files || [])
if (fileList.length > 0) {
try {
await uploadFiles(fileList)
} catch (error) {
alert('업로드에 실패했습니다.')
}
}
}
return (
<div className="file-upload">
<input
type="file"
multiple
accept="image/*"
onChange={handleFileSelect}
disabled={uploading}
/>
{uploading && <div>업로드 중...</div>}
<div className="file-list">
{files.map(file => (
<div key={file.id} className="file-item">
<img src={file.cdnUrl} alt={file.displayName} />
<span>{file.displayName}</span>
<button onClick={() => removeFile(file.id)}>삭제</button>
</div>
))}
</div>
</div>
)
}
이 가이드를 통해 Firo API를 효과적으로 활용할 수 있습니다. 더 자세한 정보는 설정 가이드와 예제 가이드를 참고하세요.