-
스프링부트+JPA - 인스타그램 클론 코딩 05 - 회원정보수정스프링/스프링부트+JPA - 인스타 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)); } });
- 사실 뒷단에 막는것은 크게 의미가없다. 프론트에서 이미 강력하게 막아놨기때문에.. 하지만 이상한 방법으로 시도하는 것에 대해 이렇게까지 세팅을 해놓으면 제일 안전한 것!
'스프링 > 스프링부트+JPA - 인스타' 카테고리의 다른 글
스프링부트+JPA - 인스타그램 클론 코딩 07 - 프로필(이미지업로드/렌더링) (0) 2021.09.24 스프링부트+JPA - 인스타그램 클론 코딩 06 - 구독API(연관관계 개념) (0) 2021.09.24 스프링부트+JPA - 인스타그램 클론 코딩 04 - 로그인 (0) 2021.09.23 스프링부트+JPA - 인스타그램 클론 코딩 03 - 2 AOP/회원가입 마무리 (0) 2021.09.23 스프링부트+JPA - 인스타그램 클론 코딩 03 - 1 시큐리티 및 회원가입 (0) 2021.09.17