온라인 도서관에 파일 전송 기능을 추가하려고 하고 있는데, 도서 등록 기능은 현재 RestApi로 하고 있었다.
form 태그를 사용하면, 단순히 enctype=”multipart/form-data” attr을 추가해주면 되는데, restApi는 어떻게 추가할 수 있을까? 바로 FormData 객체를 사용하면, Content-Type이 자동으로 multipart/form-data로 들어간다.
addBook 함수
기존 addBook 함수를 수정했다. FormData 객체를 만들고, 객체 안에 값을 append 해주는 방식으로 추가한다. 그리고 공통으로 사용하는 fetchRequest 함수를 사용한다.#
const addBook = async errorContainer => {
const bookName = document.getElementById("addBookName").value;
const isbn = document.getElementById("addBookIsbn").value;
const coverImage = document.getElementById("addBookCoverImage").files[0]; // 파일 선택
const body = new FormData();
body.append("bookName", bookName);
body.append("isbn", isbn);
body.append("coverImage", coverImage);
try {
const data = await fetchRequest(`/books/add`, "POST", body);
if (data.redirected) {
console.log("페이지가 리다이렉트되었습니다.");
return;
}
alert("결과 : " + data.message);
location.reload();
} catch (e) {
handleError(e, errorContainer);
}
};
FetchRequest 함수
FormData 객체의 특징은, stringfy로 직렬화를 해주면 안 된다는 것이다. 당연하게도, json 데이터가 아니여서 그렇다.
fetchRequest 함수는 공통으로 사용해주어야 하기 때문에, instanceof 연산으로 분기처리를 해줬다.
export const fetchRequest = async (url, method, body = null) => {
resetErrorFields();
const options = {
method: method,
};
// body가 FormData일 경우, JSON.stringify를 사용하지 않도록 처리
if (body) {
if (body instanceof FormData) {
options.body = body; // FormData는 그대로 사용
} else {
options.body = JSON.stringify(body); // 그 외에는 JSON으로 변환
options.headers = {
'Content-Type': 'application/json', // JSON 형식인 경우 Content-Type 설정
};
}
}
const response = await fetch(url, options);
// 페이지가 리다이렉트된 경우 페이지 이동
const contentType = response.headers.get("Content-Type");
if (contentType && contentType.startsWith("text/html")) {
location.href = response.url;
return { redirected: true }; // 리다이렉트를 명시적으로 반환
}
// 리디렉션이 아닌 다른 응답 처리
if (!response.ok) {
const responseJson = await response.json();
const error = new Error(`HTTP Error! status: ${response.status}, message: ${responseJson}`);
error.status = response.status;
error.response = responseJson;
throw error;
}
return await response.json();
};
BeanValidation을 사용할 수 없다.
RestApi로 파일 전송을 할 때의 단점은, BeanValidation을 사용해서 간단히 하던 비어있는 필드, size 체크가 불가하다. @RequestBody NewForm form 으로 객체화해서 받아올 수 없기 때문인데, RequestParam으로 각각 받아온 다음, validator에서 처리했다.
기본적으로 null이 들어올 수 있게 해준 다음, validator에서 체크한다.
controller
public ResponseEntity<JsonResponse> addBook(@RequestParam(required = false) String bookName,
@RequestParam(required = false) String isbn,
@RequestParam(required = false) MultipartFile coverImage) {
NewBookForm book = new NewBookForm(bookName, isbn, coverImage);
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(book, "newBookForm");
log.debug("bindingResult.target={}", bindingResult.getTarget());
log.debug("bindingResult.objectName={}", bindingResult.getObjectName());
log.debug("book={}", book);
log.debug("book.bookName={}", book.getBookName());
log.debug("book.isbn={}", book.getIsbn());
bookAddValidator.validate(book, bindingResult);
if (bindingResult.hasErrors()) {
log.debug("errors={}", bindingResult);
return errorResponseUtils.handleValidationErrors(bindingResult);
}
adminService.addBook(book);
return ResponseEntity.ok().body(JsonResponse.builder()
.message("정상 등록되었습니다.")
.build());
}
Validator
public class BookAddValidator implements Validator {
private final BookService bookService;
private static final String DUPLICATED_FIELD = "duplicated";
private static final String MIN_FIELD = "min";
private static final String NOT_BLANK_FIELD = "NotBlank";
private static final String NOT_NULL_FIELD = "NotNull";
private static final int BOOK_NAME_MIN_SIZE = 5;
private static final int ISBN_MIN_SIZE = 5;
@Override
public boolean supports(Class<?> clazz) {
return NewBookForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
NewBookForm book = (NewBookForm) target;
String bookName = book.getBookName();
String isbn = book.getIsbn();
MultipartFile coverImage = book.getCoverImage();
boolean isBookNameEmpty = !StringUtils.hasText(bookName);
boolean isIsbnEmpty = !StringUtils.hasText(isbn);
boolean isCoverImageEmpty = coverImage == null;
boolean isBookNameTooShort = bookName.length() < BOOK_NAME_MIN_SIZE;
boolean isIsbnTooShort = isbn.length() < ISBN_MIN_SIZE;
if (isBookNameEmpty || isIsbnEmpty || isCoverImageEmpty ||
isBookNameTooShort || isIsbnTooShort
) {
if (isBookNameEmpty) {
errors.rejectValue("bookName", NOT_BLANK_FIELD);
} else if (isBookNameTooShort) {
errors.rejectValue("bookName", MIN_FIELD, new Object[]{BOOK_NAME_MIN_SIZE}, null);
}
if (isIsbnEmpty) {
errors.rejectValue("isbn", NOT_BLANK_FIELD);
} else if (isIsbnTooShort) {
errors.rejectValue("isbn", MIN_FIELD, new Object[]{ISBN_MIN_SIZE}, null);
}
if (isCoverImageEmpty) {
errors.rejectValue("coverImage", NOT_NULL_FIELD);
}
return;
}
if (isDuplicated(isbn)) {
errors.rejectValue("isbn", DUPLICATED_FIELD, null);
}
}
private boolean isDuplicated(String isbn) {
return bookService.findBookByIsbn(isbn).isPresent();
}
}