스프링/스프링부트+JPA - 인스타

스프링부트+JPA - 인스타그램 클론 코딩 05 - 회원정보수정

H-V 2021. 9. 23. 22:09

이지업 최주호 강사님 강의 참조

 

01 세션정보를 받아와 페이징에 뿌리기

  • Controller에서 정보 넘기기
	@GetMapping("/user/{id}/update")
	public String update(@PathVariable int id, 
			@AuthenticationPrincipal PrincipalDetails principalDetails,
			Model model) {
		System.out.println("세션: "+principalDetails.getUser());
		model.addAttribute("principal", principalDetails.getUser());
		return "user/update";
	}
  • Model을 파라미터로 받아서 뷰로 데이터를 넘길 수 있다

 

  • update.jsp 데이터 뿌리기
<input type="text" name="name" placeholder="이름" value="${principal.name}" />
<input type="text" name="username" placeholder="유저네임" value="${principal.username}" disabled="disabled" />
<input type="text" name="website" placeholder="웹 사이트" value="${principal.website}" />
<textarea name="bio" id="" rows="3">${principal.bio}</textarea>
<input type="text" name="email" placeholder="이메일" value="${principal.email}" disabled="disabled" />
<input type="text" name="phone" placeholder="전화번호" value="${principal.phone}" />
<input type="text" name="gender" value="${principal.gender}" />

굿!

 

→ 이렇게 넘겨도 되지만 더 쉽게 시큐리티 태그로 넘겨보자

  • 모든 파일에 Include 되어있는 header.jsp에 세팅
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<sec:authorize access="isAuthenticated()">
	<sec:authentication property="principal" var="principal"/>
</sec:authorize>

※ 다른 변수명을 사용하고 싶으면 var를 바꾸면 된다

 

  • 이제 Model 마라미터는 필요가 없다 / update.jsp 에 .user를 붙여주자
	@GetMapping("/user/{id}/update")
	public String update(@PathVariable int id, 
			@AuthenticationPrincipal PrincipalDetails principalDetails) {
		System.out.println("세션: "+principalDetails.getUser());
		return "user/update";
	}
<input type="text" name="name" placeholder="이름" value="${principal.user.name}" />
<input type="text" name="username" placeholder="유저네임" value="${principal.user.username}" disabled="disabled" />
<input type="text" name="website" placeholder="웹 사이트" value="${principal.user.website}" />
<textarea name="bio" id="" rows="3">${principal.user.bio}</textarea>
<input type="text" name="email" placeholder="이메일" value="${principal.user.email}" disabled="disabled" />
<input type="text" name="phone" placeholder="전화번호" value="${principal.user.phone}" />
<input type="text" name="gender" value="${principal.user.gender}" />

 

 

 

 

02 AJAX를 이용해 회원정보 수정

  • 기본적으로 FORM태그에는 PUT/DELETE 이런 요청이 불가능 하다. 그래서 AJAX가 필요하다
  • update.jsp에서 버튼을 클릭했을때 JS 함수 호출 
<button onclick="update()">제출</button>
<script src="/js/update.js"></script>

 

  • 1차 update.js 세팅
function update(userId) {
	let data = $("#profileUpdate").serialize();
	console.log(data);
	
	$.ajax({
		type: "put",
		url: `/api/user/${userId}`,
		data: data,
		contentType:"application/x-www-form-urlencoded; charset=utf-8",
		dataType: "json"
	}).done(res=>{
		console.log("success");
	}).fail(error=>{
		console.log("fail");
	});
}

 

  • url의 주소를 찾기위한 api 세팅

@RestController
public class UserApiController {
	@PutMapping("/api/user/{id}")
	public String update(@PathVariable int id, UserUpdateDto userUpdateDto) {
		System.out.println(userUpdateDto);
		return "ok";
	}
}

 

  • 제출 버튼을 클릭했을시에 넘어오는 데이터를 받기위한 1차 Dto 세팅
@Data
public class UserUpdateDto {
	//필수
	private String name;
	private String password;
	//안필수
	private String website;
	private String bio;
	private String phone;
	private String gender;
	
	//필수가 아닌 데이터를 받는 엔티티는 위험함
	public User toEntity() {
		return User.builder()
				.name(name)
				.password(password)
				.website(website)
				.bio(bio)
				.phone(phone)
				.gender(gender)
				.build();
	}
}

→ 테스트

  • 값을 잘 받는다 

 

 

  • 실제 DB에 넣기

 

  • UserApiController
@RequiredArgsConstructor
@RestController
public class UserApiController {
	
	private final UserService userService;
	
	@PutMapping("/api/user/{id}")
	public CMRespDto<?> update(@PathVariable int id, UserUpdateDto userUpdateDto) {
		System.out.println(userUpdateDto);
		User userEntity = userService.update(id, userUpdateDto.toEntity());
		return new CMRespDto<>(1, "Success", userEntity);
	}
}
  • UserService
@RequiredArgsConstructor
@Service
public class UserService {
	
	private final UserRepository userRepository;
	private final BCryptPasswordEncoder bCryptPasswordEncoder;
	
	@Transactional
	public User update(int id, User user) {
		
		//1.영속화
		User userEntity = userRepository.findById(id).get();
		//2. 영속화된 객체를 더티체킹
		userEntity.setName(user.getName());
		String rawPassword = user.getPassword();
		String encPassword = bCryptPasswordEncoder.encode(rawPassword);
		userEntity.setPassword(encPassword);
		userEntity.setBio(user.getBio());
		userEntity.setWebsite(user.getWebsite());
		userEntity.setPhone(user.getPhone());
		userEntity.setGender(user.getGender());
		return userEntity;
	}
}

 

  • 여기서 배웠던 문제! 디비는 바껴있지만 다시 들어가보면 null이다. 해결하자 (세션정보를 바꿔야한다!)
