스프링/스프링부트+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));
}
});
- 사실 뒷단에 막는것은 크게 의미가없다. 프론트에서 이미 강력하게 막아놨기때문에.. 하지만 이상한 방법으로 시도하는 것에 대해 이렇게까지 세팅을 해놓으면 제일 안전한 것!