최원종의 개발 블로그

V15-3 인증 이메일 발송 API 테스트 본문

Spring boot 입문/Spring boot 입문 -1

V15-3 인증 이메일 발송 API 테스트

chl6698 2026. 6. 18. 12:56

인증 이메일 발송 API 테스트


MailUtil 코드

package com.tenco.blog._core.util;

import java.util.Random;

public class MailUtil {

    // 6자리 랜덤 숫자 생성 (100000 ~ 999999)
    public static String generateRandomCode() {
        Random random = new Random();
        int code = 100000  + random.nextInt(900000);
        return String.valueOf(code);
    }
}

UserRequest 코드

    @Data
    public static class EmailCheckDTO {
        private String email; // 이메일 주소
        private String code; // 인증번호(번호 확인시 사용(

        public void validate() {
            if(email == null || email.trim().isEmpty()) {
                throw new Exception400("이메일을 입력해주세요");
            }
            if(!email.contains("@")) {
                throw new Exception400("올바른 이메일 형식이 아닙니다");
            }
        }
    }

MailService 코드

package com.tenco.blog.user;

import com.tenco.blog._core.util.MailUtil;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

/**
 * 메일 발송 서비스
 *  - 비즈니스 로직
 *    1. 인증 번호 생성
 *    2. 이메일 발송
 *    3. 세션에 인증번호 저장(검증용)
 *
 */
@Service // IoC
@RequiredArgsConstructor
public class MailService {

    // 스프링 부트가 제공하는 메일 발송 객체
    // application-dev.yml 에 설정된 STMP 정보를 사용하여 메일 발송
    private final JavaMailSender javaMailSender;
    private final HttpSession session;

    public void 인증번호발송(String email) {
        // 1. 인증 번호 생성
        String code = MailUtil.generateRandomCode();
        System.out.println("생성된 인증 번호 : " + code);

        // 2. 이메일 발송 (전송 내용 만들기)
        // MimeMessage 란 - 이메일을 보낼 때 내용을 작성하는 편지봉투이다
        // 텍스트 뿐만 아니라 HTML, 첨부파일등을 포함할 수 있는 포맷이다.
        MimeMessage emailMessage = javaMailSender.createMimeMessage();

        try {
            // true 멀티파트 허용(파일 사용도 허용)
            MimeMessageHelper helper = new MimeMessageHelper(emailMessage, true, "UTF-8");

            helper.setTo(email); // 받는 사람 설정
            helper.setSubject("[m-blog 회원가입 인증 번호]"); // 메일에 제목 설정
            helper.setText("<h3>인증 번호는 "+ code +" 입니다</h3>", true); // 메일 본문 설정
            javaMailSender.send(emailMessage);

            // 세션에 인증번호 임시 저장
            // 세션 --> devnote1@naver.com --> code_devnote1@naver.com : 123456
            session.setAttribute("code_" + email, code);
            System.out.println("인증번호 발송 완료 : " + code); // TODO - 추후 삭제

        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }
}

인증 번호 검증 API 테스트


UserApiController 코드

package com.tenco.blog.user;

// AJAX 통신 요청을 받아서 처리하는 컨트롤러

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController // Controller + Response
@RequiredArgsConstructor
public class UserApiController {

    private final MailService mailService;

    // 인증 번호 발송 API
    // 주소 설계 : http://localhost:8080/api/email/send , POST
    // @RequestBody <-- 클라이언트 단에서 application/json 형식으로 요청을 준다라고 약속
    // 생성된 HTTP 요청 메세지에서 요청 본문(body) 값을 추출해서 객체로 변환 해
    @PostMapping("/api/email/send")
    public ResponseEntity<?> sendEmail(@RequestBody UserRequest.EmailCheckDTO reqDTO) {
        reqDTO.validate();
        mailService.인증번호발송(reqDTO.getEmail());
        return ResponseEntity.ok("인증번호 발송 됨");
    }

