본문 바로가기

Dot Programming/Spring Clone

[스프링 웹앱 프로젝트 #9]회원 가입 인증 메일 확인

회원 가입 : 인증 메일 확인

GET "/check-email-token" token = ${token} email = ${email} 요청 처리
 > 이메일이 정확하지 않은 경우에 대한 에러 처리
 > 토큰이 정확하지 않은 경우에 대한 에러 처리
 > 이메일과 토큰이 정확한 경우 가입 완료 처리
   >> 가입 일시 설정
   >> 이메일 인증 여부 true로 설정

인증 확인 뷰
 > 입력값에 오류가 있는 경우 적절한 메세지 출력
 > 인증이 완료된 경우, 환영 문구와 함께 몇 번째 사용자인지 보여줄 것

 

백엔드 로직 작성

AccountController.class

@Slf4j
@Controller
@RequiredArgsConstructor
public class AccountController {

    private final SignUpFormValidator signUpFormValidator;
    private final AccountService accountService;
    private final AccountRepository accountRepository;

repository를 domain계층으로 보느냐 아니면 layer, controller, service, dao 이렇게 볼거냐에 따라서 논의가 있겠지만 지금은 repository를 domain(Account)으로 보겠다. (DDD)

 

→ 그래서 여러 군데에서 참조해도 괜찮다는 가정하에 실습 진행

 

 

AccountController.class

@Slf4j
@Controller
@RequiredArgsConstructor
public class AccountController {
		
    private final SignUpFormValidator signUpFormValidator;
    private final AccountService accountService;
    private final AccountRepository accountRepository;


    ...
    
    @GetMapping("/check-email-token")
    public String checkEmailToken(String token, String email, Model model){
        Account account = accountRepository.findByEmail(email);
        String view = "account/checked-email";
        if(account == null) {
            model.addAttribute("error", "wrong.email");
            return view;
        }

        if(!account.getEmailCheckToken().equals(token)){
            model.addAttribute("error", "wrong.token");
            return view;
        }

        account.setEmailVerified(true);
        account.setJoinedAt(LocalDateTime.now());
        model.addAttribute("numberOfUser", accountRepository.count());
        model.addAttribute("nickname", account.getNickname());
        return view;

    }


}

 

 

뷰 작성

checked-email.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" th:href="@{/login}">로그인</a>
                </li>
                <li class="nav-item" >
                    <a class="nav-link" th:href="@{/sign-up}">가입</a>
                </li>
            </ul>
        </div>
    </nav>

    <div class = "py-5 text-center" th:if="${error}">
        <p class="lead">닷스터디 이메일 확인</p>
        <div class="alert alert-danger" role="alert">
            이메일 확인 링크가 정확하지 않습니다.
        </div>
    </div>


    <div class = "py-5 text-center" th:if="${error == null}">
        <p class="lead">닷스터디 이메일 확인</p>
        <h2>
            이메일을 확인했습니다. <span th:text="${numberOfUser}">10</span>번째 회원,
            <span th:text="${nickname}">이종원</span>님 가입을 축하합니다.
        </h2>

        <small class="text-info">이제부터 가입할 때 사용한 이메일 또는 닉네임과 패스워드로 로그인 할 수 있습니다.</small>
    </div>

</body>
</html>

 

 

getEmailCheckToken : null값 에러 발생

@Service
@RequiredArgsConstructor
public class AccountService {

    private final AccountRepository accountRepository;
    private final JavaMailSender javaMailSender;
    private final PasswordEncoder passwordEncoder;

    @Transactional
    public void processNewAccount(SignUpForm signUpForm) {

        Account newAccount = saveNewAccount(signUpForm);
        //Null값 에러 발생
        newAccount.generateEmailCheckToken();

        sendSignUpConfirmEmail(newAccount);
    }


    private Account saveNewAccount(@Valid SignUpForm signUpForm) {
        Account account = Account.builder()
                .email(signUpForm.getEmail())
                .nickname(signUpForm.getNickname())
                .password(passwordEncoder.encode(signUpForm.getPassword()))
                .studyCreatedByWeb(true)
                .studyEnrollmentResultByWeb(true)
                .studyUpdatedByWeb(true)
                .build();

        return accountRepository.save(account);
    }

    private void sendSignUpConfirmEmail(Account newAccount) {
        //이메일 전송
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setTo(newAccount.getEmail());
        mailMessage.setSubject("닷 스터디, 회원 가입 인증");
        mailMessage.setText("/check-email-token?token="+ newAccount.getEmailCheckToken() +
                "&email=" + newAccount.getEmail());
        javaMailSender.send(mailMessage);
    }

}

 

saveNewAccount메소드 안에서만 accountRepository.save()로 Transaction이 일어났기 때문에 해당하는 엔티티는 persist상태

그러나 그 메소드를 벗어난 상태에서는 processNewAccount메소드에 있는 newAccount에서는 detached상태이다.

(왜냐? save를 벗어났기 때문에)

 

따라서, processNewAccount메소드에 @Transactional을 명시해주어야 한다.

 

 

회원가입 후 해당 토큰을 복붙하면 끝

 

 

결과화면

인증 확인 뷰
인증 확인 에러 뷰

 

 


출처

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