diff --git a/src/main/java/ceos/ipx/domain/auth/dto/EmailVerificationPurpose.java b/src/main/java/ceos/ipx/domain/auth/dto/EmailVerificationPurpose.java index d9ca76c..a767e63 100644 --- a/src/main/java/ceos/ipx/domain/auth/dto/EmailVerificationPurpose.java +++ b/src/main/java/ceos/ipx/domain/auth/dto/EmailVerificationPurpose.java @@ -20,8 +20,15 @@ public enum EmailVerificationPurpose { @JsonCreator public static EmailVerificationPurpose from(String value) { + if (value == null) { + throw new BusinessException(ErrorCode.INVALID_INPUT_VALUE); + } + return Arrays.stream(values()) - .filter(purpose -> purpose.value.equals(value)) + .filter(purpose -> + purpose.value.equalsIgnoreCase(value) + || purpose.name().equalsIgnoreCase(value) + ) .findFirst() .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_INPUT_VALUE)); } diff --git a/src/main/java/ceos/ipx/domain/auth/service/EmailSenderService.java b/src/main/java/ceos/ipx/domain/auth/service/EmailSenderService.java index 5aba240..ed15421 100644 --- a/src/main/java/ceos/ipx/domain/auth/service/EmailSenderService.java +++ b/src/main/java/ceos/ipx/domain/auth/service/EmailSenderService.java @@ -1,5 +1,6 @@ package ceos.ipx.domain.auth.service; +import ceos.ipx.domain.auth.dto.EmailVerificationPurpose; import ceos.ipx.global.exception.BusinessException; import ceos.ipx.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; @@ -8,6 +9,7 @@ import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; +import org.springframework.mail.javamail.JavaMailSender; @Slf4j @Service @@ -16,23 +18,44 @@ public class EmailSenderService { private final JavaMailSender javaMailSender; - public void sendVerificationCode(String toEmail, String code) { + public void sendVerificationCode(String toEmail, String code, EmailVerificationPurpose purpose) { try { SimpleMailMessage message = new SimpleMailMessage(); message.setTo(toEmail); - message.setSubject("[IPX] 이메일 인증 코드 안내"); - message.setText(createVerificationCodeMessage(code)); + message.setSubject(createSubject(purpose)); + message.setText(createVerificationCodeMessage(code, purpose)); javaMailSender.send(message); - log.info("이메일 인증 코드 발송 완료. email={}", toEmail); + log.info("이메일 인증 코드 발송 완료. email={}, purpose={}", toEmail, purpose); } catch (MailException e) { - log.error("이메일 인증 코드 발송 실패. email={}", toEmail, e); + log.error("이메일 인증 코드 발송 실패. email={}, purpose={}", toEmail, purpose, e); throw new BusinessException(ErrorCode.EMAIL_SEND_FAILED); } } - private String createVerificationCodeMessage(String code) { + private String createSubject(EmailVerificationPurpose purpose) { + if (purpose == EmailVerificationPurpose.PASSWORD_RESET) { + return "[IPX] 비밀번호 재설정 인증 코드 안내"; + } + + return "[IPX] 이메일 인증 코드 안내"; + } + + private String createVerificationCodeMessage(String code, EmailVerificationPurpose purpose) { + if (purpose == EmailVerificationPurpose.PASSWORD_RESET) { + return """ + 안녕하세요. IPX입니다. + + 비밀번호 재설정을 위한 인증 코드는 아래와 같습니다. + + 인증 코드: %s + + 인증 코드는 3분 동안 유효합니다. + 본인이 비밀번호 재설정을 요청하지 않았다면 이 메일을 무시해주세요. + """.formatted(code); + } + return """ 안녕하세요. IPX입니다. diff --git a/src/main/java/ceos/ipx/domain/auth/service/EmailVerificationSendService.java b/src/main/java/ceos/ipx/domain/auth/service/EmailVerificationSendService.java index b30072a..ad932f0 100644 --- a/src/main/java/ceos/ipx/domain/auth/service/EmailVerificationSendService.java +++ b/src/main/java/ceos/ipx/domain/auth/service/EmailVerificationSendService.java @@ -3,6 +3,8 @@ import ceos.ipx.domain.auth.dto.EmailVerificationPurpose; import ceos.ipx.domain.auth.dto.EmailVerificationSendRequest; import ceos.ipx.domain.auth.dto.EmailVerificationSendResponse; +import ceos.ipx.domain.user.entity.User; +import ceos.ipx.domain.user.entity.UserProvider; import ceos.ipx.domain.user.repository.UserRepository; import ceos.ipx.global.exception.BusinessException; import ceos.ipx.global.exception.ErrorCode; @@ -21,13 +23,17 @@ public EmailVerificationSendResponse sendEmailVerificationCode(EmailVerification String email = request.getEmail(); EmailVerificationPurpose purpose = request.getPurpose(); - if (purpose == EmailVerificationPurpose.SIGNUP && userRepository.existsByEmail(email)) { - throw new BusinessException(ErrorCode.EMAIL_ALREADY_EXISTS); + if (purpose == EmailVerificationPurpose.SIGNUP) { + validateSignupEmail(email); + } + + if (purpose == EmailVerificationPurpose.PASSWORD_RESET) { + validatePasswordResetEmail(email); } String verificationCode = emailVerificationService.createAndSaveVerificationCode(purpose, email); - emailSenderService.sendVerificationCode(email, verificationCode); + emailSenderService.sendVerificationCode(email, verificationCode, purpose); return EmailVerificationSendResponse.builder() .email(email) @@ -36,4 +42,19 @@ public EmailVerificationSendResponse sendEmailVerificationCode(EmailVerification .resendAvailableIn(emailVerificationService.getResendAvailableInSeconds()) .build(); } + + private void validateSignupEmail(String email) { + if (userRepository.existsByEmail(email)) { + throw new BusinessException(ErrorCode.EMAIL_ALREADY_EXISTS); + } + } + + private void validatePasswordResetEmail(String email) { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + if (user.getProvider() != UserProvider.LOCAL) { + throw new BusinessException(ErrorCode.SOCIAL_ACCOUNT_PASSWORD_RESET_NOT_ALLOWED); + } + } } \ No newline at end of file diff --git a/src/main/java/ceos/ipx/domain/user/dto/TermsAgreementRequest.java b/src/main/java/ceos/ipx/domain/user/dto/TermsAgreementRequest.java index c0fa726..4261cf6 100644 --- a/src/main/java/ceos/ipx/domain/user/dto/TermsAgreementRequest.java +++ b/src/main/java/ceos/ipx/domain/user/dto/TermsAgreementRequest.java @@ -6,12 +6,12 @@ @Schema(description = "Terms agreement item") public record TermsAgreementRequest( - @Schema(description = "Terms type", example = "SERVICE") - @NotBlank(message = "Terms type is required.") + @Schema(description = "약관 타입", example = "SERVICE") + @NotBlank(message = "약관 타입은 필수입니다.") String type, - @Schema(description = "Whether the user agreed to the terms", example = "true") - @NotNull(message = "Terms agreement status is required.") + @Schema(description = "약관 동의 여부", example = "true") + @NotNull(message = "약관 동의 여부는 필수입니다.") Boolean agreed ) { -} +} \ No newline at end of file diff --git a/src/main/java/ceos/ipx/global/exception/ErrorCode.java b/src/main/java/ceos/ipx/global/exception/ErrorCode.java index b4cc7e2..b53ff78 100644 --- a/src/main/java/ceos/ipx/global/exception/ErrorCode.java +++ b/src/main/java/ceos/ipx/global/exception/ErrorCode.java @@ -22,6 +22,7 @@ public enum ErrorCode { EMAIL_SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "AU009", "이메일 발송에 실패했습니다."), EMAIL_VERIFICATION_CODE_EXPIRED(HttpStatus.GONE, "AU010", "인증 코드가 만료되었거나 존재하지 않습니다."), EMAIL_VERIFICATION_CODE_MISMATCH(HttpStatus.BAD_REQUEST, "AU011", "인증 코드가 일치하지 않습니다."), + SOCIAL_ACCOUNT_PASSWORD_RESET_NOT_ALLOWED(HttpStatus.FORBIDDEN, "AU012", "소셜 로그인 계정은 비밀번호를 재설정할 수 없습니다."), UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "SC001", "인증이 필요합니다."), ACCESS_DENIED(HttpStatus.FORBIDDEN, "SC002", "해당 요청에 권한이 없습니다."); diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 5a25b5c..0d32ae7 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -16,6 +16,7 @@ spring: redis: host: localhost port: 6379 + password: ${REDIS_PASSWORD:} mail: host: smtp.gmail.com