Skip to content

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를 효과적으로 활용할 수 있습니다. 더 자세한 정보는 설정 가이드예제 가이드를 참고하세요.