SpringBoot + MariaDB 3 회원정보 수정
회원 정보를 수정하기 전에 프로필에 지금 로그인된 유저의 정보가 없고 더미데이터만 있는 상황이다
이유저에 맞게 프로필에 데이터를 채우고 싶다면 어떻게 해야 할까
우선 모든 페이지에 사용하기 위해 header.jsp에 코드를 추가해 주자.
header.jsp
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<!-- 세션에 데이터를 가져오는 코드 -->
<sec:authorize access="isAuthenticated()">
<!-- property는 고정으로 principal을 사용한다 제작자가 그렇게 정의해놨음 -->
<sec:authentication property="principal" var="principal"/>
</sec:authorize>
그럼 EL태그를 이용하여 ${principal} 사용이 가능한데 하지만 principal은 PrincipalDetails이고 거기에 user의 속성을 불러와야 한다 밑의 불러오는 코드를 보고 이해하자
update.jsp
<div class="content-item__08">
<div class="item__title">이메일</div>
<div class="item__input">
<input type="text" name="email" placeholder="이메일"
value="${principal.user.email }" readonly="readonly" />
</div>
</div>
<div class="content-item__09">
<div class="item__title">전회번호</div>
<div class="item__input">
<input type="text" name="tel" placeholder="전화번호"
value="${principal.user.phone }" />
</div>
</div>
<div class="content-item__10">
<div class="item__title">성별</div>
<div class="item__input">
<input type="text" name="gender" value="${principal.user.gender }" />
</div>
</div>
위처럼 principal의 user의 성별을 달라 이런 식으로 작성을 해야 값이 들어옵니다.
이제 값은 잘 들어오는 것을 확인했으니 수정을 만들어보려고 한다
update.jsp
<%@ include file="../layout/header.jsp"%>
<form id="profileUpdate">
............
<div class="content-item__11">
<div class="item__title"></div>
<div class="item__input">
<button type="button" onclick="update(${principal.user.id})">제출</button>
</div>
</div>
</form>
<script src="/js/update.js"></script>
button 클릭 시 update라는 함수가 발동되게 하고 세션에 저장된 유저의 id만 보내게 설정하였다
밑의 스크립트에 js파일로 가보자
js파일은 정적 파일이라 resources안에 static에 저장된 모습이다.
update.js
// (1) 회원정보 수정
function update(userId) {
// 폼태그의 id=profileUpdate의 serialize를 하게되면 name=value&age=value 식으로 값이 넘어온다
let data = $("#profileUpdate").serialize();
console.log(data)
$.ajax({
type:"put",
url:`/api/user/${userid}`,
data : data,
// 콘솔에서 확인했듯이 key=value형태는 x-www-form-urlencoded타입이다.
contentType:"application/x-www-form-urlencoded; charset=utf-8",
dataType:"json"
}).done(res=>{
console.log("update성공");
}).fail(error=>{
console.log("실패");
})
}
form으론 put요청을 하지 못하니 ajax를 이용하여 put요청
jsp파일에서는 ``(빽틱)을 사용 못하지만 js파일에서는 사용가능합니다.
header에 jquery를 사용하게 설정해 주어야 jquery사용이 가능합니다.
header.jsp에 jquery사용을 위한 코드
<!-- 제이쿼리 -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
이제 백엔드에서 데이터를 받는 것을 작성해 보자 우선 두 개의 파일을 만들어주자
받아온 자료를 객체화하여 담을 Dto와 ajax처리를 위한 controller을 만든다.
UserapiController.java
package com.cos.photogramstart.web.api;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RestController;
import com.cos.photogramstart.web.dto.user.UserUpdateDto;
@RestController
public class UserApiController {
@PutMapping("/api/user/{id}")
public String update(UserUpdateDto userUpdateDto) {
System.out.println(userUpdateDto);
return "ok";
}
}
UserUpdateDto.java
package com.cos.photogramstart.web.dto.user;
import com.cos.photogramstart.domain.user.User;
import lombok.Data;
@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();
}
}
이름과 패스워드는 필수로 입력이 돼야 한다 그 말은 Validation체크를 해줘야 하는데 하는 방법은 나중에 알아보자 우선은 컨트롤에서 전달받은 데이터로 서비스에 넘겨 디비에 적용시키는 방법을 알아보자
컨트롤러먼저 수정
@RequiredArgsConstructor
@RestController
public class UserApiController {
private final UserService userService;
@PutMapping("/api/user/{id}")
public CMRespDto<?> update(@PathVariable int id, UserUpdateDto userUpdateDto) {
User userEnntity = userService.회원수정(id, userUpdateDto.toEntity());
return new CMRespDto<>(1,"회원수정완료",userEnntity);
}
}
서비스에게 회원정보수정을 요청하고 받아와 리턴하는 모습이다 그럼 이제 서비스가 필요하니 서비스를 만들어보자.
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
@Transactional
public User 회원수정(int id,User user) {
// 1. 영속화
User usereEntity = userRepository.findById(id).get();
// 1) 무조건 찾았다 걱정마 get()
// 2) 못찾았어 익섹션 발동시킬께 orElseThrow()
// 2. 영속화된 오브젝트를 수정 - 더티체킹(업데이트 완료)
return null;
}
}
지금은 get을사용하여 무조건 찾았다는 가정하에 코드를 실행하지만 나중에는 안전하게 못 찾았을 시 처리해 주는 orElseThrow를 사용할 것이다
그럼 위에 영속화라고 되어있는데 영속화란 무엇일까?
1. 스프링부트 서버에서 db에 id를 이용하여 데이터를 찾았다
2. 그럼 스프링부트 서버와 db사이에 있는 영속성 콘텍스트에 내가 찾은 유저의 정보가 담기게 된다
3. 그래서 영속성 콘텍스트에 담긴 데이터를 수정해 주면 db에도 반영이 된다
위에 자료기반으로 수정된 서비스코드는 이러하다
UserService.java
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Transactional
public User 회원수정(int id,User user) {
// 1. 영속화
User userEntity = userRepository.findById(id).get();
// 1) 무조건 찾았다 걱정마 get()
// 2) 못찾았어 익섹션 발동시킬께 orElseThrow()
userEntity.setName(user.getName());
String rawPasString = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPasString);
userEntity.setPassword(user.getPassword());
userEntity.setBio(user.getBio());
userEntity.setWebsite(user.getWebsite());
userEntity.setPhone(user.getPhone());
userEntity.setGender(user.getGender());
// 2. 영속화된 오브젝트를 수정 - 더티체킹(업데이트 완료)
return userEntity;
// 더티체킹이 일어나서 업에이트가 완료됨.
}
}
1. userEntity에 id로 db에서 데이터를 가져와 저장(영속성 콘텍스트에 저장된다)
2. userEntity의 정보를 수정만 해주면 db에 바로 적용이 된다.
주의
비밀번호 encode 하여 암호화하는 거 잊지 말자.
하지만 세션에 바뀐 데이터의 정보가 입력되지 않았다 세션에 바뀐 데이터도 적용시켜 주려면 아래와 같다
UserApiController 수정
@RequiredArgsConstructor
@RestController
public class UserApiController {
private final UserService userService;
@PutMapping("/api/user/{id}")
public CMRespDto<?> update(@PathVariable int id, UserUpdateDto userUpdateDto,@AuthenticationPrincipal PrincipalDetails principalDetails) {
User userEnntity = userService.회원수정(id, userUpdateDto.toEntity());
principalDetails.setUser(userEnntity);
return new CMRespDto<>(1,"회원수정완료",userEnntity);
}
}
@AuthenticationPrincipal PrincipaDetails로 세션을 받아와서 세션의 유저를 바뀐 유저로 세팅해 주게 된다면 끝이다.
수정이 완료되고 성공했을 때 프로필 페이지로 보내고 싶다면?
아까 만든 js파일에 스크립트를 done가 성공했을 때이니 done안에 location.href를 추가해 주면 된다.
지금까지 만들어진 로직을 보면 많이 불안하다 왜냐하면 name과 password가 공백으로 들어오면 그대로 db에 저장을 하기 때문에 그렇다면 어떻게 처리해야 할까?
회원가입할 때처럼 프런트단에서 1차적으로 막고 유효성검사를 하고 2차로 db에서 검사를 하면 안정적으로 될 거 같다
그림으로 표현하면 이러하다
1번 유저를 수정해야 하는데 1번 유저가 없다면 처리를 어떻게 할지 잘못된 데이터는 어떻게 데이터가 수정되지 않게 할지 알아보도록 하자 먼저 프런트에서의 처리
update.jsp
<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 />
</div>
</div>
<div class="content-item__03">
<div class="item__title">유저네임</div>
<div class="item__input">
<input type="text" name="username" placeholder="유저네임"
value="${principal.user.username }" readonly="readonly"/>
</div>
</div>
<div class="content-item__04">
<div class="item__title">패스워드</div>
<div class="item__input">
<input type="password" name="password" placeholder="패스워드" required />
</div>
</div>
<!--제출버튼-->
<div class="content-item__11">
<div class="item__title"></div>
<div class="item__input">
<button>제출</button>
</div>
</div>
<!--제출버튼end-->
</form>
<!--프로필수정 form end-->
바뀐 사항
button에 type과 onclick이벤트 삭제
버튼이 서브밋 버튼이 아니라 그냥 버튼으로 타입을 잡게 되면 required 같은 게 안 먹힌다
(submit버튼에 의해 required가 판단을 하는데 그냥 버튼으로 타입을 지정해 버리면 작동하지 않음)
form에 onsubmit에 update함수 안에 user의 id와 event를 전달
event를 전달하는 이유는 submit의 기능을 막기 위해 밑의 js를 보며 알아보자
원래 submit에 action을 넣어서 어디로 보낼 거다라고 정의를 하는데 만약 action이 없다면 원래 페이지로 돌아오게 된다
하지만 우리가 원하는 것은 회원정보가 바뀌면 프로필페이지로 보내는 것이다 그렇기 때문에 원래 submit의 기능을 막기 위해 사단에 event.prventDefault()로 기능을 막고 우리가 원하는 ajax기능을 실행하는 모습이다.
위까지 다했다면 일단 프런트단에선 다 막았다 이제 백엔드에서 2차로 막는 걸 알아보자
UserUpdateDto에 필수값에다 @NotBlank 달아주기
UserApiController.java 에서 @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()) {
Map<String, String>errorMap = new HashMap<>();
// 모든 에러가 getFieldErrors에 리스트 형태로 담겨있다.
for(FieldError error : bindingResult.getFieldErrors()) {
errorMap.put(error.getField(), error.getDefaultMessage());
}
// exception을 발동시켜 에러를 던진다
throw new CustomValidationApiException("유효성검사 실패함",errorMap);
}else {
User userEnntity = userService.회원수정(id, userUpdateDto.toEntity());
principalDetails.setUser(userEnntity); // 세션 정보 변경
return new CMRespDto<>(1,"회원수정완료",userEnntity);
}
}
}
AuthController에서 사용했던 13~22줄을 가져와 유효성 검사를 적용시킨다 (if문만 가져오기)
주의
BindingResult는 무조건 @valid뒤에 와야 적용이 됩니다.
그럼 이제 익셉션을 새로 커스텀해줘야 한다 이유는 지금 우리는 수정완료와 유저의 정보를 반환하고 싶다 하지만
우리가 회원가입 때 사용했던 익셉션은 스크립트 즉 스트링을 뱉어내고 있어 우리가 원하는 형태로 다시 익셉션을 만들어보자
CustomValidationException.java를 복사해서 같은 위치에 CustomValidationApiException.java라고 생성
package com.cos.photogramstart.handler.ex;
import java.util.Map;
public class CustomValidationApiException extends RuntimeException{
// 시리얼번호는 객체를 구분할때 사용된다
private static final long serialVersionUID = 1L;
private Map<String, String> errorMap;
public CustomValidationApiException(String massage, Map<String, String> errorMap) {
super(massage); // 메세지는 부모에게 넘김(Exception)
this.errorMap = errorMap;
}
public Map<String, String> getErrorMap(){
return errorMap;
}
}
ControllerExceptionHandler.java에 새로 만든 Exception등록
@ExceptionHandler(CustomValidationApiException.class)
public CMRespDto<?> validationApiException(CustomValidationApiException e) {
return new CMRespDto<>(-1,e.getMessage(),e.getErrorMap());
}
이름 input에 require삭제시키고 빈값을 넣고 수정을 했는데 성공이라는 콘솔과 안의 내용은 실패가 되어있다
이상하지 않은가 실패면 실패지 왜 성공일까
이유는 ajax에서 데이터를 받아오는데 항상 200번대를 받아와서. done이 실행이 되는 것이다
그렇다면 데이터를 받아올 때 실패일 때는 200번대가 아니게 바꿔주면 될 거 같다 바로 해보자
ControllerExceptionHandler.java의 validationApiException을 밑과 같이 수정해 주자
@ExceptionHandler(CustomValidationApiException.class)
public ResponseEntity<?> validationApiException(CustomValidationApiException e) {
return new ResponseEntity<>(new CMRespDto<>(-1,e.getMessage(),e.getErrorMap()),HttpStatus.BAD_REQUEST);
}
상태코드도 같이 전달하고 싶다면 ResponseEntity를 사용하여야 한다 왼쪽에 바디가 오고 오른쪽에 상태가 있는 모습이다.
httpStatus.BAD_REQUEST는 400번을 뱉어내며 400번은 요청을 잘못했다는 뜻이 있다.
수정하고 난 뒤 다시 이름을 비우고 수정 요청을 하니 위와 같은 콘솔이 보인다면 성공이다
사용자에게 메시지를 alert으로 전달하고 싶다면
}).fail(error=>{ // httpStatus 상태코드 200번대가 아닐 때
alert(error.responseJSON.data.name)
// alert(JSON.stringify(error.responseJSON.data))
})
name같이 하나의 string을 가져올 경우 잘 보이지만
data 같은 경우 객체이기 때문에 object:object 이런 식으로 원치 않는 값이 나올 것이다 이땐
JSON.stringify를 사용하여 객채를 스트링화 시켜 유저에게 보여준다
JSON.stringify를 사용하면 좋아 보이는 점이 어디에서 잘못됐는지 키에 name이 있고 value에 빈값은 허용 안 함 이런 식으로 담겨있으니 어디에 무엇이 잘못되었는지 더 명확하게 알려줄 수 있다.
프런트 앞단에서 막기 끝
이제 백엔드에서 유효성 검사를 해보려 한다
UserService.java의 회원수정 함수를 수정해 주자 전에 말했던 orElseThrow()를 사용할 것이다.
@Transactional
public User 회원수정(int id,User user) {
// 1. 영속화
User userEntity = userRepository.findById(id).orElseThrow(()->{
return new CustomValidationApiException("찾을 수 없는 id입니다.");
});
id로 조회한 게 존재한다면 넘어가고 만약 데이터가 없다면 예외를 발생시킬 것이다
테스트할 때는 findById() 괄호 안에 없는 id를 넣어주고 테스트하자
CustomValidationApiException은 String과 Map을 매개변수로 받게 생성자가 되어있는데 수정해 주자.
public class CustomValidationApiException extends RuntimeException{
// 시리얼번호는 객체를 구분할때 사용된다
private static final long serialVersionUID = 1L;
private Map<String, String> errorMap;
public CustomValidationApiException(String massage, Map<String, String> errorMap) {
super(massage); // 메세지는 부모에게 넘김(Exception)
this.errorMap = errorMap;
}
public CustomValidationApiException(String massage) {
super(massage); // 메세지는 부모에게 넘김(Exception)
}
public Map<String, String> getErrorMap(){
return errorMap;
}
}
오버라이드하여 매개변수 한 개로도 생성이 되게 만들어주자.
그럼 에러처리는 완료하였고 그 에러에 대한 클라이언트에게 응답을 js문을 수정하여 보여주자
update.js
}).fail(error=>{ // httpStatus 상태코드 200번대가 아닐 때
if(error.data == null){
alert(error.responseJSON.message)
}else{
alert(JSON.stringfy(error.responseJSON.data));
}
})
없는 id가 들어올 경우 message 방금 우리가 설정한 찾을 수 없는 id입니다가 나올 거고
아까 우리가 설정한 update이름이 없을 때의 경우는 else에서 발동될 것이다
이러면 백엔드에서도 처리가 완료된 것이다 회원수정 끝!