    /**
     *   {
     *       "email" : "본인@naver.com",
     *       "code"  : 123456
     *   }
      *
     */
    // 인증 번호 검증 요청 API
    @PostMapping("/api/email/verify")
    public ResponseEntity<?> 인증번호확인(@RequestBody UserRequest.EmailCheckDTO reqDTO) {
        reqDTO.validate();
        if(reqDTO.getCode() == null || reqDTO.getCode().trim().isEmpty()) {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "인증번호를 입력해주세요"));
        }
        // 인증 번호 검사 로직 메일 서비스로 위임
        boolean  isVerified =  mailService.인증번호확인(reqDTO.getEmail(), reqDTO.getCode());
        // 결과에 따른 응답처리
        if (isVerified) {
            return ResponseEntity.ok()
                    .body(Map.of("message", "인증되었습니다"));
        } else {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "인증번호가 일치하지 않습니다"));
        }
    }

}

MailService 코드

package com.tenco.blog.user;

import com.tenco.blog._core.util.MailUtil;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

/**
 * 메일 발송 서비스
 *  - 비즈니스 로직
 *    1. 인증 번호 생성
 *    2. 이메일 발송
 *    3. 세션에 인증번호 저장(검증용)
 *
 */
@Service // IoC
@RequiredArgsConstructor
public class MailService {

    // 스프링 부트가 제공하는 메일 발송 객체
    // application-dev.yml 에 설정된 STMP 정보를 사용하여 메일 발송
    private final JavaMailSender javaMailSender;
    private final HttpSession session;

    public void 인증번호발송(String email) {
        // 1. 인증 번호 생성
        String code = MailUtil.generateRandomCode();
        System.out.println("생성된 인증 번호 : " + code);

        // 2. 이메일 발송 (전송 내용 만들기)
        // MimeMessage 란 - 이메일을 보낼 때 내용을 작성하는 편지봉투이다
        // 텍스트 뿐만 아니라 HTML, 첨부파일등을 포함할 수 있는 포맷이다.
        MimeMessage emailMessage = javaMailSender.createMimeMessage();

        try {
            // true 멀티파트 허용(파일 사용도 허용)
            MimeMessageHelper helper = new MimeMessageHelper(emailMessage, true, "UTF-8");

            helper.setTo(email); // 받는 사람 설정
            helper.setSubject("[m-blog 회원가입 인증 번호]"); // 메일에 제목 설정
            helper.setText("<h3>인증 번호는 "+ code +" 입니다</h3>", true); // 메일 본문 설정
            javaMailSender.send(emailMessage);

            // 세션에 인증번호 임시 저장
            // 세션 --> devnote1@naver.com --> code_devnote1@naver.com : 123456
            session.setAttribute("code_" + email, code);
            System.out.println("인증번호 발송 완료 : " + code); // TODO - 추후 삭제

        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }

    public boolean 인증번호확인(String email, String code) {
        // 1. 인증번호발송 API ---> 세션에 임시 코드값이 저장된 상태이다.
        // code_devnote1@naver.com : 123456
        String savedCode = (String) session.getAttribute("code_" + email); // 이 값으로 - value : 값을 추출
        if (savedCode != null && savedCode.equals(code)) {
            // 인증된 상태를 의미 한다
            // 기존에 저장된 세션 값을 반드시 제거
            session.removeAttribute("code_" + email);
            // 이 이메일은 은증 완료됨이란 상태를 다시 세션에 저장한다 (도장찍어둔다)
            // 회원 가입시 UserService 에서 이 도장을 확인하고 회원 가입 처리를 한다.
            session.setAttribute("verified_email", email);
            return true;
        } else {
            return false;
        }
    }
}

인증, 검증 UI + AJAX

\templates\user\join-form.mustache

{{> layout/header}}
<div class="container p-5 flex-grow-1">
    <div class="card">
        <div class="card-header"><b>회원가입을해주세요</b></div>
        <div class="card-body">
            <!--
                중요!!
                파일 업로드 사용시 method (POST)
                enctype 을 반드시 multipart/form-data 설정 해주어야 한다.
                설정을 안하면 파일 데이터를 서버측에서 받을 수 없음
                (form 태그 사용시 서버측으로 파일 데이터를 보내는 방법)

