티스토리 뷰
안녕하세요. 오늘은 SpringBoot 와 Vue를 활용해 문자열 데이터와 멀티파트 파일을 동시에 REST API로 주고받는 방법을 알아보겠습니다. JSON 형식의 문자열 데이터와, 관련 파일을 함께 업로드하는 과정을 단계별로 소스 코드와 함께 설명하겠습니다.
목표
- 프론트엔드: Vue에서 FormData를 사용해 태스크 데이터(JSON)와 파일을 서버로 전송.
- 백엔드: Spring Boot에서 @RequestPart와 MultipartHttpServletRequest로 데이터를 받아 처리.
- 결과: 태스크별로 파일을 분배하고, 데이터베이스에 저장.
1. 프론트엔드 구현(Vue)
Vue에서는 axios를 활용해 multipart/form-data 요청을 생성합니다. 태스크 데이터를 JSON 문자열로, 파일은 태스크별로 구분해 전송합니다.
export async function createTask(data: TaskRegistRequest[]): Promise<boolean> {
const formData = new FormData();
// 태스크 데이터를 JSON으로 변환
const taskData = data.map(task => ({
clientId: task.clientId,
taskMajor: task.taskMajor,
processorId: task.processorId,
dueDate: task.dueDate,
// ... 나머지 필드
}));
formData.append('tasks', JSON.stringify(taskData));
// 태스크별 파일 추가
data.forEach((task, taskIndex) => {
if (task.files && task.files.length > 0) {
task.files.forEach(file => {
formData.append(`files[${taskIndex}]`, file); // 태스크 인덱스로 파일 구분
});
}
});
// API 호출
const response = await callMultipart<ApiResponse<TaskInfo[]>>(
'/api/task',
'POST',
formData
);
if (response.status !== 200 || !response.result) {
throw new Error(response.message || 'Task 등록 실패');
}
return true;
}
export async function callMultipart<T>(
url: string,
method: 'POST' | 'PUT' = 'POST',
data: FormData
): Promise<T> {
const response = await apiClient.request<T>({
url,
method,
data,
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
}
- FormData 에 tasks 키로 JSON 문자열을 추가
- 파일은 files[0], files[1] 처럼 task 인덱스를 붙여 구분
- multipart/form-data 헤더로 요청 전송
2. 백엔드 구현(SpringBoot)
Spring Boot에서는 MultipartHttpServletRequest를 사용해 문자열과 파일을 동시에 처리합니다. 태스크별 파일을 매핑해 저장합니다.
Controller
@PostMapping("/api/task")
public ResponseEntity<?> addTask(
@RequestPart("tasks") String tasksJson,
MultipartHttpServletRequest request
) throws IOException {
// JSON 문자열 맵핑
ObjectMapper objectMapper = new ObjectMapper();
List<TaskRegistRequest> taskRequests
= objectMapper.readValue(tasksJson, new TypeReference<>() {});
// 파일 매핑
Map<String, List<MultipartFile>> filesMap = new HashMap<>();
Map<String, List<MultipartFile>> multiFileMap = request.getMultiFileMap();
for (Map.Entry<String, List<MultipartFile>> entry : multiFileMap.entrySet()) {
String key = entry.getKey(); // 예: "files[0]"
List<MultipartFile> files = entry.getValue();
String indexStr = key.replaceAll("files\\[(\\d+)\\]", "$1");
if (!indexStr.equals(key)) {
int taskIndex = Integer.parseInt(indexStr);
filesMap.put("files[" + taskIndex + "]", files);
}
}
// 태스크에 파일 연결
if (!filesMap.isEmpty()) {
for (int i = 0; i < taskRequests.size(); i++) {
String key = "files[" + i + "]";
List<MultipartFile> files = filesMap.get(key);
if (files != null && !files.isEmpty()) {
taskRequests.get(i).setFiles(files.toArray(new MultipartFile[0]));
}
}
}
return ResponseEntity.ok(taskService.addTasks(taskRequests));
}
-
@RequestPart("tasks")로 JSON 문자열 수신
-
MultipartHttpServletRequest.getMultiFileMap()으로 태스크별 파일 리스트 추출
Service
@Transactional
public List<TaskInfo> addTask(List<TaskRegistRequest> taskRequests) throws IOException {
List<TaskInfo> tasks = taskRequests.stream()
.map(taskRequest -> {
TaskInfo task = new TaskInfo();
task.setTaskMajor(taskRequest.getTaskMajor());
task.setProcessorId(taskRequest.getProcessorId());
task.setDueDate(taskRequest.getDueDate() != null ?
LocalDate.parse(taskRequest.getDueDate()) : null);
// ... 나머지 필드 매핑
return task;
})
.collect(Collectors.toList());
// JPA Insert 후 taskId 반환
List<TaskInfo> savedTasks = taskInfoRepository.saveAll(tasks);
// 파일 저장(taskId 맵핑)
for (int i = 0; i < savedTasks.size(); i++) {
TaskRegistRequest request = taskRequests.get(i);
if (request.getFiles() != null && request.getFiles().length > 0) {
fileService.uploads(request.getFiles(), savedTasks.get(i).getTaskId());
}
}
return savedTasks;
}
Upload
@Transactional
public FileInfo upload(MultipartFile file, String targetId) throws IOException {
// 파일ID 채번
String fileId = fileInfoRepository.generateFileId();
// 파일명 & 확장자 추출
String fileName = file.getOriginalFilename();
String fileExtension = getFileExtension(fileName);
// 경로지정
String uploadDir = String.format("%s/%s/", uploadDir,
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")));
// 파일저장
Files.createDirectories(Paths.get(uploadDir));
Path filePath = Paths.get(uploadDir + fileId + fileExtension);
Files.write(filePath, file.getBytes());
return fileInfo;
}
통합 동작
- Vue에서 태스크 데이터와 파일을 FormData로 묶어 전송.
- Spring Boot가 tasks와 files[0], files[1] 등을 수신.
- 백엔드에서 태스크별 파일을 매핑하고, 데이터베이스에 저장.
- 파일은 지정된 디렉토리에 저장되고, 태스크에 파일 ID가 연결.
Spring Boot와 Vue를 사용하면 문자열과 멀티파트 파일을 동시에 처리하는 REST API를 쉽게 구현할 수 있습니다. FormData와 MultipartHttpServletRequest를 활용한 이 방식은 확장성도 뛰어나며, 파일 업로드가 필요한 다양한 시나리오에 적용할 수 있습니다.
감사합니다.
'프레임워크 > SpringBoot' 카테고리의 다른 글
[SpringBoot] HTTP 요청 다루기(@RequestBody, @RequestParam, @ModelAttribute) (0) | 2025.04.08 |
---|---|
[SpringBoot] RabbitMQ 튜토리얼 (0) | 2025.03.10 |
[SpringBoot] RestErrorAdvice 설정하는 방법(SpringSecurity+JWT) (0) | 2025.01.16 |
[SpringBoot] Docker 배포 가이드 (2) | 2024.12.30 |
[SpringBoot] JPA + GraphQL 연동 예제 (2) | 2024.12.23 |
최근에 올라온 글
- Total
- Today
- Yesterday