From 875ae4411fc8af90b3b47c0f1195345217ac69ad Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Wed, 18 Feb 2026 15:09:00 +0900 Subject: [PATCH 1/2] =?UTF-8?q?member:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EC=9E=AC=EB=B0=B0=EC=B9=98=20=E2=80=94=20MemberService?= =?UTF-8?q?=EC=97=90=EC=84=9C=20jwtUtil,=20frontendUrl=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EmailVerificationTokenProvider 신설: 토큰 생성/검증 책임 분리 - EmailVerificationClaim 레코드 신설: 토큰 파싱 결과 - EmailChangeMailBuilder: 토큰 생성 + URL 조합 로직 흡수 - MemberService: 비즈니스 검증 + 오케스트레이션만 수행 --- .../ryu/studyhelper/member/MemberService.java | 45 ++++++------------- .../member/mail/EmailChangeMailBuilder.java | 14 +++++- .../member/mail/EmailVerificationClaim.java | 10 +++++ .../mail/EmailVerificationTokenProvider.java | 35 +++++++++++++++ 4 files changed, 70 insertions(+), 34 deletions(-) create mode 100644 src/main/java/com/ryu/studyhelper/member/mail/EmailVerificationClaim.java create mode 100644 src/main/java/com/ryu/studyhelper/member/mail/EmailVerificationTokenProvider.java diff --git a/src/main/java/com/ryu/studyhelper/member/MemberService.java b/src/main/java/com/ryu/studyhelper/member/MemberService.java index 3979e7c..8faf1f6 100644 --- a/src/main/java/com/ryu/studyhelper/member/MemberService.java +++ b/src/main/java/com/ryu/studyhelper/member/MemberService.java @@ -2,12 +2,13 @@ import com.ryu.studyhelper.common.enums.CustomResponseStatus; import com.ryu.studyhelper.common.exception.CustomException; -import com.ryu.studyhelper.config.security.jwt.JwtUtil; import com.ryu.studyhelper.infrastructure.discord.DiscordMessage; import com.ryu.studyhelper.infrastructure.discord.DiscordNotifier; import com.ryu.studyhelper.infrastructure.solvedac.SolvedAcClient; import com.ryu.studyhelper.infrastructure.mail.sender.MailSender; import com.ryu.studyhelper.member.mail.EmailChangeMailBuilder; +import com.ryu.studyhelper.member.mail.EmailVerificationClaim; +import com.ryu.studyhelper.member.mail.EmailVerificationTokenProvider; import com.ryu.studyhelper.member.domain.Member; import com.ryu.studyhelper.member.dto.response.MemberSearchResponse; import com.ryu.studyhelper.member.dto.response.MyProfileResponse; @@ -16,7 +17,6 @@ import com.ryu.studyhelper.team.repository.TeamMemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,15 +34,12 @@ public class MemberService { private final TeamMemberRepository teamMemberRepository; private final SolvedAcClient solvedAcClient; private final SolveService solveService; - private final JwtUtil jwtUtil; private final MailSender mailSender; private final EmailChangeMailBuilder emailChangeMailBuilder; + private final EmailVerificationTokenProvider emailVerificationTokenProvider; private final DiscordNotifier discordNotifier; private final Clock clock; - @Value("${FRONTEND_URL:http://localhost:5173}") - private String frontendUrl; - @Transactional(readOnly = true) public Member getById(Long id) { return memberRepository.findById(id) @@ -124,14 +121,8 @@ public void sendEmailVerification(Long memberId, String newEmail) { // 2. 회원 존재 확인 getById(memberId); - // 3. 이메일 인증 토큰 생성 (5분 만료) - String token = jwtUtil.createEmailVerificationToken(memberId, newEmail); - - // 4. 인증 URL 생성 - String verificationUrl = frontendUrl + "/verify-email?token=" + token; - - // 5. 이메일 발송 - mailSender.send(emailChangeMailBuilder.build(newEmail, verificationUrl)); + // 3. 인증 메일 생성 및 발송 + mailSender.send(emailChangeMailBuilder.build(memberId, newEmail)); } /** @@ -140,29 +131,19 @@ public void sendEmailVerification(Long memberId, String newEmail) { * @return 변경된 이메일 주소 */ public String verifyAndChangeEmail(String token) { - // 1. 토큰 유효성 검증 (만료, 잘못된 서명 등) - jwtUtil.validateTokenOrThrow(token); - - // 2. 토큰 타입 확인 - String tokenType = jwtUtil.getTokenType(token); - if (!JwtUtil.TOKEN_TYPE_EMAIL_VERIFICATION.equals(tokenType)) { - throw new CustomException(CustomResponseStatus.INVALID_EMAIL_VERIFICATION_TOKEN); - } - - // 3. 토큰에서 정보 추출 - Long memberId = jwtUtil.getIdFromToken(token); - String newEmail = jwtUtil.getEmailFromToken(token); + // 1. 토큰 검증 및 클레임 추출 + EmailVerificationClaim claim = emailVerificationTokenProvider.parseToken(token); - // 4. 이메일 중복 재확인 (인증 메일 발송 후 다른 사용자가 해당 이메일로 가입했을 수 있음) - if (!isEmailAvailable(newEmail)) { + // 2. 이메일 중복 재확인 (인증 메일 발송 후 다른 사용자가 해당 이메일로 가입했을 수 있음) + if (!isEmailAvailable(claim.newEmail())) { throw new CustomException(CustomResponseStatus.EMAIL_ALREADY_EXISTS); } - // 5. 회원 정보 업데이트 - Member member = getById(memberId); - member.changeEmail(newEmail); + // 3. 회원 정보 업데이트 + Member member = getById(claim.memberId()); + member.changeEmail(claim.newEmail()); - return newEmail; + return claim.newEmail(); } /** diff --git a/src/main/java/com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java b/src/main/java/com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java index da6c0e1..5f29153 100644 --- a/src/main/java/com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java +++ b/src/main/java/com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java @@ -2,23 +2,33 @@ import com.ryu.studyhelper.infrastructure.mail.sender.MailMessage; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; /** - * 이메일 변경 인증 메일 빌더 + * 이메일 변경 인증 메일 생성 */ @Component @RequiredArgsConstructor public class EmailChangeMailBuilder { private final TemplateEngine templateEngine; + private final EmailVerificationTokenProvider tokenProvider; + + @Value("${FRONTEND_URL:http://localhost:5173}") + private String frontendUrl; /** * 이메일 변경 인증 메일 생성 + * @param memberId 회원 ID + * @param newEmail 변경할 이메일 */ - public MailMessage build(String newEmail, String verificationUrl) { + public MailMessage build(Long memberId, String newEmail) { + String token = tokenProvider.createToken(memberId, newEmail); + String verificationUrl = frontendUrl + "/verify-email?token=" + token; + String subject = "[CodeMate] 이메일 변경 인증"; Context context = new Context(); diff --git a/src/main/java/com/ryu/studyhelper/member/mail/EmailVerificationClaim.java b/src/main/java/com/ryu/studyhelper/member/mail/EmailVerificationClaim.java new file mode 100644 index 0000000..3d24481 --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/member/mail/EmailVerificationClaim.java @@ -0,0 +1,10 @@ +package com.ryu.studyhelper.member.mail; + +/** + * 이메일 인증 토큰에서 추출한 클레임 + */ +public record EmailVerificationClaim( + Long memberId, + String newEmail +) { +} \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/member/mail/EmailVerificationTokenProvider.java b/src/main/java/com/ryu/studyhelper/member/mail/EmailVerificationTokenProvider.java new file mode 100644 index 0000000..a74f407 --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/member/mail/EmailVerificationTokenProvider.java @@ -0,0 +1,35 @@ +package com.ryu.studyhelper.member.mail; + +import com.ryu.studyhelper.common.enums.CustomResponseStatus; +import com.ryu.studyhelper.common.exception.CustomException; +import com.ryu.studyhelper.config.security.jwt.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 이메일 인증 토큰 생성 및 검증 + */ +@Component +@RequiredArgsConstructor +public class EmailVerificationTokenProvider { + + private final JwtUtil jwtUtil; + + public String createToken(Long memberId, String email) { + return jwtUtil.createEmailVerificationToken(memberId, email); + } + + public EmailVerificationClaim parseToken(String token) { + jwtUtil.validateTokenOrThrow(token); + + String tokenType = jwtUtil.getTokenType(token); + if (!JwtUtil.TOKEN_TYPE_EMAIL_VERIFICATION.equals(tokenType)) { + throw new CustomException(CustomResponseStatus.INVALID_EMAIL_VERIFICATION_TOKEN); + } + + Long memberId = jwtUtil.getIdFromToken(token); + String newEmail = jwtUtil.getEmailFromToken(token); + + return new EmailVerificationClaim(memberId, newEmail); + } +} \ No newline at end of file From 02ecf4f434311ea5f3ce92456e222447b9f4f19b Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Wed, 18 Feb 2026 15:20:46 +0900 Subject: [PATCH 2/2] =?UTF-8?q?member:=20frontendUrl=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=EA=B0=92=EC=9D=84=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98=20URL?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java b/src/main/java/com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java index 5f29153..eba078e 100644 --- a/src/main/java/com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java +++ b/src/main/java/com/ryu/studyhelper/member/mail/EmailChangeMailBuilder.java @@ -17,7 +17,7 @@ public class EmailChangeMailBuilder { private final TemplateEngine templateEngine; private final EmailVerificationTokenProvider tokenProvider; - @Value("${FRONTEND_URL:http://localhost:5173}") + @Value("${FRONTEND_URL:https://codemate.kr}") private String frontendUrl; /**