From d5b681b375891b494c56ccacfa3d451dbe18b4f2 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 11 Jul 2025 14:26:44 +0900 Subject: [PATCH 1/8] =?UTF-8?q?:sparkles:=20feat:=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=EC=BD=94=EB=93=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=B0=8F=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84(=EB=A0=88=EB=94=94=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EmailVerificationStorageConstants.java | 15 +++++++++ ...VerificationCodeStorageCommandService.java | 28 ++++++++++++++++ ...VerificationCodeStorageCommandService.java | 33 +++++++++++++++++++ ...ilVerificationCodeStorageQueryService.java | 6 ++++ ...ilVerificationCodeStorageQueryService.java | 23 +++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/constants/EmailVerificationStorageConstants.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailVerificationCodeStorageCommandService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/RedisEmailVerificationCodeStorageCommandService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/EmailVerificationCodeStorageQueryService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/RedisEmailVerificationCodeStorageQueryService.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/constants/EmailVerificationStorageConstants.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/constants/EmailVerificationStorageConstants.java new file mode 100644 index 0000000..0f3b0ed --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/constants/EmailVerificationStorageConstants.java @@ -0,0 +1,15 @@ +package org.withtime.be.withtimebe.domain.auth.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +import java.time.Duration; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class EmailVerificationStorageConstants { + + public static final String VERIFICATION_CODE_PREFIX = "EMAIL-VERIFICATION-CODE:"; + public static final String EMAIL_VERIFICATION_PREFIX = "EMAIL-VERIFICATION:"; + public static final Duration VERIFICATION_CODE_DURATION = Duration.ofMinutes(3); + public static final Duration EMAIL_VERIFICATION_DURATION = Duration.ofHours(1); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailVerificationCodeStorageCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailVerificationCodeStorageCommandService.java new file mode 100644 index 0000000..d2c289f --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailVerificationCodeStorageCommandService.java @@ -0,0 +1,28 @@ +package org.withtime.be.withtimebe.domain.auth.service.command; + +public interface EmailVerificationCodeStorageCommandService { + /** + * 인증 코드 저장 + * @param email 인증 코드를 확인할 이메일 + * @param verificationCode 이메일에 대한 인증 코드 + */ + void saveVerificationCode(String email, String verificationCode); + + /** + * 이메일에 대한 인증이 완료됨을 저장 + * @param email 인증이 완료됨을 저장할 이메일 + */ + void saveVerifiedEmail(String email); + + /** + * 이메일에 대한 인증 코드 삭제 + * @param email 인증 코드를 삭제할 이메일 + */ + void deleteVerificationCode(String email); + + /** + * 인증 정보 삭제 + * @param email 인증 정보를 삭제할 이메일 + */ + void deleteVerifiedEmail(String email); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/RedisEmailVerificationCodeStorageCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/RedisEmailVerificationCodeStorageCommandService.java new file mode 100644 index 0000000..5cc5e7f --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/RedisEmailVerificationCodeStorageCommandService.java @@ -0,0 +1,33 @@ +package org.withtime.be.withtimebe.domain.auth.service.command; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.withtime.be.withtimebe.domain.auth.constants.EmailVerificationStorageConstants; +import org.withtime.be.withtimebe.global.util.RedisUtil; + +@Service +@RequiredArgsConstructor +public class RedisEmailVerificationCodeStorageCommandService implements EmailVerificationCodeStorageCommandService { + + private final RedisUtil redisUtil; + + @Override + public void saveVerificationCode(String email, String verificationCode) { + redisUtil.set(EmailVerificationStorageConstants.VERIFICATION_CODE_PREFIX+ email, verificationCode, EmailVerificationStorageConstants.VERIFICATION_CODE_DURATION); + } + + @Override + public void saveVerifiedEmail(String email) { + redisUtil.set(EmailVerificationStorageConstants.EMAIL_VERIFICATION_PREFIX, true, EmailVerificationStorageConstants.EMAIL_VERIFICATION_DURATION); + } + + @Override + public void deleteVerificationCode(String email) { + redisUtil.delete(EmailVerificationStorageConstants.VERIFICATION_CODE_PREFIX + email); + } + + @Override + public void deleteVerifiedEmail(String email) { + redisUtil.delete(EmailVerificationStorageConstants.EMAIL_VERIFICATION_PREFIX + email); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/EmailVerificationCodeStorageQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/EmailVerificationCodeStorageQueryService.java new file mode 100644 index 0000000..32f5386 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/EmailVerificationCodeStorageQueryService.java @@ -0,0 +1,6 @@ +package org.withtime.be.withtimebe.domain.auth.service.query; + +public interface EmailVerificationCodeStorageQueryService { + boolean checkVerificationCode(String email, String verificationCode); + boolean isVerified(String email); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/RedisEmailVerificationCodeStorageQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/RedisEmailVerificationCodeStorageQueryService.java new file mode 100644 index 0000000..872ccfb --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/RedisEmailVerificationCodeStorageQueryService.java @@ -0,0 +1,23 @@ +package org.withtime.be.withtimebe.domain.auth.service.query; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.withtime.be.withtimebe.domain.auth.constants.EmailVerificationStorageConstants; +import org.withtime.be.withtimebe.global.util.RedisUtil; + +@Service +@RequiredArgsConstructor +public class RedisEmailVerificationCodeStorageQueryService implements EmailVerificationCodeStorageQueryService { + + private final RedisUtil redisUtil; + + @Override + public boolean checkVerificationCode(String email, String verificationCode) { + return redisUtil.get(EmailVerificationStorageConstants.VERIFICATION_CODE_PREFIX, String.class).equals(verificationCode); + } + + @Override + public boolean isVerified(String email) { + return redisUtil.has(EmailVerificationStorageConstants.EMAIL_VERIFICATION_PREFIX + email); + } +} From 80c74b439ec2239e52abd9b9ff73ad214a307663 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 11 Jul 2025 14:27:12 +0900 Subject: [PATCH 2/8] =?UTF-8?q?:sparkles:=20feat:=206=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=EC=88=AB=EC=9E=90=20=EC=9D=B8=EC=A6=9D=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EA=B8=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/generator/RandomGenerator.java | 6 ++++++ .../auth/generator/RandomSixDigitGenerator.java | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomGenerator.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomSixDigitGenerator.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomGenerator.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomGenerator.java new file mode 100644 index 0000000..e5fc6a6 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomGenerator.java @@ -0,0 +1,6 @@ +package org.withtime.be.withtimebe.domain.auth.generator; + +public interface RandomGenerator { + + T generateRandom(); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomSixDigitGenerator.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomSixDigitGenerator.java new file mode 100644 index 0000000..9d6e88b --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomSixDigitGenerator.java @@ -0,0 +1,17 @@ +package org.withtime.be.withtimebe.domain.auth.generator; + +import org.springframework.stereotype.Component; + +import java.security.SecureRandom; +import java.util.Random; + +@Component +public class RandomSixDigitGenerator implements RandomGenerator { + + private static final Random RANDOM = new SecureRandom(); + + @Override + public Integer generateRandom() { + return RANDOM.nextInt(1000000); + } +} From 7e5584b65cea8a6adf30b7a4ffaec310320a8596 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 11 Jul 2025 15:40:52 +0900 Subject: [PATCH 3/8] =?UTF-8?q?:sparkles:=20feat:=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=84=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++ .../auth/util/MailVerificationCodeSender.java | 5 +++ .../util/SMTPMailVerificationCodeSender.java | 40 +++++++++++++++++++ .../global/error/code/EmailErrorCode.java | 27 +++++++++++++ .../error/exception/EmailException.java | 10 +++++ src/main/resources/application.yml | 11 +++++ .../templates/email-verification.html | 28 +++++++++++++ 7 files changed, 126 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/util/MailVerificationCodeSender.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/util/SMTPMailVerificationCodeSender.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/code/EmailErrorCode.java create mode 100644 src/main/java/org/withtime/be/withtimebe/global/error/exception/EmailException.java create mode 100644 src/main/resources/templates/email-verification.html diff --git a/build.gradle b/build.gradle index 494dbc6..093801e 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,11 @@ dependencies { // redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // Email + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + } tasks.named('test') { diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/util/MailVerificationCodeSender.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/util/MailVerificationCodeSender.java new file mode 100644 index 0000000..e970459 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/util/MailVerificationCodeSender.java @@ -0,0 +1,5 @@ +package org.withtime.be.withtimebe.domain.auth.util; + +public interface MailVerificationCodeSender { + void sendMail(String toEmail, String code); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/util/SMTPMailVerificationCodeSender.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/util/SMTPMailVerificationCodeSender.java new file mode 100644 index 0000000..51a2638 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/util/SMTPMailVerificationCodeSender.java @@ -0,0 +1,40 @@ +package org.withtime.be.withtimebe.domain.auth.util; + +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.withtime.be.withtimebe.global.error.code.EmailErrorCode; +import org.withtime.be.withtimebe.global.error.exception.EmailException; + +@Component +@RequiredArgsConstructor +public class SMTPMailVerificationCodeSender implements MailVerificationCodeSender { + private final JavaMailSender mailSender; + private final TemplateEngine templateEngine; + + @Override + public void sendMail(String toEmail, String code) { + + // 1. 템플릿 처리 + Context context = new Context(); + context.setVariable("code", code); + String html = templateEngine.process("email-verification", context); + + // 2. 메일 작성 및 전송 + MimeMessage mimeMessage = mailSender.createMimeMessage(); + try { + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "UTF-8"); + helper.setTo(toEmail); + helper.setSubject("[WithTime] 이메일 인증 코드입니다."); + helper.setText(html, true); // true → HTML 형식 + + mailSender.send(mimeMessage); + } catch (Exception e) { + throw new EmailException(EmailErrorCode.FAIL_EMAIL_SEND); + } + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/code/EmailErrorCode.java b/src/main/java/org/withtime/be/withtimebe/global/error/code/EmailErrorCode.java new file mode 100644 index 0000000..e8f608f --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/code/EmailErrorCode.java @@ -0,0 +1,27 @@ +package org.withtime.be.withtimebe.global.error.code; + +import lombok.AllArgsConstructor; +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.code.dto.supports.DefaultResponseErrorReasonDTO; +import org.springframework.http.HttpStatus; + +@AllArgsConstructor +public enum EmailErrorCode implements BaseErrorCode { + + FAIL_EMAIL_SEND(HttpStatus.INTERNAL_SERVER_ERROR, "EMAIL500_1", "이메일 전송에 실패했습니다."), + INCORRECT_EMAIL_VERIFICATION_CODE(HttpStatus.UNAUTHORIZED, "EMAIL401_1", "이메일 인증에 실패했습니다."), + UNVERIFIED_EMAIL(HttpStatus.UNAUTHORIZED, "EMAIL401_2", "인증되지 않은 이메일이거나 인증 유효기간이 지났습니다."), + ; + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public DefaultResponseErrorReasonDTO getReason() { + return DefaultResponseErrorReasonDTO.builder() + .httpStatus(this.httpStatus) + .code(this.code) + .message(this.message) + .build(); + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/global/error/exception/EmailException.java b/src/main/java/org/withtime/be/withtimebe/global/error/exception/EmailException.java new file mode 100644 index 0000000..2ceeac6 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/global/error/exception/EmailException.java @@ -0,0 +1,10 @@ +package org.withtime.be.withtimebe.global.error.exception; + +import org.namul.api.payload.code.BaseErrorCode; +import org.namul.api.payload.error.exception.ServerApplicationException; + +public class EmailException extends ServerApplicationException { + public EmailException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a8e9cfd..73e6586 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,6 +15,17 @@ spring: redis: host: ${REDIS_HOST} port: 6379 + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL_SENDER_USERNAME} + password: ${MAIL_SENDER_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true jwt: secret: ${JWT_SECRET} diff --git a/src/main/resources/templates/email-verification.html b/src/main/resources/templates/email-verification.html new file mode 100644 index 0000000..eb6c6ff --- /dev/null +++ b/src/main/resources/templates/email-verification.html @@ -0,0 +1,28 @@ + + + + + 이메일 인증 + + + +
+ + +
+ + \ No newline at end of file From a0ba2abd167c38b2c99a4af813dedb4c99672d5f Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 11 Jul 2025 21:12:09 +0900 Subject: [PATCH 4/8] =?UTF-8?q?:bug:=20fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=A1=B0=EA=B1=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/command/AuthCommandServiceImpl.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java index fe20212..213b4f0 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java @@ -7,14 +7,17 @@ import org.springframework.stereotype.Service; import org.withtime.be.withtimebe.domain.auth.converter.AuthConverter; import org.withtime.be.withtimebe.domain.auth.dto.request.AuthRequestDTO; +import org.withtime.be.withtimebe.domain.auth.service.query.EmailVerificationCodeStorageQueryService; import org.withtime.be.withtimebe.domain.auth.service.query.TokenQueryService; import org.withtime.be.withtimebe.domain.auth.service.query.TokenStorageQueryService; import org.withtime.be.withtimebe.domain.member.entity.Member; import org.withtime.be.withtimebe.domain.member.repository.MemberRepository; import org.withtime.be.withtimebe.global.error.code.AuthErrorCode; +import org.withtime.be.withtimebe.global.error.code.EmailErrorCode; import org.withtime.be.withtimebe.global.error.code.MemberErrorCode; import org.withtime.be.withtimebe.global.error.code.TokenErrorCode; import org.withtime.be.withtimebe.global.error.exception.AuthException; +import org.withtime.be.withtimebe.global.error.exception.EmailException; import org.withtime.be.withtimebe.global.error.exception.MemberException; import org.withtime.be.withtimebe.global.error.exception.TokenException; import org.withtime.be.withtimebe.global.security.constants.AuthenticationConstants; @@ -31,6 +34,7 @@ public class AuthCommandServiceImpl implements AuthCommandService { private final TokenStorageCommandService tokenStorageCommandService; private final TokenQueryService tokenQueryService; private final TokenStorageQueryService tokenStorageQueryService; + private final EmailVerificationCodeStorageQueryService emailVerificationCodeStorageQueryService; @Override public void signUp(AuthRequestDTO.SignUp request) { @@ -87,6 +91,9 @@ private void validateSignUp(AuthRequestDTO.SignUp request) throws AuthException if (memberRepository.existsByEmail(request.email())) { throw new AuthException(AuthErrorCode.ALREADY_EXIST_EMAIL); } + if (!emailVerificationCodeStorageQueryService.isVerified(request.email())) { + throw new EmailException(EmailErrorCode.UNVERIFIED_EMAIL); + } } private Long getUserId(String token) { From 03550fe8f56c988bafea72f6a23fd2f604300af3 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 11 Jul 2025 21:12:53 +0900 Subject: [PATCH 5/8] =?UTF-8?q?:bug:=20fix:=20NPE,=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=20=ED=82=A4=20=EA=B0=92=20=EB=B0=8F=20develop=20yml?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...disEmailVerificationCodeStorageCommandService.java | 4 ++-- ...RedisEmailVerificationCodeStorageQueryService.java | 2 +- src/main/resources/application-develop.yml | 11 +++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/RedisEmailVerificationCodeStorageCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/RedisEmailVerificationCodeStorageCommandService.java index 5cc5e7f..ee7918e 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/RedisEmailVerificationCodeStorageCommandService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/RedisEmailVerificationCodeStorageCommandService.java @@ -13,12 +13,12 @@ public class RedisEmailVerificationCodeStorageCommandService implements EmailVer @Override public void saveVerificationCode(String email, String verificationCode) { - redisUtil.set(EmailVerificationStorageConstants.VERIFICATION_CODE_PREFIX+ email, verificationCode, EmailVerificationStorageConstants.VERIFICATION_CODE_DURATION); + redisUtil.set(EmailVerificationStorageConstants.VERIFICATION_CODE_PREFIX + email, verificationCode, EmailVerificationStorageConstants.VERIFICATION_CODE_DURATION); } @Override public void saveVerifiedEmail(String email) { - redisUtil.set(EmailVerificationStorageConstants.EMAIL_VERIFICATION_PREFIX, true, EmailVerificationStorageConstants.EMAIL_VERIFICATION_DURATION); + redisUtil.set(EmailVerificationStorageConstants.EMAIL_VERIFICATION_PREFIX + email, true, EmailVerificationStorageConstants.EMAIL_VERIFICATION_DURATION); } @Override diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/RedisEmailVerificationCodeStorageQueryService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/RedisEmailVerificationCodeStorageQueryService.java index 872ccfb..cb5b0b3 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/RedisEmailVerificationCodeStorageQueryService.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/query/RedisEmailVerificationCodeStorageQueryService.java @@ -13,7 +13,7 @@ public class RedisEmailVerificationCodeStorageQueryService implements EmailVerif @Override public boolean checkVerificationCode(String email, String verificationCode) { - return redisUtil.get(EmailVerificationStorageConstants.VERIFICATION_CODE_PREFIX, String.class).equals(verificationCode); + return verificationCode.equals(redisUtil.get(EmailVerificationStorageConstants.VERIFICATION_CODE_PREFIX + email, String.class)); } @Override diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index e9cfc7a..8bc022a 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -15,6 +15,17 @@ spring: redis: host: ${REDIS_HOST} port: 6379 + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL_SENDER_USERNAME} + password: ${MAIL_SENDER_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true jwt: secret: ${JWT_SECRET} From e8737cae152147df83c8f7af5e325dbc6f25acb7 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 11 Jul 2025 21:20:23 +0900 Subject: [PATCH 6/8] =?UTF-8?q?:sparkles:=20feat:=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 31 ++++++++++++++ .../auth/dto/request/EmailRequestDTO.java | 20 +++++++++ .../service/command/EmailCommandService.java | 8 ++++ .../command/EmailCommandServiceImpl.java | 42 +++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/EmailRequestDTO.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandService.java create mode 100644 src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java index ed2f300..2ad149b 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/controller/AuthController.java @@ -11,7 +11,9 @@ import org.namul.api.payload.response.DefaultResponse; import org.springframework.web.bind.annotation.*; import org.withtime.be.withtimebe.domain.auth.dto.request.AuthRequestDTO; +import org.withtime.be.withtimebe.domain.auth.dto.request.EmailRequestDTO; import org.withtime.be.withtimebe.domain.auth.service.command.AuthCommandService; +import org.withtime.be.withtimebe.domain.auth.service.command.EmailCommandService; @RestController @RequiredArgsConstructor @@ -20,6 +22,7 @@ public class AuthController { private final AuthCommandService authCommandService; + private final EmailCommandService emailCommandService; @Operation(summary = "회원가입 API by 요시", description = "최초 회원가입 시 필요한 정보를 포함하여 회원가입 진행") @ApiResponses({ @@ -86,4 +89,32 @@ public DefaultResponse logout(HttpServletRequest request, HttpServletRes authCommandService.logout(request, response); return DefaultResponse.noContent(); } + + @Operation(summary = "이메일 인증 번호 전송 API by 요시", description = "이메일로 인증 번호를 전송하는 API") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "이메일 전송 성공, 인증 코드는 3분 동안 유효"), + @ApiResponse( + responseCode = "500", + description = "EMAIL500_1: 이메일 전송 실패" + ) + }) + @PostMapping("/email-verifications") + public DefaultResponse sendVerificationCodeToEmail(@Valid @RequestBody EmailRequestDTO.Send request) { + emailCommandService.sendEmail(request); + return DefaultResponse.noContent(); + } + + @Operation(summary = "이메일 인증 번호 확인 API by 요시" ,description = "이메일 인증 번호 확인 API") + @ApiResponses({ + @ApiResponse(responseCode = "204", description = "이메일 인증 성공, 성공 시 1시간 동안 유효"), + @ApiResponse( + responseCode = "401", + description = "EMAIL401_1: 이메일 인증 실패" + ) + }) + @PostMapping("/check-email-verifications") + public DefaultResponse checkVerificationCode(@Valid @RequestBody EmailRequestDTO.Check request) { + emailCommandService.checkEmail(request); + return DefaultResponse.noContent(); + } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/EmailRequestDTO.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/EmailRequestDTO.java new file mode 100644 index 0000000..9d69684 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/dto/request/EmailRequestDTO.java @@ -0,0 +1,20 @@ +package org.withtime.be.withtimebe.domain.auth.dto.request; + + +import jakarta.validation.constraints.Email; + +public record EmailRequestDTO() { + + public record Send( + @Email + String email + ) { + } + + public record Check( + @Email + String email, + String code + ) { + } +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandService.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandService.java new file mode 100644 index 0000000..f8e5b10 --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandService.java @@ -0,0 +1,8 @@ +package org.withtime.be.withtimebe.domain.auth.service.command; + +import org.withtime.be.withtimebe.domain.auth.dto.request.EmailRequestDTO; + +public interface EmailCommandService { + void sendEmail(EmailRequestDTO.Send request); + void checkEmail(EmailRequestDTO.Check request); +} diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java new file mode 100644 index 0000000..8b02cdc --- /dev/null +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java @@ -0,0 +1,42 @@ +package org.withtime.be.withtimebe.domain.auth.service.command; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.withtime.be.withtimebe.domain.auth.dto.request.EmailRequestDTO; +import org.withtime.be.withtimebe.domain.auth.generator.RandomGenerator; +import org.withtime.be.withtimebe.domain.auth.service.query.EmailVerificationCodeStorageQueryService; +import org.withtime.be.withtimebe.domain.auth.util.MailVerificationCodeSender; +import org.withtime.be.withtimebe.global.error.code.EmailErrorCode; +import org.withtime.be.withtimebe.global.error.exception.EmailException; + +@Service +@RequiredArgsConstructor +public class EmailCommandServiceImpl implements EmailCommandService { + + private final RandomGenerator randomSixDigitGenerator; + private final MailVerificationCodeSender mailVerificationCodeSender; + private final EmailVerificationCodeStorageCommandService emailVerificationCodeStorageCommandService; + private final EmailVerificationCodeStorageQueryService emailVerificationCodeStorageQueryService; + + @Override + public void sendEmail(EmailRequestDTO.Send request) { + String email = request.email(); + String code = String.valueOf(randomSixDigitGenerator.generateRandom()); + + mailVerificationCodeSender.sendMail(email, code); + + emailVerificationCodeStorageCommandService.saveVerificationCode(email, code); + } + + @Override + public void checkEmail(EmailRequestDTO.Check request) { + String email = request.email(); + if (emailVerificationCodeStorageQueryService.checkVerificationCode(email, request.code())) { + emailVerificationCodeStorageCommandService.saveVerifiedEmail(email); + } + else { + throw new EmailException(EmailErrorCode.INCORRECT_EMAIL_VERIFICATION_CODE); + } + emailVerificationCodeStorageCommandService.deleteVerificationCode(email); + } +} From 490c97d853983fe743db632b038824662af22770 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 11 Jul 2025 22:07:24 +0900 Subject: [PATCH 7/8] =?UTF-8?q?:bug:=20Generator=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EA=B0=92=20=ED=83=80=EC=9E=85,=20yml=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=8B=A8=20Transacti?= =?UTF-8?q?onal=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/generator/RandomSixDigitGenerator.java | 6 +++--- .../domain/auth/service/command/AuthCommandServiceImpl.java | 2 ++ .../auth/service/command/EmailCommandServiceImpl.java | 6 ++++-- src/main/resources/application-develop.yml | 2 +- src/main/resources/application.yml | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomSixDigitGenerator.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomSixDigitGenerator.java index 9d6e88b..cb780a4 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomSixDigitGenerator.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/generator/RandomSixDigitGenerator.java @@ -6,12 +6,12 @@ import java.util.Random; @Component -public class RandomSixDigitGenerator implements RandomGenerator { +public class RandomSixDigitGenerator implements RandomGenerator { private static final Random RANDOM = new SecureRandom(); @Override - public Integer generateRandom() { - return RANDOM.nextInt(1000000); + public String generateRandom() { + return String.format("%06d", RANDOM.nextInt(1000000)); } } diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java index 213b4f0..48d7d10 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/AuthCommandServiceImpl.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.withtime.be.withtimebe.domain.auth.converter.AuthConverter; import org.withtime.be.withtimebe.domain.auth.dto.request.AuthRequestDTO; import org.withtime.be.withtimebe.domain.auth.service.query.EmailVerificationCodeStorageQueryService; @@ -26,6 +27,7 @@ @Service @RequiredArgsConstructor +@Transactional public class AuthCommandServiceImpl implements AuthCommandService { private final PasswordEncoder passwordEncoder; diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java index 8b02cdc..bafd06a 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java @@ -2,6 +2,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.withtime.be.withtimebe.domain.auth.dto.request.EmailRequestDTO; import org.withtime.be.withtimebe.domain.auth.generator.RandomGenerator; import org.withtime.be.withtimebe.domain.auth.service.query.EmailVerificationCodeStorageQueryService; @@ -11,9 +12,10 @@ @Service @RequiredArgsConstructor +@Transactional public class EmailCommandServiceImpl implements EmailCommandService { - private final RandomGenerator randomSixDigitGenerator; + private final RandomGenerator randomSixDigitGenerator; private final MailVerificationCodeSender mailVerificationCodeSender; private final EmailVerificationCodeStorageCommandService emailVerificationCodeStorageCommandService; private final EmailVerificationCodeStorageQueryService emailVerificationCodeStorageQueryService; @@ -21,7 +23,7 @@ public class EmailCommandServiceImpl implements EmailCommandService { @Override public void sendEmail(EmailRequestDTO.Send request) { String email = request.email(); - String code = String.valueOf(randomSixDigitGenerator.generateRandom()); + String code = randomSixDigitGenerator.generateRandom(); mailVerificationCodeSender.sendMail(email, code); diff --git a/src/main/resources/application-develop.yml b/src/main/resources/application-develop.yml index 8bc022a..353c6f3 100644 --- a/src/main/resources/application-develop.yml +++ b/src/main/resources/application-develop.yml @@ -16,7 +16,7 @@ spring: host: ${REDIS_HOST} port: 6379 mail: - host: smtp.gmail.com + host: ${MAIL_SENDER_HOST} port: 587 username: ${MAIL_SENDER_USERNAME} password: ${MAIL_SENDER_PASSWORD} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 73e6586..f630364 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,7 +16,7 @@ spring: host: ${REDIS_HOST} port: 6379 mail: - host: smtp.gmail.com + host: ${MAIL_SENDER_HOST} port: 587 username: ${MAIL_SENDER_USERNAME} password: ${MAIL_SENDER_PASSWORD} From 3d5c3b8b02885b70d0fea7f1f9d7858e5eda76e8 Mon Sep 17 00:00:00 2001 From: Jeongmo Seo Date: Fri, 11 Jul 2025 22:08:14 +0900 Subject: [PATCH 8/8] =?UTF-8?q?:bug:=20fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=EC=9D=B4=20=EB=B3=B4=EB=82=B4=EC=A7=80=EA=B3=A0=20Redis?= =?UTF-8?q?=EC=97=90=20=EC=A0=80=EC=9E=A5=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EA=B0=80=20=EC=95=88=20=EC=83=9D?= =?UTF-8?q?=EA=B8=B0=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/command/EmailCommandServiceImpl.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java index bafd06a..a3ff95e 100644 --- a/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java +++ b/src/main/java/org/withtime/be/withtimebe/domain/auth/service/command/EmailCommandServiceImpl.java @@ -25,9 +25,14 @@ public void sendEmail(EmailRequestDTO.Send request) { String email = request.email(); String code = randomSixDigitGenerator.generateRandom(); - mailVerificationCodeSender.sendMail(email, code); - emailVerificationCodeStorageCommandService.saveVerificationCode(email, code); + try { + mailVerificationCodeSender.sendMail(email, code); + } catch (Exception e) { + emailVerificationCodeStorageCommandService.deleteVerificationCode(email); + throw e; + } + } @Override