[Spring Boot Tutorial] 10. 회원가입 화면 만들기

2020. 3. 11. 14:08Spring/Spring Boot Tutorial

300x250
반응형

이전 포스팅에서 JDBC기반의 Spring Security 인증&인가를 이용하여 웹 로그인을 했습니다.
실질적인 테스트를 위해서는 ROLE_VIEW 권한을 가지고 있는 사용자를 만들어야할 텐데, 사용자의 비밀번호를 BCryptPasswordEncoder를 이용해 변환해야하는 번거로움이 있습니다.

물론 tip에서 만들었던 문자열 인코더 api를 이용하여 설정할 비밀번호의 변환값을 직접 sql로 insert 해도 되지만, 회원을 추가할 때마다 db를 통해 직접 추가해야하기 때문에 매우 성가십니다.

이번 시간에는 간단한 회원가입 화면을 만들어보도록 하겠습니다.

  1. 만들고자 하는 화면
  2. 회원가입 화면 이동 버튼 추가하기
  3. 회원가입 페이지로 이동 처리(controller - get /join)
  4. 회원가입 화면 만들기
    1. 기본 뼈대 구성
    2. 회원가입 템플릿 만들기
    3. 비동기로 중복체크 및 회원가입
  5. 중복체크 및 회원가입 처리
    1. controller - post /join
    2. service
    3. repository

1. 만들고자하는 화면

68
기존의 로그인 화면에서 Login버튼 아래에 Join버튼을 추가합니다.

69
필수 정보인 이메일, 비밀번호, 이름, 성별과 옵션 정보인 생년월일, 전화번호를 입력받습니다.

Back버튼을 누를시 이전화면(로그인 화면)으로 이동됩니다.

71
필수 정보인 이메일, 비밀번호, 이름을 입력하지 않을 시, submit되지 않습니다.

70
회원가입 중, 중복된 이메일일 경우에는 중복알림을 띄운 후 이메일 input을 지우고 focus 합니다.

72
회원가입 성공할 경우 회원가입 완료 알림 후 로그인 화면으로 이동됩니다.
회원가입 로직에는 회원 정보 추가와 view 페이지 접속 권한을 위한 ROLE_VIEW 권한 추가가 들어있습니다.


2. 회원가입 화면 이동 버튼 추가하기

<section layout:fragment="f-content">
    ...
    <div>
        <button type="button" class="btn btn-info btn-large form-control"
            id="btn_login">Login</button>
	</div>
    <div>
        <button type="button" class="btn btn-secondary btn-large form-control"
            id="btn_joinForm">Join</button>
    </div>
    ...
</section>

로그인 버튼 아래에 회원가입 화면 이동 버튼을 추가합니다.

<th:block layout:fragment="f-script">
<script>
$(function() {
    ...
    $("#btn_joinForm").on("click", function() {
        location.href = "[[@{/join}]]";
    });
});
</script>
</th:block>

/join페이지로 이동합니다.
타임리프 템플릿상에서 context-relative url을 가져오기 위해 @{} 를 씁니다.
javascript태그 내에서 context-relative url을 가져오기 위해서는 [[]] 괄호를 붙여야 합니다.


3. 회원가입 페이지로 이동 처리

컨트롤러에 /join url에 관한 Get 메서드를 생성합니다.

@RequiredArgsConstructor
@Controller
public class LoginController {

	private final UserService userService;

	...

	@GetMapping(value = "/join")
	public String joinForm(@AuthenticationPrincipal SecurityUser securityUser){
		if(securityUser != null && securityUser.getRoleTypes().contains(RoleType.ROLE_VIEW)) {
			return "redirect:/v";
		}
		return "login/join";
	}

}

/login url로 접속했을 시, ROLE_VIEW 권한을 가진 사용자가 로그인 중일 경우 view 페이지의 메인화면으로 이동되게 했던 것과 같이 joinForm 메서드에도 사전 체크를 추가합니다.

로그인되지 않거나 ROLE_VIEW 권한을 보유하고 있지 않을 경우, join페이지를 엽니다.


4. 회원가입 화면 만들기

4-1. 기본 뼈대 구성

<!DOCTYPE html>
<html lang="ko"
	xmlns:th="http://www.thymeleaf.org"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
	layout:decorator="cmm/layout_login">

<th:block layout:fragment="f-title">DEMO 회원가입</th:block>

<section layout:fragment="f-content">

</section>

<th:block layout:fragment="f-script">

</th:block>

login 화면에서 이용했었던 cmm/layout_login 레이아웃을 이용합니다.
회원가입에 필요한 정보는 f-content 영역에 넣고,
회원가입에 필요한 기타 javascript 코드는 f-script 내부에 작성합니다.

4-2. 회원가입 템플릿 만들기

