Skip to content

Commit

Permalink
Merge pull request #177 from 9uttery/feat/#173
Browse files Browse the repository at this point in the history
[Feature] 이메일 전송 및 인증 기능 구현
  • Loading branch information
mingeun0507 authored Apr 20, 2024
2 parents f18d3f9 + fcdf5ae commit 02b7c19
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 1 deletion.
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ repositories {

dependencies {
// Spring Boot 의존성
// JPA, Redis, Security, Validation, Web, Cache
// JPA, Redis, Security, Validation, Web, Cache, Mail, Thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-mail:3.1.2'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// AWS S3, CloudWatch
implementation 'io.awspring.cloud:spring-cloud-aws-s3:3.0.2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public enum ErrorDetails {

FILE_UPLOAD_FAILED("F001", HttpStatus.INTERNAL_SERVER_ERROR.value(), "파일 업로드에 실패했습니다."),

MAIL_SEND_FAILED("M001", HttpStatus.INTERNAL_SERVER_ERROR.value(), "메일 전송 중 오류가 발생했습니다."),
MAIL_VERIFY_FAILED("M002", HttpStatus.BAD_REQUEST.value(), "이메일 인증에 실패했습니다."),

FIREBASE_INTEGRATION_FAILED("FI001", HttpStatus.INTERNAL_SERVER_ERROR.value(), "Firebase 연동 중 오류가 발생했습니다. 다시 시도해 주세요."),
FIREBASE_NOTIFICATION_SEND_ERROR("FI002", HttpStatus.INTERNAL_SERVER_ERROR.value(), "Firebase 알림 전송 중 오류가 발생했습니다."),
USER_TOKEN_INFORMATION_NOT_EXISTS("FI003", HttpStatus.NOT_FOUND.value(), "사용자의 토큰 정보가 존재하지 않습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.guttery.madii.domain.mail.application;

import com.guttery.madii.common.exception.CustomException;
import com.guttery.madii.common.exception.ErrorDetails;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

import java.io.UnsupportedEncodingException;
import java.util.concurrent.TimeUnit;

@RequiredArgsConstructor
@Slf4j
@Service
public class MailSendService {
private static final Long EXPIRE_TIME = 1000 * 60 * 3L + 1000 * 10; // 3분 10초

private final JavaMailSender javaMailSender;
private final SpringTemplateEngine springTemplateEngine;
private final RedisTemplate<String, String> redisTemplate;

public void sendSignUpMail(final String email) {
final String code = MailVerificationCodeGenerator.generate(); // 인증코드 생성
final MimeMessage message = javaMailSender.createMimeMessage();

try {
message.addRecipients(MimeMessage.RecipientType.TO, email); // 보낼 이메일 설정
message.setSubject("[MADII] 회원가입 인증번호 발송"); // 이메일 제목
message.setText(setContext(code), "utf-8", "html"); // 내용 설정(Template Process)
message.setFrom(new InternetAddress("[email protected]", "마디"));
} catch (final MessagingException | UnsupportedEncodingException e) {
throw CustomException.of(ErrorDetails.MAIL_SEND_FAILED);
}

redisTemplate.opsForValue().set(email, code, EXPIRE_TIME, TimeUnit.MILLISECONDS); // Redis에 인증코드 저장
javaMailSender.send(message); // 이메일 전송
}

private String setContext(final String code) { // 타임리프 설정하는 코드
final Context context = new Context();
context.setVariable("code", code); // Template에 전달할 데이터 설정

return springTemplateEngine.process("mail", context); // mail.html
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.guttery.madii.domain.mail.application;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.RandomUtils;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MailVerificationCodeGenerator {
private static final int CODE_LOWER_BOUND = 100000;
private static final int CODE_UPPER_BOUND = 1000000;

public static String generate() {
return String.valueOf(RandomUtils.nextInt(CODE_LOWER_BOUND, CODE_UPPER_BOUND));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.guttery.madii.domain.mail.application;

import com.guttery.madii.common.exception.CustomException;
import com.guttery.madii.common.exception.ErrorDetails;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Slf4j
@Service
public class MailVerifyService {
private final RedisTemplate<String, String> redisTemplate;

@Transactional(readOnly = true, propagation = Propagation.MANDATORY)
public void verifyEmail(final String email, final String code) {
final String redisCode = redisTemplate.opsForValue().get(email);
if (redisCode == null || !redisCode.equals(code)) {
throw CustomException.of(ErrorDetails.MAIL_VERIFY_FAILED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.guttery.madii.domain.mail.presentation;

import com.guttery.madii.domain.mail.application.MailSendService;
import com.guttery.madii.domain.mail.application.MailVerifyService;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/mail")
@Validated
public class MailController {
private final MailSendService mailSendService;
private final MailVerifyService mailVerifyService;

@GetMapping("/v1/sign-up")
public void sendSignUpMail(
@NotBlank @RequestParam("email") final String email
) {
mailSendService.sendSignUpMail(email);
}

@GetMapping("/v1/password-reset")
public void sendPasswordResetMail(
@NotBlank @RequestParam("email") final String email
) {
mailSendService.sendSignUpMail(email); // TODO: 추후 수정 필요
}

@GetMapping("/v1/verify")
public void verifyEmail(
@NotBlank @RequestParam("email") final String email,
@NotBlank @RequestParam("code") final String code
) {
mailVerifyService.verifyEmail(email, code);
}
}
36 changes: 36 additions & 0 deletions src/main/resources/templates/mail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>회원가입 인증</title>
</head>
<body style="margin: 0; padding: 0; background-color: #F6F6F6; font-family: 'Pretendard', sans-serif;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color: #F6F6F6;" width="100%">
<tr>
<td align="center" style="padding: 20px;" valign="top">
<!-- 이메일 본문 테이블 -->
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius: 14px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
<tr>
<td style="background-image: url('https://cdn.madii.kr/signup_email_background.png'); background-size: cover; background-position: top; border-radius: 14px 14px 0 0; padding: 50px; text-align: left;">
<!-- 상단 이미지와 텍스트 -->
<h1 style="color: #FFFFFF; font-size: 28px; font-weight: 700; margin-top: 0; margin-bottom: 24px;">
회원가입을 진심으로 환영합니다!</h1>
<p style="color: #FFFFFF; font-size: 16px; font-weight: 500; margin-top: 0; margin-bottom: 24px;">
안녕하세요. 마디입니다.<br>
아래의 인증코드 6자리를 입력하여 회원가입을 완료해주세요.<br>
만약 해당 이메일로 가입을 시도한 적이 없다면 [email protected]로 문의바랍니다.
</p>
<!-- 인증코드 표시 부분 -->
<td style="font-size: 42px; font-family: 'Pretendard', sans-serif; font-weight: 600; line-height: 63px; color: black; padding-top: 30px;">
<p style="text-align: center; font-size: 1.25rem; font-weight: bold; border: none; border-radius: 0.25rem; padding: 1rem 1.25rem; margin-bottom: 1rem; background-color: #F6F6F6; color: #717171;"
th:text="${code}"></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

0 comments on commit 02b7c19

Please sign in to comment.