@RequiredArgsConstructor
@RestController
public class UserApiController {
	
	private final UserService userService;
	
	@PutMapping("/api/user/{id}")
	public CMRespDto<?> update(@PathVariable int id, UserUpdateDto userUpdateDto,
			@AuthenticationPrincipal PrincipalDetails principalDetails) {
		System.out.println(userUpdateDto);
		User userEntity = userService.update(id, userUpdateDto.toEntity());
		principalDetails.setUser(userEntity);
		return new CMRespDto<>(1, "Success", userEntity);
	}
}

굿!

 

  • 마무리로 회원정보수정이 성공했으면 페이지를 넘겨주자 (update.js)
	}).done(res=>{
		console.log("success");
		location.href=`/user/${userId}`;

 

 

 

 

03 정보 수정시 유효성 검사 (프론트단 및 DB막기)

  • DB에 수정 삽입은 잘 되지만 2가지의 문제가 있다 1) 필수 데이터 NULL값 2) 영속화된 유저 없어짐
    이를 해결해 보자
  • 필수 데이터값 입력 강제 시키키 및 유효성 검사
*update.jsp
<input type="text" name="name" placeholder="이름" value="${principal.user.name}" required="required" />
<input type="password" name="password" placeholder="패스워드" required="required" />


* 버튼을 일반 버튼으로 바꾸고 폼태그가 작동하도록 변경
<button>제출</button>

<form id="profileUpdate" onsubmit="update(${principal.user.id}, event)">

굿!

  • event 파라미터를 JS에서 받아서 수정 후 넘어가도록 변경
function update(userId, event) {
	event.preventDefault();
	....

 

 

 

  • 프론트에서는 다 막혔다. 이제 유효성 검사를 하여 DB도 막자
@Data
public class UserUpdateDto {
	//필수
	@NotBlank
	private String name;
	@NotBlank
	private String password;
	...

→ 필수 데이터 널값을 못하도록 어노테이션을 걸고 유효성 검사를 할 파라미터에 @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,
			@AuthenticationPrincipal PrincipalDetails principalDetails) {
		
		if(bindingResult.hasErrors()) {
			Map<String, String> errorMap = new HashMap<>();
			for (FieldError error : bindingResult.getFieldErrors()) {
				errorMap.put(error.getField(), error.getDefaultMessage());
			}
			throw new CustomValidationApiException("Failed", errorMap);
		} else {
			User userEntity = userService.update(id, userUpdateDto.toEntity());
			principalDetails.setUser(userEntity);
			return new CMRespDto<>(1, "Success", userEntity);
		}
	}
}
  • 똑같이 유효성 검사 핸들러 세팅
public class CustomValidationApiException extends RuntimeException {

	//객체 구분용 - 중요하지 않음
	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;
	}
}
  • Api에 대한 예외처리 함수 추가
@RestController
@ControllerAdvice
public class ControllerExceptionHandler {
	...
    	@ExceptionHandler(CustomValidationApiException.class)
	public CMRespDto<?> validationApiException(CustomValidationApiException e) {
		return new CMRespDto<>(-1, e.getMessage(), e.getErrorMap());
	}
}
  • 회원수정은 AJAX와 통신이기때문에 데이터를 리턴해야한다!

→ 테스트

 

 

 

  • 실패인데 성공이라고 뜬다. AJAX 통신에서 .fail로 넘어가려면 상태코드 200번대가 아니어야 넘어간다. 상태코드를 받기위해 코드 수정을 해보자
@RestController
@ControllerAdvice
public class ControllerExceptionHandler {
	...
    	@ExceptionHandler(CustomValidationApiException.class)
	public ResponseEntity<?> validationApiException(CustomValidationApiException e) {
		return new ResponseEntity<>(new CMRespDto<>(-1, e.getMessage(), e.getErrorMap()), HttpStatus.BAD_REQUEST);
	}
}
*update.js

	}).fail(error=>{
		alert(error.responseJSON.data.name);
		console.log("fail",error.responseJSON.data);

굿!

 

 

 

04 영속성 유저가 사라지는 오류 해결

  • 회원가입시에 UserService에서 .get()으로 userEntity 객체를 선언한것을 바꿔주고 똑같이 AJAX통신이기 때문에 Api 예외 핸들러로 통일해서 처리 
User userEntity = userRepository.findById(id)
				.orElseThrow(()-> {return new CustomValidationApiException("Not found");});
  • 이 오류시에는 메세지만 나오면 되니 CustomValidationApiException에 메세지 전용 생성자 추가
	public CustomValidationApiException(String message) {
		super(message);
	}
  • update.js 에서 errorMap이 현재 null이기때문에 null값이 들어오면 분기를 시키자
	}).fail(error => {
		if (error.data == null) {
			alert(error.responseJSON.message);
		} else {
			alert(JSON.stringify(error.responseJSON.data));
			console.log("fail", JSON.stringify(error.responseJSON.data));
		}
	});

 

 

 

  • 사실 뒷단에 막는것은 크게 의미가없다. 프론트에서 이미 강력하게 막아놨기때문에.. 하지만 이상한 방법으로 시도하는 것에 대해 이렇게까지 세팅을 해놓으면 제일 안전한 것!