위의 기본 뼈대 중 section영역을 채웁니다.

<section layout:fragment="f-content">
    <div class="login_wrapper" style="margin: calc(50vh - 320px) auto 0px;">
        <h1><img th:src="@{/static/img/like.png}" width="50" height="auto" alt="demo" id="btn_loginHome"></h1>
        <form method="post" th:action="@{/join}">
            <div>
                <input type="text" name="email" class="form-control" placeholder="이메일" autocomplete="off" required/>
            </div>
            <div>
                <input type="password" name="password" class="form-control" placeholder="비밀번호" autocomplete="off" required/>
            </div>
            <div>
                <input type="text" name="name" class="form-control" placeholder="이름" autocomplete="off" required/>
            </div>
            <div style="margin: 0;">
                <div class="custom-control custom-radio custom-control-inline">
                    <input type="radio" class="custom-control-input" id="sex-1" name="sex" value="1" checked>
                    <label class="custom-control-label" for="sex-1"></label>
                </div>
                <div class="custom-control custom-radio custom-control-inline">
                    <input type="radio" class="custom-control-input" id="sex-2" name="sex" value="2">
                    <label class="custom-control-label" for="sex-2"></label>
                </div>
            </div>
            <div>
                <input type="text" name="birthDate" class="form-control" placeholder="생년월일(yyMMdd)" autocomplete="off"/>
            </div>
            <div style="margin-bottom: 50px;">
                <input type="text" name="phoneNumber" class="form-control" placeholder="전화번호(-생략)" autocomplete="off"/>
            </div>
            <div>
                <button type="submit" class="btn btn-dark btn-large form-control">Join</button>
            </div>
            <div>
                <button type="button" class="btn btn-secondary btn-large form-control" id="btn_loginForm">Back</button>
            </div>
        </form>
        <hr class="separator"/>
        <div>
            <h1>DEMO</h1>
            <p>©2019 All Rights Reserved.</p>
        </div>
    </div>
</section>

필수 입력요소인 email, password, name input 태그에 required 속성을 추가합니다.(ln 6,9,12)

form 태그 안의 required 속성을 이용한 html validation 체크는 form이 submit될 때 이뤄집니다.
validation 체크를 해야할 Join버튼의 type을 submit으로 설정합니다.(ln 31)


4-3. 비동기로 중복체크 및 회원가입

<th:block layout:fragment="f-script">
<script>
$(function() {

    $("form").on("submit", function(e) {
        e.preventDefault();
        var $form = $(this).closest("form");
        var formData = $form.serializeObject();

        $("section div:eq(0)").append('<div id="d-spin" class="spinner-border">');
        $.ajax({
            type : $form.attr("method"),
            dataType : 'json',
            contentType : "application/json",
            data : JSON.stringify(formData),
            url : $form.attr("action"),
        beforeSend : function(xhr){
            xhr.setRequestHeader($("meta[name='_csrf_header']").attr("content"), $("meta[name='_csrf']").attr("content"));
        },
        success : function(res) {
            $("#d-spin").remove();
            if (res.duplicate) {
                $.notify("중복된 이메일 입니다.");
                $("input[name='email']").val("");
                $("input[name='email']").focus();
            } else if (res.success) {
                $.notify("회원 가입 완료되었습니다.");
                setTimeout(function() {window.location = document.referrer}, 800);						
            } else {
                $("#d-spin").hide();
                $.notify("crud fail");
            }
        },
        error : function(error) {
            alert(error.errorMsg);
        }
        });
    });

    $("#btn_loginForm").on("click", function() {
        location.href = document.referrer;
    });

});
</script>
</th:block>

request를 json 형태로 보내기 위해 contentType을 application/json으로 설정하고,
form 내의 name 설정된 input태그를 serializeObject하여 객체화 하였고
이 값을 JSON.stringify로 문자열형태로 변환하여 보냈습니다.

이 데이터는 @RequestBody 를 이용하여 받을 수 있습니다.

이메일이 중복될 경우 응답값에서 duplicate: true를 반환하며
정상적으로 회원가입을 완료할 경우 success: true를 반환합니다.

73

serializeObject와 JSON.stringify를 거친 데이터 값


참고로 serializeObject 메서드는 jquery3 에서는 포함되어있지 않아, 레이아웃의 jquery3 import문 아래에 추가했습니다.

$.fn.serializeObject = function() {
    var obj = null;
    try {
        if (this[0].tagName && this[0].tagName.toUpperCase() == "FORM") {
            var arr = this.serializeArray();
            if (arr) {
                obj = {};
                jQuery.each(arr, function() {
                    obj[this.name] = this.value;
                });
            }
        }
    } catch (e) {
        alert(e.message);
    } finally {
    }
    return obj;
};

