메타코딩 SNS프로젝트

29. 프로필 페이지 - Image upload 로직 유효성 검사하기

정현3 2022. 6. 21. 00:45

Image를 서버에 업로드하고, DB에 INSERT하는 로직까지 끝이났다.

당연히 어떠한 서비스를 만들었으면 '유효성 검사(Validation)'를 해주어야 한다.

가장 쉬운 프론트엔드 단에서의 '유효성 검사'를 해주자

첫번째로 Image파일을 설명하는 caption부분을 '비울 수 없게' 설정 해주겠다.

upload.jsp

<!--사진설명 + 업로드버튼-->
<div class="upload-form-detail">
    <input type="text" placeholder="사진설명" name="caption" required="required"/>
    <button class="cta blue">업로드</button>
</div>
<!--사진설명end-->

그리고 두번째로 'Image파일'없이 업로드하는것은 허용하지 않고 싶다

하지만 Image의 타입인 MultipartFile 타입은 @NotBlank같은 Validation을 지원하지 않는다.

따라서, Image upload에 대한 '유효성 검사'는 if문을 사용해서 '수동'으로 만들어 주어야 한다.

ImageController로 가보자

package com.cos.photogramstart.web;

import com.cos.photogramstart.config.PrincipalDetails;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.service.ImageService;
import com.cos.photogramstart.web.dto.Image.ImageUploadDto;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@RequiredArgsConstructor
@Controller
public class ImageController {

    private final ImageService imageService;

    @GetMapping({"/", "/image/story"})
    public String story() { //기본페이지
        return "image/story";
    }

    @GetMapping("/image/popular")
    public String popular() { //인기페이지
        return "image/popular";
    }

    @GetMapping("/image/upload")
    public String upload() { //사진 등록을 위한 페이지
        return "image/upload";
    }

    @PostMapping("/image")
    public String imageUpload(ImageUploadDto imageUploadDto,
                              @AuthenticationPrincipal PrincipalDetails principalDetails) {

        //서비스 호출
        imageService.사진업로드(imageUploadDto, principalDetails);

        if (imageUploadDto.getFile().isEmpty()) { // imageUploadDto에서 File이 없으면
            throw new CustomValidationException("이미지가 첨부되지 않았습니다.", null);
        }
        
        return "redirect:/user/"+principalDetails.getUser().getId();

    }
}

-> 이 상태로 Image없이 업로드를 시도해보자

package com.cos.photogramstart.web.dto;

import com.cos.photogramstart.domain.User;
import com.cos.photogramstart.domain.image.Image;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

@Data
public class ImageUploadDto { //file과 caption을 받을것이다.

    private MultipartFile file;
    private String caption;

    public Image toEntity(String postImageUrl, User user) {
        return Image.builder()
                .caption(caption)
                .postImageUrl(postImageUrl)
                .user(user)
                .build();
    }
}

-> HttpStatus 상태코드 500이 응답되면서, message도 함께 응답되었다.

그런데 CustomValidationException을 걸어주면, 이러한 HttpStatus 코드가 응답되어선 안되는데???

CustomValidationException이 제대로 동작했는지 확인해보자.

package com.cos.photogramstart.handler;

import java.util.Map;

public class CustomValidationException extends RuntimeException {


    //객체를 구분하기 위해 '시리얼넘버'를 넣어주는것
    //JVM을 위해 걸어주는것이다
    private static final long serialVersionUID = 1L;

    private Map<String, String> errorMap;

    public CustomValidationException(String message, Map<String, String> errorMap) {
        super(message);
        this.errorMap = errorMap;
    }

    public Map<String, String> getErrorMap() {
        return errorMap;
    }
}

-> validationException에서 분기하여 errorMap도 같이 응답할것인지, message만 응답할 것인지를 if문으로 만들어주자.

package com.cos.photogramstart.handler;

import com.cos.photogramstart.handler.ex.CustomApiException;
import com.cos.photogramstart.handler.ex.CustomValidationApiException;
import com.cos.photogramstart.handler.ex.CustomValidationException;
import com.cos.photogramstart.util.Script;
import com.cos.photogramstart.web.dto.CMRespDTO;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

@RestController
@ControllerAdvice //모든 Exception을 다 낚아챈다
public class ControllerExceptionHandler {

    //JavaScript로 응답하는 Handler
    @ExceptionHandler(CustomValidationException.class)
    public String validationException(CustomValidationException e) {

        //CMRespDto, Script 비교
        //1. 클라이언트에게 응답할때는 Script 좋음
        //2. Ajax통신을 하거나 Android 통신을 하게되면 CMRespDto가 좋다
        //즉, 개발자를 위한 응답에는 CMRespDto, 클라이언트를 위해서는 Script가 좋다

        if (e.getErrorMap() == null) {
            return Script.back(e.getMessage());
        } else {
            return Script.back(e.getErrorMap().toString());
        }



        //자바스크립트로 짜는 부분까지 2가지 방향으로 갔을때 사용자에게 어떤것이 좋을지 판단해보라고 나눈것
    }

    //CMRespDto 오브젝트를 응답하는 핸들러
    @ExceptionHandler(CustomValidationApiException.class)
    public ResponseEntity<?> validationApiException(CustomValidationApiException e) {

        return new ResponseEntity<>(
                new CMRespDTO<>(-1, e.getMessage(), e.getErrorMap()),
                HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(CustomApiException.class)
    public ResponseEntity<?> apiException(CustomApiException e) {

        return new ResponseEntity<>(
                new CMRespDTO<>(-1, e.getMessage(),null),
                HttpStatus.BAD_REQUEST);
    }
    // <?>를 사용하면 제네릭 타입이 결정이 된다
    // BAD_REQUEST는 400번대 오류이다 -> 너가 요청을 잘못했다
}

-> 정상적으로 ValidationException이 동작했지만, 클라이언트가 알 필요없는 경로까지 응답된다. 따라서 return Script.back(e.toString());이 아닌 getMessage를 응답해주도록 하자.

if (e.getErrorMap() == null) {
    return Script.back(e.getMessage());
} else {
    return Script.back(e.getMessage());
}