diff --git a/build.gradle b/build.gradle index 27e8fbe..82cbca7 100644 --- a/build.gradle +++ b/build.gradle @@ -28,10 +28,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.projectlombok:lombok' + //email 인증 + implementation 'org.springframework.boot:spring-boot-starter-mail' + // database implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + runtimeOnly 'com.mysql:mysql-connector-j' runtimeOnly 'com.h2database:h2' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/icurriculum/IcurriculumApplication.java b/src/main/java/icurriculum/IcurriculumApplication.java index 974c70c..af90a09 100644 --- a/src/main/java/icurriculum/IcurriculumApplication.java +++ b/src/main/java/icurriculum/IcurriculumApplication.java @@ -2,8 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @SpringBootApplication +@EnableRedisRepositories +@EnableJpaAuditing public class IcurriculumApplication { public static void main(String[] args) { diff --git a/src/main/java/icurriculum/global/response/status/ErrorStatus.java b/src/main/java/icurriculum/global/response/status/ErrorStatus.java index be40867..d4d25c8 100644 --- a/src/main/java/icurriculum/global/response/status/ErrorStatus.java +++ b/src/main/java/icurriculum/global/response/status/ErrorStatus.java @@ -13,6 +13,11 @@ public enum ErrorStatus { UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "로그인 인증이 필요합니다."), FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + /* + * mail + */ + MAIL_NOT_FOUND(HttpStatus.BAD_REQUEST, "MAIL400","잘못된 메일입니다."), + MAIL_NOT_SEND(HttpStatus.BAD_REQUEST, "MAIL401", "메일 전송에 실패했습니다."), /* * take */ diff --git a/src/main/java/icurriculum/global/verification/controller/MailController.java b/src/main/java/icurriculum/global/verification/controller/MailController.java new file mode 100644 index 0000000..c3f83cb --- /dev/null +++ b/src/main/java/icurriculum/global/verification/controller/MailController.java @@ -0,0 +1,39 @@ +package icurriculum.global.verification.controller; + + +import icurriculum.global.response.ApiResponse; +import icurriculum.global.response.status.ErrorStatus; +import icurriculum.global.verification.dto.request.MailRequestDTO; +import icurriculum.global.verification.dto.response.MailResponseDTO; +import icurriculum.global.verification.service.EmailService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static icurriculum.global.verification.dto.response.MailResponseDTO.*; + +@RestController +@RequestMapping("/mail") +@RequiredArgsConstructor +public class MailController { + + private final EmailService emailService; + + @PostMapping("/send") + public ApiResponse mailSend(@RequestBody MailRequestDTO.MailSend request) { + MailSend mailSend = emailService.integratedProcee(request.getEmail()); + if (!mailSend.getStatus()) { + return ApiResponse.onFailure(ErrorStatus.MAIL_NOT_SEND, mailSend); + } + return ApiResponse.onSuccess(mailSend); + } + + @PostMapping("/verify") + public ApiResponse mailVerify(@RequestBody @Valid MailRequestDTO.MailVerify request) { + MailVerify mailVerify = emailService.verifyCode(request.getEmail(), request.getCode()); + return ApiResponse.onSuccess(mailVerify); + } +} diff --git a/src/main/java/icurriculum/global/verification/converter/EmailConverter.java b/src/main/java/icurriculum/global/verification/converter/EmailConverter.java new file mode 100644 index 0000000..4dc6213 --- /dev/null +++ b/src/main/java/icurriculum/global/verification/converter/EmailConverter.java @@ -0,0 +1,27 @@ +package icurriculum.global.verification.converter; + +import icurriculum.global.verification.dto.response.MailResponseDTO; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +import static icurriculum.global.verification.dto.response.MailResponseDTO.*; + +@Component +public class EmailConverter { + + public MailSend toMailSendResponse(String comment, Boolean status) { + return MailSend.builder() + .responseComment(comment) + .status(status) + .timeStamp(LocalDateTime.now()) + .build(); + } + + public MailVerify toMailVerifyResponse(Boolean check, String email) { + return MailVerify.builder() + .check(check) + .email(email) + .build(); + } +} diff --git a/src/main/java/icurriculum/global/verification/dto/request/MailRequestDTO.java b/src/main/java/icurriculum/global/verification/dto/request/MailRequestDTO.java new file mode 100644 index 0000000..ccd8975 --- /dev/null +++ b/src/main/java/icurriculum/global/verification/dto/request/MailRequestDTO.java @@ -0,0 +1,33 @@ +package icurriculum.global.verification.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public abstract class MailRequestDTO { + + @Builder + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class MailSend { + @Email + @NotBlank + private String email; + } + + @Builder + @Getter + @AllArgsConstructor + public static class MailVerify { + @Email + @NotBlank + private String email; + @NotBlank + private String code; + } + +} diff --git a/src/main/java/icurriculum/global/verification/dto/response/MailResponseDTO.java b/src/main/java/icurriculum/global/verification/dto/response/MailResponseDTO.java new file mode 100644 index 0000000..a5a5763 --- /dev/null +++ b/src/main/java/icurriculum/global/verification/dto/response/MailResponseDTO.java @@ -0,0 +1,28 @@ +package icurriculum.global.verification.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; + +public abstract class MailResponseDTO { + + @Builder + @Getter + @AllArgsConstructor + public static class MailSend{ + private String responseComment; + private Boolean status; + private LocalDateTime timeStamp; + } + + + @Builder + @Getter + @AllArgsConstructor + public static class MailVerify{ + private Boolean check; + private String email; + } +} diff --git a/src/main/java/icurriculum/global/verification/entity/EmailVerification.java b/src/main/java/icurriculum/global/verification/entity/EmailVerification.java new file mode 100644 index 0000000..9c75573 --- /dev/null +++ b/src/main/java/icurriculum/global/verification/entity/EmailVerification.java @@ -0,0 +1,22 @@ +package icurriculum.global.verification.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import java.io.Serializable; + + +@RedisHash(value = "Email", timeToLive = 600) //Redis 저장 엔티티, 제한시간 10분 +@AllArgsConstructor +@Getter +@Setter +public class EmailVerification implements Serializable { + @Id + private String email; + private String verificationCode; +} + diff --git a/src/main/java/icurriculum/global/verification/repository/EmailVerificationRepository.java b/src/main/java/icurriculum/global/verification/repository/EmailVerificationRepository.java new file mode 100644 index 0000000..28cad7a --- /dev/null +++ b/src/main/java/icurriculum/global/verification/repository/EmailVerificationRepository.java @@ -0,0 +1,9 @@ +package icurriculum.global.verification.repository; + +import icurriculum.global.verification.entity.EmailVerification; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface EmailVerificationRepository extends CrudRepository { +} diff --git a/src/main/java/icurriculum/global/verification/service/EmailService.java b/src/main/java/icurriculum/global/verification/service/EmailService.java new file mode 100644 index 0000000..6e39ead --- /dev/null +++ b/src/main/java/icurriculum/global/verification/service/EmailService.java @@ -0,0 +1,100 @@ +package icurriculum.global.verification.service; + +import icurriculum.global.verification.converter.EmailConverter; +import icurriculum.global.verification.dto.response.MailResponseDTO; +import icurriculum.global.verification.entity.EmailVerification; +import icurriculum.global.verification.repository.EmailVerificationRepository; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +import static icurriculum.global.verification.dto.response.MailResponseDTO.*; + +@Service +@RequiredArgsConstructor +public class EmailService { + + private final JavaMailSender mailSender; + private final EmailConverter emailConverter; + private final EmailVerificationRepository emailVerificationRepository; + private final RedisTemplate redisTemplate; + + private final String SUBJECT = "[ICURRICULUM] 인하대학교 학생 인증 메일입니다."; + + public String makeVerificationCode() { + Random random = new Random(); + return String.format("%06d", random.nextInt(999999)); + } + public String getRedisKey(String email) { + return "Email:" + email; + } + + @Transactional + public MailSend sendVerificationEmail(String email, String code) { + try { + EmailVerification verification = new EmailVerification(email, code); + + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper messageHelper = new MimeMessageHelper(message, true); + + messageHelper.setTo(email); + messageHelper.setSubject(SUBJECT); + String htmlContent = getVerificationMessage(code); + messageHelper.setText(htmlContent, true); + + if (emailVerificationRepository.existsById(getRedisKey(email))) { //redis에 email이 있으면 삭제 + emailVerificationRepository.deleteById(getRedisKey(email)); + } + emailVerificationRepository.save(verification); //Redis에 인증 코드 저장 + mailSender.send(message); + + } catch (MessagingException e) { + e.printStackTrace(); + return emailConverter.toMailSendResponse("메일 전송에 실패했습니다.", false); + } + return emailConverter.toMailSendResponse("메일 전송에 성공했습니다.", true); + } + + @Transactional + public MailSend integratedProcee(String email) { + String code = makeVerificationCode(); + return sendVerificationEmail(email, code); + } + + public String getVerificationMessage(String verificationCode) { + StringBuffer verificationMessage = new StringBuffer(); + verificationMessage + .append("

[ICURRICULUM] 인증메일

"); + verificationMessage + .append("

인증코드 : ") + .append(verificationCode) + .append("

"); + return verificationMessage.toString(); + } + + public MailVerify verifyCode(String email, String code) { + Optional verificationObject = emailVerificationRepository.findById(email); + boolean check = false; + if (verificationObject.isPresent()) { + EmailVerification verification = verificationObject.get(); + if (verification.getVerificationCode().equals(code)) { + emailVerificationRepository.deleteById(email); + check = true; + } + } + return emailConverter.toMailVerifyResponse(check, email); + } + + public Long getTTL(String key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } +}