Spring

List<Object>와 MultipartFile을 한번의 요청으로 주고받는 방법

기억용블로그 2022. 6. 5. 15:41
728x90

상황은 다음과 같았다.

유저는 상품을 등록할 수 있고 상품은 메인 옵션, 세부 옵션, 가격, 재고라는 한 요소와 공통적인 상품명, 설명, 사진 등을 가진다.

옵션에 관련된 요소는 여러개를 등록할 수 있다.

이 상품을 한번의 요청으로 보낼 수 있어야 한다.

 

문제는 옵션을 등록하지 않는 경우에 대한 form을 미리 만들어 두었고

같은 페이지를 사용하지만 해당 form을 이용하는 것이 아닌 새로운 방법을 통해 값을 보내고자 하였다.

 

프론트

먼저 유저가 상품에 대해 등록하면 바로 확인할 수 있게 보여주고자 하였다.

이전에는 createElement라는 방법을 사용했는데 더 찾아보니 cloneNode(true)라는 아주 훌륭한 방법이 있었다.

미리 원본을 만들어두고 그 원본을 복사해서 사용하는 방식이다. 원본은 class="d-none"을 주어 숨겨두면 되고.

 

옵션 추가 버튼을 누르면 유저가 입력한 값을 받아오고 가공과 유효성 검사를 거친 다음에

optionCard라는 요소를 미리 만들어두고 그것을 복제해서 변수에 할당한다.

 

            let optionArray = new Array();
            let index = 0;
            document.getElementById('optionAddButton').addEventListener('click', () => {
                let option1 = document.getElementById('option1').value.trim();
                let option2 = document.getElementById('option2').value.trim();
                let optionSalePrice = document.getElementById('optionSalePrice').value.trim();
                let optionOriginalPrice = document.getElementById('optionOriginalPrice').value.trim();
                let optionStock = document.getElementById('optionStock').value.trim();


                option1 = regExp(option1);
                option2 = regExp(option2);

                if (option1 == '' || option2 == '') {
                    alert('옵션을 적어주세요');
                    return false;
                }
                if (optionSalePrice == '') {
                    alert('판매가를 적어주세요');
                    return false;
                }
                if (optionStock == '') {
                    alert('재고를 적어주세요');
                    return false;
                }
                if (option1.length > 10 || option2.length > 10) {
                    alert('10글자 이내로 적어주세요');
                    return false;
                }

                let opitonCardOriginal = document.getElementById('optionCard');
                let optionCard = opitonCardOriginal.cloneNode(true);

 

그 다음 해당 clone을 컨트롤할 수 있어야 하므로 clone의 모든 node에 id를 붙이는 함수를 실행하고

붙이고자 하는 위치에 appendChild한다.

그리고 위에 받아두었던 값을 append된 clone에 값을 넣는다.

 

여기까지가 유저에게 responsive하게 하기 위한 작업.

 

이때 id는 index라는 전역 변수로 컨트롤하고 있으므로 index++해주고

Object를 생성해서 받아온 옵션 요소를 넣고 Array에 넣어준다.

 

이렇게 하면 List<Object>가 완성된다 

                addIdToClone(optionCard);
                document.getElementById('cardBody').appendChild(optionCard);

                document.getElementById('optionValueOption1' + index).textContent = option1;
                document.getElementById('optionValueOption2' + index).textContent = option2;
                document.getElementById('optionValueSalePrice' + index).textContent = optionSalePrice;
                document.getElementById('optionValueOriginalPrice' + index).textContent = optionOriginalPrice;
                document.getElementById('optionValueStock' + index).textContent = optionStock;
                document.getElementById('optionRemove' + index).onclick = removeOption;

                index++;

                let optionObject = {
                    option1: option1,
                    option2: option2,
                    optionSalePrice: optionSalePrice,
                    optionOriginalPrice: optionOriginalPrice,
                    optionStock: optionStock
                }
                optionArray.push(optionObject);

 

모든 node에 id를 붙이는 함수

function addIdToClone(clone) {
                clone.id = clone.id + index;
                for (let i = 0; i < clone.childElementCount; i++) {
                    addIdToClone(clone.children[i]);
                }
            }

 

그리고 이제 상품의 값을 서버에 보내야 한다.

유저가 입력하였던 옵션 Object는 이미 필드 변수로 만들어져 있으니 나머지 값들을 받아온다.

 

이때 input type="file"의 값을 받아오는 방법은 .files로 받아온다.

 

FormData를 새로 생성해서 받아온 값들을 append한다.

document.getElementById('optionItemSubmit').addEventListener('click', (e) => {
                e.preventDefault();


                let itemName = document.getElementById('itemName').value;
                let description = document.getElementById('description').value;
                let mainImage = document.getElementById('mainImage').files[0];
                let descriptionImage = document.getElementById('descriptionImage').files;

                let formData = new FormData();

                formData.append('mainImage', mainImage);
                formData.append('descriptionImage', descriptionImage);
                formData.append('apiWriteItemRequest', new Blob([JSON.stringify({
                    itemName,
                    description,
                    optionArray
                })], {
                    type: "application/json"
                }));

                let data = {
                    method: 'POST',
                    body: formData
                };

                const onSuccess = res => {

                }
                const onFailure = res => {
                    return res.json().then(json => alert(json.message));
                }

                fetch(`/api/v1/seller/write`, data)
                    .then(res => {
                        if (!res.ok) {
                            throw res;
                        }
                        return res
                    })
                    .then(onSuccess, onFailure)
                    .catch(err => {
                        alert(err.message);
                    });
            })

 

 

서버

서버에서는 다음과 같이 값을 받는다.

multipart와 json을 받을 수 있게 명시하고

@RequestPart로 파일을 제외한 모든 값을 받아오는데 이때 DTO 내에는 List<Object>를 받는 필드가 있어야 한다.

public class SellerApiController {

    private final SellerService sellerService;
    private final AuthenticationManager authenticationManager;
    private final ObjectMapper objectMapper;

    @PostMapping(value = "/write", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
    public ResponseEntity<?> sellerItemWrite(@Valid @RequestPart ApiWriteItemRequest apiWriteItemRequest,
                                             @RequestParam("mainImage") MultipartFile file,
                                             @RequestParam(value = "descriptionImage", required = false) List<MultipartFile> files,
                                             @CurrentUser User user) {
        final List<OptionValue> optionValues = objectMapper.convertValue(apiWriteItemRequest.getOptionArray(), new TypeReference<List<OptionValue>>() {
        });

 

마지막으로 List<Object>를 실제로 사용할 수 있게 ObjectMapper를 통해 자바의 Object에 값을 매핑한다.

 

 

여기까지 하면 공통으로 들어가는 값, 유저가 입력하는 값에 의해 변동적으로 들어오는 List<Object>, 꼭 필요한 메인 이미지, 필요하지 않은 부가 이미지까지 한 번에 받을 수 있게 된다.

 

 

 

생각보다 많이 복잡해서 구상하는 시간이 오래 걸렸지만 막상 방법을 떠올리고 나니 이미 그 방법을 많이들 쓰고 있어 자료가 많아 구현 자체는 쉽게 했다.