본문 바로가기
공부 기록

[스프링 부트로 게시판 만들기] 08. 회원 수정

by 매트(Mat) 2021. 7. 21.




회원수정 페이지 만들기

  • 회원가입을 하면 회원수정을 할 수 있어야 겠죠.

UserController 클래스

package com.azurealstn.blogproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserController {
    ...

    /**
     * 회원수정 페이지
     */
    @GetMapping("/user/update")
    public String userUpdate() {
        return "layout/user/user-update";
    }
}



  • 회원수정 페이지를 만듭니다.

user-update.html

<!doctype html>
<html lang="en" class="h-100" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

<head th:replace="layout/header :: head ('회원수정')"></head>

<body class="text-center d-flex flex-column h-100">
<header th:replace="layout/header :: header"></header>

<main class="form-signin">
    <div class="container border rounded flex-md-row mb-4 shadow-sm h-md-250">
        <form id="needs-validation" novalidate>
            <h1 class="h3 m-3 fw-normal">회원가입</h1>
            <input type="hidden" id="id" th:value="${#authentication.principal.id}">
            <div class="form-floating m-3">
                <input type="text" th:value="${#authentication.principal.username}" class="form-control" id="username" placeholder="아이디를 입력하세요." required
                       minlength="4" size="20" readonly>
                <label for="username">아이디</label>
                <div class="valid-feedback">
                    good!
                </div>
                <div class="invalid-feedback">
                    아이디는 4자 이상 입력해야 합니다.
                </div>
            </div>

            <div class="form-floating m-3">
                <input type="password" class="form-control" id="password" placeholder="패스워드를 입력하세요." required
                       minlength="8" size="20">
                <label for="password">패스워드</label>
                <div class="valid-feedback">
                    very good!
                </div>
                <div class="invalid-feedback">
                    패스워드는 8자 이상 입력해야 합니다.
                </div>
            </div>
            <div class="form-floating m-3">
                <input type="email" th:value="${#authentication.principal.email}" class="form-control" id="email" placeholder="이메일을 입력하세요." required readonly>
                <label for="email">이메일</label>
                <div class="valid-feedback">
                    nice!
                </div>
                <div class="invalid-feedback">
                    이메일 형식으로 입력해야 합니다.
                </div>
            </div>
            <div class="form-floating m-3">
                <input type="text" th:value="${#authentication.principal.nickname}" class="form-control" id="nickname" placeholder="닉네임을 입력하세요." required
                       minlength="4" size="20">
                <label for="nickname">닉네임</label>
                <div class="valid-feedback">
                    very nice!
                </div>
                <div class="invalid-feedback">
                    닉네임은 4자 이상 입력해야 합니다.
                </div>
            </div>
        </form>
        <button class="w-100 btn btn-lg btn-success" id="btn-update">회원수정</button>
    </div>
</main>

<footer th:replace="layout/footer :: footer"></footer>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
        crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script th:src="@{/js/validation-update.js}"></script>
<script th:src="@{/js/user.js}"></script>
</body>

</html>
  • th:value="${#authentication.principal.email}" 이렇게 사용하시면 input의 value값에 유저 정보가 들어 있는 PrincipalDetail의 값을 가져올 수 있습니다.
  • th:value="${#authentication.principal.id}"를 hidden으로 받아서 어떤 유저가 회원수정을 하는지 알기 위해.
  • 사실 제가 Thymeleaf는 처음써봐서 이거 찾는데 꽤 걸렸네요 ㅠ.ㅠ
  • 그리고 PrincipalDetail 클래스에 가서 email과 nickname, id도 받도록 하겠습니다.

PrincipalDetail 클래스

@RequiredArgsConstructor
public class PrincipalDetail implements UserDetails {

    private final User user;

    ...

    //사용자 이메일
    public String getEmail() {
        return user.getEmail();
    }

    //사용자 닉네임
    public String getNickname() {
        return user.getNickname();
    }

    //사용자 pk
    public Long getId() {
        return user.getId();
    }
    ...



  • user.js 파일에 update 함수를 추가합니다.

user.js

'use strict';

let index = {
    init: function () {
        ...
        $("#btn-update").on("click", () => {
            let form = document.querySelector("#needs-validation");
            if (form.checkValidity() == false) {
                console.log("회원수정 안됨")
            } else {
                this.update();
            }
        });
    },

    save: function () {...},

    update: function () {
        let data = {
            id: $("#id").val(),
            password: $("#password").val(),
            nickname: $("#nickname").val()
        }

        $.ajax({
            type: "PUT",
            url: "/api/v1/user",
            data: JSON.stringify(data),
            contentType: "application/json; charset=utf-8",
            dataType: "json"
        }).done(function (res) {
            alert("회원수정이 완료되었습니다.");
            location.href = "/";
        }).fail(function (err) {
            alert(JSON.stringify(err));
        });
    }
}
index.init();
  • hidden 값으로 받은 id 데이터를 추가합니다. id: $("#id").val()



UserApiController

@RequiredArgsConstructor
@RestController
public class UserApiController {