5. 중복체크 및 회원가입 처리

실질적인 회원가입처리를 하는 컨트롤러 메서드를 만듭니다.

5-1. controller - post /join

@RequiredArgsConstructor
@Controller
public class LoginController {

    private final UserService userService;
    ...

    @ResponseBody
    @PostMapping(value = "/join")
    public Map<String, Object> join(@RequestBody UserValue value){
        Map<String, Object> response = new HashMap<>();

        if(userService.findByEmail(value.getEmail()).isPresent()) {
            response.put("duplicate", true);
            return response;
        }

        response.put("success", userService.join(value) != null ? true : false);
        return response;
    }
}

이메일이 이미 등록되어있을 경우 duplicate: true를 반환합니다. (ln 13-16)
회원 가입 성공할 경우 success: true를 반환합니다. (ln 18)

5-2. service

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final UserRoleRepository userRoleRepository;
    private final PasswordEncoder passwordEncoder;
    ...

    @Transactional
    public User save(UserValue value) {
        User user = User.builder()
            .type(value.getType())
            .email(value.getEmail())
            .birthDate(value.getBirthDate())
            .name(value.getName())
            .password(passwordEncoder.encode(value.getPassword()))
            .phoneNumber(value.getPhoneNumber())
            .sex(value.getSex()).build();
        return userRepository.save(user);
    }

    @Transactional
    private UserRole saveUserRole(User user) {
        return userRoleRepository.save(UserRole.builder()
            .user(user).roleName(RoleType.ROLE_VIEW).build());
    }

    public User join(UserValue value) {
        User user = save(value);
        saveUserRole(user);
        return user;
    }

    public Optional<User> findByEmail(String email) {
        return userRepository.findByEmail(email);
    }
}

join 메서드는 user를 저장 한 후, 그 사용자의 ROLE_VIEW user_role을 저장합니다. (ln 30-31)
비밀번호는 passwordEncoder를 이용하여 BCryptPasswordEncoder를 이용하여 인코딩한 값을 넣습니다.(ln 17)

5-3. repository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

	...

	Optional<User> findByEmail(String email);
}

email을 이용하여 user엔티티를 조회하는 메서드를 추가합니다.

email은 unique 컬럼이기 때문에 findTop1 하지 않아도 됩니다.
save 메서드는 JpaRepository에 정의되어 있으므로 메서드를 추가하지 않아도 됩니다.

GitHub에서 demo 프로젝트를 다운받아 볼 수 있습니다.


  • how to use context-relative url in javascript using thymeleaf template
  • thymeleaf 템플릿에서 javascript 내의 context path설정
  • join using thymeleaf template
300x250
반응형
  • 프로필사진
    1332022.06.14 13:23

    안녕하세요 혹시 갑자기 마이바티스가 왜 나오는지 여쭤봐도 될까요ㅜㅜ JPA로 할 수 있는 부분만 JPA가 다루고, 나머지는 MYBATIS가 다루는건가요..?

    • 프로필사진
      Favicon of https://blog.jiniworld.me BlogIcon jiniya222022.06.14 13:26 신고

      JPA와 Mybatis를 함께 이용하고 싶을 경우 사용하면 되는 설정입니다.
      Mybatis를 사용하지 않을거라면 관련 설정은 생략하고 진행하면 돼요

    • 프로필사진
      BlogIcon 1332022.06.14 13:38

      Mybatis 사용하지 않으면 mapper랑 다 생략해도 되는 걸까요? ㅜ 올리신 게시물을 끝까지 다 따라하고 싶은데 어렵네유.. ㅜ mybatis를 설정하지 않을 시 따라가는데에 문제가 있진 않을까요?

    • 프로필사진
      Favicon of https://blog.jiniworld.me BlogIcon jiniya222022.06.14 14:34 신고

      넵 빼고 진행해도 문제 없을거에요

    • 프로필사진
      BlogIcon 1332022.06.14 14:51

      앗 그러면 하나만 더 물어보고 싶은 게 있는데, 작성자님께서 두 개를 다 쓰신 이유는 JPA에서 다루기가 어려운 것들을 Mybatis에서 다루도록 하려고 구성한 걸까요? JPA에서는 INSERT, UPDATE, DELETE만, MyBatis에선 SELECT만!?!

    • 프로필사진
      Favicon of https://blog.jiniworld.me BlogIcon jiniya222022.06.14 16:10 신고

      MyBatis를 이용할 경우, SQL native 문법을 활용할 수 있어, 통계와 같은 복잡한 쿼리를 이용하고자 할때 편리합니다.

      물론 관련 모든 기능은 JPA로도 가능하기 때문에 필요하지 않다면 MyBatis 설정은 하지 않아도 됩니다.