ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링부트+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));
    		}
    	});

     

     

     

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