프로젝트/SNS 프로젝트

[회원정보 수정] 유효성 검사

영카이브 2024. 3. 29. 13:32

현재 상황 

1. 필수로 받아야 하는 데이터가 공백으로 들어갈 수 있다. >> 서버단(프론트,백엔드)에서 해결

2. 현재 userId를 1번으로 고정시켜놔서 이것도 변수로 처리해줘야 한다. >> DB단에서 해결

 

 

프론트단

 

update.js에서 수정버튼을 누르면 required가 제대로 동작하지않고 데이터간다

따라서 update()함수를 호출하는 것을 button태그가 아닌 form태그에 두었다. 

 

<!--프로필 수정-->
<!--버튼을 누르면 form태그 실행, action이 없으면 자기자신호출-->
<form id="profileUpdate" onsubmit="update(${principal.user.id},event)">
    <div class="content-item__02">
        <div class="item__title">이름</div>
        <div class="item__input">
            <input type="text" name="name" placeholder="이름"
                value="${principal.user.name}" required="required"/>
        </div>
    </div>

 

form태그에서는 action경로를 지정하지않고 update()에 event를 담아

event.preventDefault();

 

를 사용해서 ajax의 .done에 지정한 경로로 이동할 수 있게 했다. 

 

백엔드단

 

프론트단뿐만 아니라 백엔드단에서도 막아야하는 이유: 제공한 페이지가 아닌 Postman을 이용해 요청하면 프론트단에서의 제약조건이 의미가 없기 때문이다. 

 

1. 필수값엔 @NotBlank 걸기 

 

UserUpdateDto

@Data
public class UserUpdateDto {
	@NotBlank
	private String name; //필수값
	@NotBlank
	private String password; //필수값
	private String website; 
	private String bio; 
	private String phone; 
	private String gender;
	
	// 필수값이 아닌값까지 고려해서 Entity를 만드는 것은 위험함 
	// 사용자가 이름,패스워드 기재 안하면 DB에 공백이 들어가므로 validation체크해야함
	public User toEntity() {
		return User.builder()
				.name(name) // Validation 체크
				.password(password) // Validation 체크
				.website(website)
				.bio(bio)
				.phone(phone)
				.gender(gender)
				.build();
	}
}

 

 

2. @NotBlank가 걸린 UserUpdateDto에 @Valid 어노테이션을 걸어 제대로 작동하게 하고 유효성검사가 실패 했을때를 고려해 에러를 담아줄 BindingResult를 사용한다. BindingResult의 위치는 꼭 @Valid 되는 파라미터 다음에 적는다. 

@RequiredArgsConstructor
@RestController
public class UserApiController {
	
	private final UserService userService;
	
	// 데이터 트랜스젝션 오브젝트
	@PutMapping("/api/user/{id}")
	public CMRespDto<?> update(@PathVariable int id,
			@Valid UserUpdateDto userUpdateDto,
			BindingResult bindingResult,//꼭 @Valid가 적혀있는 다음 파라미터에 적어야함
			@AuthenticationPrincipal PrincipalDetails principalDetails){
		
		if(bindingResult.hasErrors()) {
		    // 유효성 검사 에러가 발생한 경우
			// 유효성 검사 에러를 담기 위한 'errorMap'생성
		    Map<String, String> errorMap = new HashMap<>();
		    
		    // 각 필드에 대한 에러를 errorMap에 저장
		    // bindingResult객체가 발생한 모든 에러를 순회하면서 각 필드에 대한 에러 정보 추출
		    for(FieldError error : bindingResult.getFieldErrors()) {
		        errorMap.put(error.getField(), error.getDefaultMessage());
		    }
		    
		    // 유효성 검사 실패 예외를 발생시킴
		    throw new CustomValidationApiException("유효성검사 실패함", errorMap); 
		} else {
		    // 유효성 검사 통과한 경우
		    // 회원 정보 수정 요청 처리
		    User userEntity = userService.회원수정(id, userUpdateDto.toEntity());
		    
		    // 수정된 회원 정보를 세션에 반영
		    principalDetails.setUser(userEntity);
		    
		    // 성공 응답 반환
		    return new CMRespDto<>(1,"회원수정완료", userEntity); 
		}

		
		
	}
}

 

 

3. ApiController에서 작동할 CustomValidationApiException을 만든다. 기존  CustomValidationException이 있음에도 하나 더 만드는 이유는 ajax, android통신에는 데이터를 보내므로 CMRespDto로 응답하는 것이 더 좋다. 따라서 ControllerExceptionHandler에서 CMREspDto로 응답하는 Exception을 만들어준다. 

 

이렇게 하면 ajax가 .done을 타서 성공한듯 보이지만 백엔드에 막혀 DB에 반영되지 않는다. done은 HTTPStatus상태코드가 done이 200번(정상)일 때 실행되고 fail은 HTTPStatus상태코드가 200번대가 아닐 때(에러) 실행된다. 따라서 HTTPStatus상태코드도 함께 응답해주어야 컨트롤이 가능하다. 

	// 데이터 리턴 (Ajax를 통해 응답할때)
	@ExceptionHandler(CustomValidationApiException.class)
	public ResponseEntity<?> validataionApiException(CustomValidationApiException e) {
		return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(),e.getErrorMap()),HttpStatus.BAD_REQUEST);
	}

 

 

3번 적용 

public class CustomValidationApiException extends RuntimeException{

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

	// 유효성 검사 실패 시 발생한 에러메세지를 담은 맵
	private Map<String, String> errorMap;
	
	// 생성자로 전달된 메세지와 에러맵을 사용해 예외객체 생성
	public CustomValidationApiException(String message,Map<String, String> errorMap) {
		super(message);
		this.errorMap = errorMap; 
	}
	
	// 에러메세지를 담은 맵을 반환하는 메서드
	public Map<String, String> getErrorMap(){
		return errorMap;
	}
	
}
@RestController
@ControllerAdvice // 모든 exception을 낚아채는 어노테이션
public class ControllerExceptionHandler {
	// CMRespDto와 Script 비교
	// 1. 클라이언트에게 응답할 때는 Script좋음 
	// 2. Ajax통신 - CMRespDto
	// 3. Android통신 -CMREspDto 
	// 2, 3은 개발자가 응답받을 때는 코드로 받는것이 좋다. 
	
	// 자바스크립트 리턴
	@ExceptionHandler(CustomValidationException.class)
	public String validataionException(CustomValidationException e) {
		return Script.back(e.getErrorMap().toString()); 
	}
	
	// 데이터 리턴 (Ajax를 통해 응답할때)
	@ExceptionHandler(CustomValidationApiException.class)
	public ResponseEntity<?> validataionApiException(CustomValidationApiException e) {
		return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(),e.getErrorMap()),HttpStatus.BAD_REQUEST);
	}
}
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CMRespDto<T> {
	private int code; // 1(성공),-1(실패)
	private String message;
	private T data;
}