            -->
            <form action="/join" method="post" enctype="multipart/form-data" onsubmit="return checkVerify()">
                <div class="mb-3">
                    <input type="text" class="form-control" placeholder="enter username" name="username" value="cos5">
                </div>
                <div class="mb-3">
                    <input type="password" class="form-control" placeholder="enter password" name="password"
                           value="1234">
                </div>


                <div class="mb-3">
                    <label for="" class="form-lable">이메일</label>
                    <div class="input-group">
                        <input type="email" class="form-control email"
                               placeholder="enter email" name="email" value="aa@naver.com" required>
                        <button type="button" class="btn btn-outline-secondary" onclick="callSendApi()">인증번호 전송</button>
                    </div>
                </div>

                <!--   인증번호 입력 (처음에 숨김, 발송 후 보임)  -->
                <div class="mb-3" id="code-box" style="display: none;">
                    <label for="" class="form-label">인증번호</label>
                    <div class="input-group">
                        <input type="text" class="form-control" id="code"
                               placeholder="인증번호 6자리" maxlength="6">
                        <button type="button" class="btn btn-outline-secondary"
                                onclick="callVerifyApi()">확인
                        </button>
                    </div>
                    <div id="msg"></div>
                </div>

                <!--   인증 완료 여부 저장 필드 (hidden 필드)     -->
                <input type="hidden" id="isEmailVerified" value="false">

                <!--  프로필 이미지 등록 필드(선택 사항)  -->
                <div class="mb-3">
                    <label for="profileImage" class="form-label">프로필 사진(선택사항)</label>
                    <input type="file" id="profileImage" class="form-control" name="profileImage" accept="image/*">
                    <small class="form-text text-muted">이미지 파일만 업로드 가능합니다(JPG, PNG, GIF 등)</small>
                    <small class="form-text text-muted d-block mt-1">⭐ 프로필 사진을 등록하지 않아도 회원 가입 가능 </small>
                </div>
                <!--  submit 으로 다시 정정               -->
                <button type="submit" class="btn btn-primary form-control" >회원가입</button>
            </form>
        </div>
    </div>
</div>
<script>
    // 1. 인증번호 메일 발송 API
    async function callSendApi() {
      const email = document.querySelector(".email").value;
      if(email == false) {
        alert("이메일을 입력해주세요");
        return;
      }

    const response = await fetch("/api/email/send", {
        method: "POST",
        headers: {"Content-Type" : "application/json; charset=UTF-8"},
        body: JSON.stringify({email : email}),
      });

    if(response.ok) {
        alert("인증번호가 발송되었습니다. 메일함을 확인하세요");
        // 인증 번호 입력창 노출
        document.querySelector("#code-box").style.display = "block";
    } else {
        let error = await response.json();
        alert("실패 : " + error.message);
    }

    } // end of callSendApi

    // 2. 인증번호 검증 API
    async function callVerifyApi() {
          const email = document.querySelector(".email").value;
          const code = document.querySelector("#code").value;
          if(code == false || email == false) {
            alert("이메일 또는 인증코드가 필요 합니다");
            return;
          }

        try {
            const response = await fetch("/api/email/verify",
            {
                method: "POST",
                headers: {"Content-Type" : "application/json; charset=UTF-8"},
                body: JSON.stringify({email : email, code: code})
             });


            const result = await response.json();
            const msgElement = document.querySelector("#msg");

            if(response.ok) {
                console.log(result);
                console.log(typeof result);
                // 인증 성공
                msgElement.innerHTML = `<span style='color:green'> ${result.message} </span>`;
                document.querySelector("#isEmailVerified").value = true;
                // 이메일 인증 성공 후 사용자가 값을 변경 못 하게 1단계 처리
                document.querySelector(".email").readOnly = true;
            } else {
                msgElement.innerHTML = `<span style='color:red'> ${result.message} </span>`;
            }
        } catch(error) {

            alert("인증실패" + error.message);
        }
    }

    // 3. 회원가입 폼 제출 시 최종 체크
    function checkVerify() {
        let isVerified = document.querySelector("#isEmailVerified").value;
        if(isVerified == "false") {
            alert("이메일 인증을 먼저 완료해주세요");
            return false; // 폼 제출 막기
        }
        return true; // 폼 제출 허용
    }
</script>
{{> layout/footer}}