본문 바로가기

Dot Programming/Spring Clone

[스프링 웹앱 프로젝트 #4]회원 가입 뷰

회원가입  뷰
> 부트스트랩
 >> 네비게이션 바 만들기
 >> 폼 만들기

> 타임리프
 >> SignUpForm 타입 객체를 폼 객체로 설정하기

> 웹(HTML, CSS, JavaScript)
 >>제약 검증 기능 사용하기
   >>> 닉네임 (3~20자, 필수 입력)
   >>> 이메일 (이메일 형식, 필수 입력)
   >>> 패스워드 (8~50자, 필수 입력) 

 

 

스프링 환경 설정

서버 재시작할 필요 없이 빌드를 수정하고 바로 html로 변경된 부분을 확인할 수 있다.

developmentOnly 'org.springframework.boot:spring-boot-devtools' 

 

html에 타임리프(thymeleaf) 설정

<html lang="en" xmlns:th="http://www.thymeleaf.org">

 

href의 값을 Thymeleaf 렌더링 할 때 @{/}으로 바꿔줌 / Thymeleaf렌더링 하지 않을 때는 그냥 href 값 사용

th:href="@{/}"   (@는 서블릿 컨텍스트 루트에 따라 알아서 값을 치환해줌 ; 루트를 앱으로주면 알아서 앱으로 치환됨)

 

 

뷰 작성

sign-up.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Dot Study</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
    <style>
        .container{
            max-width: 100%;
        }
    </style>
</head>
<body class="bg-light">
    <nav th:fragment="main-nav" class="navbar navbar-expand-sm navbar-dark bg-dark">
        <a class="navbar-brand" href="/" th:href="@{/}">
            <img src="/images/logo.png" width="30" height="30">
        </a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <form th:action="@{/search/study}" class="form-inline" method="get">
                        <input class="form-control mr-sm-2" name="keyword" type="search" placeholder="스터디 찾기" aria-label="Search" />
                    </form>
                </li>
            </ul>

            <ul class="navbar-nav justify-content-end">
                <li class="nav-item" >
                    <a class="nav-link" href="#" th:href="@{/login}">로그인</a>
                </li>
                <li class="nav-item" >
                    <a class="nav-link" th:href="@{/sign-up}">가입</a>
                </li>
            </ul>
        </div>
    </nav>

<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.min.js" integrity="sha384-w1Q4orYjBQndcko6MimVbzY0tgp4pWB4lZ7lr30WKz0vr/aWKhXdBNmNb5D92v7s" crossorigin="anonymous"></script>
</body>
</html>

 

thymeleaf으로 렌더링안할때에도 그냥 html로 렌더링이 가능하니깐 href="#"를 설정해준다.

<a class="nav-link" href="#" th:href="@{/login}">로그인</a>

 

 

sign-up.html

<!--        계정만들기   -->
        <div class="container">
            <div class="py-5 text-center">
                <h2>계정 만들기</h2>
            </div>
            <div class="row justify-content-center">
                <form class="needs-validation col-sm-6" action="#" 
                      th:action="@{/sign-up}" th:object="${signUpForm}" method="post" novalidate>
                    <div class="form-group">
                        <label for="nickname">닉네임</label>
                        <input id="nickname" type="text" th:field="*{nickname}" class="form-control"
                               placeholder="whiteship" aria-describedby="nicknameHelp" required minlength="3" maxlength="20">
                        <small id="nicknameHelp" class="form-text text-muted">
                            공백없이 문자와 숫자로만 3자 이상 20자 이내로 입력하세요. 가입후에 변경할 수 있습니다.
                        </small>
                        <small class="invalid-feedback">닉네임을 입력하세요.</small>
                        <small class="form-text text-danger" th:if="${#fields.hasErrors('nickname')}" th:errors="*{nickname}">Nickname Error</small>
                    </div>

                    <div class="form-group">
                        <label for="email">이메일</label>
                        <input id="email" type="email" th:field="*{email}" class="form-control"
                               placeholder="your@email.com" aria-describedby="emailHelp" required>
                        <small id="emailHelp" class="form-text text-muted">
                            스터디올래는 사용자의 이메일을 공개하지 않습니다.
                        </small>
                        <small class="invalid-feedback">이메일을 입력하세요.</small>
                        <small class="form-text text-danger" th:if="${#fields.hasErrors('email')}" th:errors="*{email}">Email Error</small>
                    </div>

                    <div class="form-group">
                        <label for="password">패스워드</label>
                        <input id="password" type="password" th:field="*{password}" class="form-control"
                               aria-describedby="passwordHelp" required minlength="8" maxlength="50">
                        <small id="passwordHelp" class="form-text text-muted">
                            8자 이상 50자 이내로 입력하세요. 영문자, 숫자, 특수기호를 사용할 수 있으며 공백은 사용할 수 없습니다.
                        </small>
                        <small class="invalid-feedback">패스워드를 입력하세요.</small>
                        <small class="form-text text-danger" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Password Error</small>
                    </div>

                    <div class="form-group">
                        <button class="btn btn-primary btn-block" type="submit"
                                aria-describedby="submitHelp">가입하기</button>
                        <small id="submitHelp" class="form-text text-muted">
                            <a href="#">약관</a>에 동의하시면 가입하기 버튼을 클릭하세요.
                        </small>
                    </div>
                </form>
            </div>


    <footer th:fragment="footer">
        <div class="row justify-content-center">
            <img class="mb-2" src="/images/test_logo.jpg" alt="" width="100">
            <small class="d-block mb-3 text-muted">&copy; 2020</small>
        </div>
    </footer>

</div>




<script type="application/javascript" th:fragment="form-validation">
        (function () {
            'use strict';

            window.addEventListener('load', function () {
                // Fetch all the forms we want to apply custom Bootstrap validation styles to
                var forms = document.getElementsByClassName('needs-validation');

                // Loop over them and prevent submission
                Array.prototype.filter.call(forms, function (form) {
                    form.addEventListener('submit', function (event) {
                        if (form.checkValidity() === false) {
                            event.preventDefault();
                            event.stopPropagation();
                        }
                        form.classList.add('was-validated')
                    }, false)
                })
            }, false)
        }())
    </script>

 

signUpForm을 Form object으로 설정 -> form태그를 채우는 객체로 사용

th:object="${signUpForm}"th:action="@{/sign-up}" th:object="${signUpForm}" method="post" novalidate>

 

nickname을 input의 필드(input 파라미터 네임, 밸류)로 사용

<input id="nickname" type="text" th:field="*{nickname}" class="form-control"

 

 

document에서 needs-validation을 class로 가지고 있는 form을 가져온 다음 적용

<form class="needs-validation col-sm-6" action="#" 
더보기
 <script type="application/javascript" th:fragment="form-validation">
        (function () {
            'use strict';

            window.addEventListener('load', function () {
                // Fetch all the forms we want to apply custom Bootstrap validation styles to
                var forms = document.getElementsByClassName('needs-validation');

                // Loop over them and prevent submission
                Array.prototype.filter.call(forms, function (form) {
                    form.addEventListener('submit', function (event) {
                        if (form.checkValidity() === false) {
                            event.preventDefault();
                            event.stopPropagation();
                        }
                        form.classList.add('was-validated')
                    }, false)
                })
            }, false)
        }())
    </script>

needs-validation 폼

폼이 유효하지 않으면 폼 제출이 안되도록 설정

 

 

닉네임 제약 조건 설정 (에러시 invalid-feedback 노출)

<input id="nickname" type="text" th:field="*{nickname}" class="form-control" placeholder="whiteship" aria-describedby="nicknameHelp" required minlength="3" maxlength="20">
<small id="nicknameHelp" class="form-text text-muted"> 공백없이 문자와 숫자로만 3자 이상 20자 이내로 입력하세요. 가입후에 변경할 수 있습니다. </small>

 

이메일 제약조건 설정 (에러시 invalid-feedback 노출)

<input id="email" type="email" th:field="*{email}" class="form-control" placeholder="your@email.com" aria-describedby="emailHelp" required>

 

패스워드 제약조건 설정 (에러시 invalid-feedback 노출)

<input id="password" type="password" th:field="*{password}" class="form-control" aria-describedby="passwordHelp" required minlength="8" maxlength="50">
<small id="passwordHelp" class="form-text text-muted">
8자 이상 50자 이내로 입력하세요. 영문자, 숫자, 특수기호를 사용할 수 있으며 공백은 사용할 수 없습니다.  </small>

 

백엔드 로직 작성

이젠 SignUpForm을 만들어주면 된다.

 

SignUpForm.java

/**
 * 회원가입할 때 받아올 데이터
 */
@Data
public class SignUpForm {

    private String nickname;

    private String email;

    private String password;
}

 

AccontController.java

@Controller
public class AccountController {

    @GetMapping("/sign-up")
    public String signUpForm(Model model){
        model.addAttribute("signUpForm", new SignUpForm());  //add Code
        return "account/sign-up";
    }


}

 

SecurityConfig.java에 아래의 코드를 추가

static 리소스들 security필터 적용해제 (/static/images 파일 접근 허용)

@Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

 

결과화면

 

 

테스트 코드 작성

마지막으로 테스트코드 작성해준다.

@SpringBootTest
@AutoConfigureMockMvc
class AccountControllerTest {

    @Autowired
    private MockMvc mockMvc;


    @DisplayName("회원 가입 화면 보이는지 테스트")
    @Test
    public void signUpForm() throws Exception{
        mockMvc.perform(get("/sign-up"))
                .andExpect(status().isOk())
                .andExpect(view().name("account/sign-up"))
                .andExpect(model().attributeExists("signUpForm"));

     }

}

 

 

TIP) model.addAttribute() 

"abc" 을 생락해도 된다. new Abc() 클래스 이름과 상응하는 attribute값에 자동으로 대입됨.

model.addAttribute("signUpForm" , new SignUpForm());  -> model.addAttribute(new SignUpForm()); 으로 축소 가능

 


참고

인프런 강의 - 스프링과 JPA 기반 웹 애플리케이션 개발