    private final UserService userService;
    ...

    /**
     * 회원수정 API
     */
    @PutMapping("/api/v1/user")
    public Long update(@RequestBody User user) {
        return userService.update(user);
    }

}
  • ajax에서 JSON 데이터 Http Body에 담아서 요청하기 때문에 @RequestBody를 써야합니다.



UserService 클래스

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    ...

    /**
     * 회원수정 로직
     */
    @Transactional
    public Long update(User user) {
        User userEntity = userRepository.findById(user.getId()).orElseThrow(() -> new IllegalArgumentException("해당 회원이 없습니다. id=" + user.getId()));
        userEntity.update(bCryptPasswordEncoder.encode(user.getPassword()), user.getNickname());
        return userEntity.getId();
    }


}
  • Service에서는 먼저 User 객체를 영속화 시켜야 합니다. 그 이유는 더티체킹 때문인데요. JPA를 사용하면 영속성 컨텍스트가 메모리에 뜨는데 따로 update 쿼리문을 날리지 않아도 1차 캐시에 있는 데이터에서 변경이 감지되면 알아서 update를 시켜줍니다. (JPA의 장점이죠.) 이렇게 변경 감지되는 것을 더티체킹이라고 합니다.
  • 그래서 영속화된 userEntity에 변경된 데이터를 넣어주면 됩니다.
  • User 클래스에 update 메소드를 하나 추가하겠습니다



User 클래스

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class User {
    ...

    /**
     * 비밀번호 암호화 메소드
     */
    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * 권한 메소드
     */
    public String getRoleKey() {
        return this.role.getKey();
    }

    /**
     * 회원수정 메소드
     */
    public void update(String password, String nickname) {
        this.password = password;
        this.nickname = nickname;
    }
}
  • 회원수정에 대한 테스트 코드는 잠시 넘어가겠습니다.. (자꾸 에러가 나서 해결되지 않아요 ㅠ.ㅠ)



  • 회원수정 후에 세션을 유지하기 위해 코드를 수정합니다.

SecurityConfig 클래스

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
  • AuthenticationManager 클래스를 오버라이딩해서 Bean으로 등록합니다.



UserApiController

@RequiredArgsConstructor
@RestController
public class UserApiController {

    ...

    /**
     * 회원수정 API
     */
    @PutMapping("/api/v1/user")
    public Long update(@RequestBody User user, @AuthenticationPrincipal PrincipalDetail principalDetail) {
        userService.update(user, principalDetail);
        return user.getId();
    }

}
  • @AuthenticationPrincipal에 PrincipalDetail타입으로 파라미터를 받으면 유저 정보를 얻을 수 있습니다.



UserService

@RequiredArgsConstructor
@Service
public class UserService {
    ...

    /**
     * 회원수정 로직
     */
    @Transactional
    public Long update(User user, @AuthenticationPrincipal PrincipalDetail principalDetail) {
        User userEntity = userRepository.findById(user.getId()).orElseThrow(() -> new IllegalArgumentException("해당 회원이 없습니다. id=" + user.getId()));
        userEntity.update(bCryptPasswordEncoder.encode(user.getPassword()), user.getNickname());
        principalDetail.setUser(userEntity); //추가
        return userEntity.getId();
    }
}
  • UserService 클래스에서도 @AuthenticationPrincipal PrincipalDetail principalDetail를 파라미터로 받아서 update된 유저 정보를 principalDetail에 집어넣습니다.
  • PrincipalDetail 클래스에서 setUser 메소드를 만들어주세요.
  • 그러면 회원정보가 변경됨과 동시에 다시 회원정보를 들어가보면 정상적으로 세션 반영이 되었을 겁니다.






다음으로

  • 회원수정은 여기까지 하겠습니다.
  • 다음 이제 게시글 만들도록 하겠습니다.
  • 제가 Thymeleaf도 처음써보고 해서 원래 회원수정 API를 PathVariable로 id 받아서 처리할려고 하는데, Thymeleaf에서 id값이 자꾸 알 수가 없다해서... 그래서... Test 코드도 꼬이고 해서 좀 더 공부하겠습니다 ㅜ.ㅜ






References

댓글