diff --git a/src/main/java/dmu/dasom/api/ApiApplication.java b/src/main/java/dmu/dasom/api/ApiApplication.java index 721665e..ffc869d 100644 --- a/src/main/java/dmu/dasom/api/ApiApplication.java +++ b/src/main/java/dmu/dasom/api/ApiApplication.java @@ -3,11 +3,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; @EnableJpaAuditing @SpringBootApplication +@EnableAsync public class ApiApplication { - public static void main(String[] args) { SpringApplication.run(ApiApplication.class, args); } diff --git a/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java b/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java index 547ea2b..94433cf 100644 --- a/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java @@ -132,11 +132,7 @@ public void sendEmailsToApplicants(MailType mailType) { } for (Applicant applicant : applicants) { - try { - emailService.sendEmail(applicant.getEmail(), applicant.getName(), mailType); - } catch (MessagingException e) { - System.err.println("Failed to send email to: " + applicant.getEmail()); - } + emailService.sendEmail(applicant.getEmail(), applicant.getName(), mailType); } } diff --git a/src/main/java/dmu/dasom/api/domain/google/entity/EmailLog.java b/src/main/java/dmu/dasom/api/domain/google/entity/EmailLog.java new file mode 100644 index 0000000..6cb13b9 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/google/entity/EmailLog.java @@ -0,0 +1,40 @@ +package dmu.dasom.api.domain.google.entity; + +import dmu.dasom.api.domain.google.enums.MailSendStatus; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class EmailLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String recipientEmail; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private MailSendStatus status; + + private String errorMessage; + + @CreationTimestamp + private LocalDateTime sentAt; + + public static EmailLog of(String recipientEmail, MailSendStatus status, String errorMessage) { + return EmailLog.builder() + .recipientEmail(recipientEmail) + .status(status) + .errorMessage(errorMessage) + .build(); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/google/enums/MailSendStatus.java b/src/main/java/dmu/dasom/api/domain/google/enums/MailSendStatus.java new file mode 100644 index 0000000..c52974d --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/google/enums/MailSendStatus.java @@ -0,0 +1,6 @@ +package dmu.dasom.api.domain.google.enums; + +public enum MailSendStatus { + SUCCESS, + FAILURE +} diff --git a/src/main/java/dmu/dasom/api/domain/google/enums/MailTemplate.java b/src/main/java/dmu/dasom/api/domain/google/enums/MailTemplate.java new file mode 100644 index 0000000..435c664 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/google/enums/MailTemplate.java @@ -0,0 +1,26 @@ +package dmu.dasom.api.domain.google.enums; + +import dmu.dasom.api.domain.common.exception.CustomException; +import dmu.dasom.api.domain.common.exception.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +@Getter +@RequiredArgsConstructor +public enum MailTemplate { + DOCUMENT_RESULT(MailType.DOCUMENT_RESULT, "동양미래대학교 컴퓨터소프트웨어공학과 전공 동아리 DASOM 서류 결과 안내", "document-result-email"), + FINAL_RESULT(MailType.FINAL_RESULT, "동양미래대학교 컴퓨터소프트웨어공학과 전공 동아리 DASOM 최종 면접 결과 안내", "final-result-email"); + + private final MailType mailType; + private final String subject; + private final String templateName; + + public static MailTemplate getMailType(MailType mailType) { + return Arrays.stream(values()) + .filter(template -> template.getMailType() == mailType) + .findFirst() + .orElseThrow(() -> new CustomException(ErrorCode.MAIL_TYPE_NOT_VALID)); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/google/repository/EmailLogRepository.java b/src/main/java/dmu/dasom/api/domain/google/repository/EmailLogRepository.java new file mode 100644 index 0000000..ab4d027 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/google/repository/EmailLogRepository.java @@ -0,0 +1,7 @@ +package dmu.dasom.api.domain.google.repository; + +import dmu.dasom.api.domain.google.entity.EmailLog; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EmailLogRepository extends JpaRepository { +} diff --git a/src/main/java/dmu/dasom/api/domain/google/service/EmailLogService.java b/src/main/java/dmu/dasom/api/domain/google/service/EmailLogService.java new file mode 100644 index 0000000..aa28313 --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/google/service/EmailLogService.java @@ -0,0 +1,21 @@ +package dmu.dasom.api.domain.google.service; + +import dmu.dasom.api.domain.google.entity.EmailLog; +import dmu.dasom.api.domain.google.enums.MailSendStatus; +import dmu.dasom.api.domain.google.repository.EmailLogRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EmailLogService { + + private final EmailLogRepository emailLogRepository; + + @Async + public void logEmailSending(String recipientEmail, MailSendStatus status, String errorMessage) { + EmailLog emailLog = EmailLog.of(recipientEmail, status, errorMessage); + emailLogRepository.save(emailLog); + } +} diff --git a/src/main/java/dmu/dasom/api/domain/google/service/EmailService.java b/src/main/java/dmu/dasom/api/domain/google/service/EmailService.java index 5bc0cb4..0b5e0c1 100644 --- a/src/main/java/dmu/dasom/api/domain/google/service/EmailService.java +++ b/src/main/java/dmu/dasom/api/domain/google/service/EmailService.java @@ -2,79 +2,78 @@ import dmu.dasom.api.domain.common.exception.CustomException; import dmu.dasom.api.domain.common.exception.ErrorCode; +import dmu.dasom.api.domain.google.enums.MailSendStatus; +import dmu.dasom.api.domain.google.enums.MailTemplate; import dmu.dasom.api.domain.google.enums.MailType; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; +@Slf4j @RequiredArgsConstructor @Service public class EmailService { private final TemplateEngine templateEngine; private final JavaMailSender javaMailSender; + private final EmailLogService emailLogService; @Value("${spring.mail.username}") private String from; - public void sendEmail(String to, String name, MailType mailType) throws MessagingException { - if (mailType == null){ - throw new CustomException(ErrorCode.MAIL_TYPE_NOT_VALID); - } + @Async + public void sendEmail(String to, String name, MailType mailType) { + MailSendStatus mailSendStatus = MailSendStatus.SUCCESS; + String errorMessage = null; + try { + if (mailType == null) { + throw new CustomException(ErrorCode.MAIL_TYPE_NOT_VALID); + } - // 메일 제목 및 템플릿 설정 - String subject; - String emailContent; - String buttonUrl = "https://dmu-dasom.or.kr/recruit/result"; - String buttonText; + // 메일 템플릿 조회 + MailTemplate mailTemplate = MailTemplate.getMailType(mailType); + String buttonUrl = "https://dmu-dasom.or.kr/recruit/result"; - switch (mailType) { - case DOCUMENT_RESULT -> { - subject = "동양미래대학교 컴퓨터소프트웨어공학과 전공 동아리 DASOM 서류 결과 안내"; - emailContent = "먼저 다솜 34기에 많은 관심을 두고 지원해 주셔서 감사드리며,
" + - "내부 서류 평가 결과 및 추후 일정에 관해 안내해드리고자 이메일을 발송하게 되었습니다.
" + - "서류 전형 결과는 아래 버튼 혹은 홈페이지를 통해 확인이 가능합니다."; - buttonText = "서류 결과 확인하기"; - } - case FINAL_RESULT -> { - subject = "동양미래대학교 컴퓨터소프트웨어공학과 전공 동아리 DASOM 최종 면접 결과 안내"; - emailContent = "먼저 다솜 34기에 많은 관심을 두고 지원해 주셔서 감사드리며,
" + - "최종 면접 결과 및 추후 일정에 관해 안내해드리고자 이메일을 발송하게 되었습니다.
" + - "최종 면접 결과는 아래 버튼 혹은 홈페이지를 통해 확인이 가능합니다."; - buttonText = "최종 결과 확인하기"; - } - default -> throw new IllegalStateException("Unexpected value: " + mailType); - } + // HTML 템플릿에 전달할 데이터 설정 + Context context = new Context(); + context.setVariable("name", name); // 지원자 이름 전달 + context.setVariable("buttonUrl", buttonUrl); // 버튼 링크 전달 - // HTML 템플릿에 전달할 데이터 설정 - Context context = new Context(); - context.setVariable("name", name); // 지원자 이름 전달 - context.setVariable("emailContent", emailContent); // 이메일 내용 전달 - context.setVariable("buttonUrl", buttonUrl); // 버튼 링크 전달 - context.setVariable("buttonText", buttonText); - // HTML 템플릿 처리 - String htmlBody = templateEngine.process("email-template", context); + // HTML 템플릿 처리 + String htmlBody = templateEngine.process(mailTemplate.getTemplateName(), context); - // 이메일 생성 및 전송 - MimeMessage message = javaMailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + // 이메일 생성 및 전송 + MimeMessage message = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); - helper.setTo(to); - helper.setSubject(subject); - helper.setText(htmlBody, true); - helper.setFrom((from != null && !from.isEmpty()) ? from : "dasomdmu@gmail.com"); + helper.setTo(to); + helper.setSubject(mailTemplate.getSubject()); + helper.setText(htmlBody, true); + helper.setFrom((from != null && !from.isEmpty()) ? from : "dasomdmu@gmail.com"); - // Content-Type을 명시적으로 설정 - message.setContent(htmlBody, "text/html; charset=utf-8"); + // Content-Type을 명시적으로 설정 + message.setContent(htmlBody, "text/html; charset=utf-8"); - javaMailSender.send(message); + javaMailSender.send(message); + log.info("Email sent successfull {}", to); + } catch (MessagingException e) { + log.error("Failed to send email to {}: {}", to, e.getMessage()); + mailSendStatus = MailSendStatus.FAILURE; + errorMessage = e.getMessage(); + } catch (CustomException e) { + log.error("Email sending error for {}: {}", to, e.getMessage()); + mailSendStatus = MailSendStatus.FAILURE; + errorMessage = e.getMessage(); + } + emailLogService.logEmailSending(to, mailSendStatus, errorMessage); } - } diff --git a/src/main/resources/templates/document-result-email.html b/src/main/resources/templates/document-result-email.html new file mode 100644 index 0000000..457f3d0 --- /dev/null +++ b/src/main/resources/templates/document-result-email.html @@ -0,0 +1,122 @@ + + + + + + + + 이메일 안내 + + +
+
+
+
+ +
+ 로고 +
+
+
+
+ +
+ DASOM +
+
+
+
+ 컴퓨터공학부 전공동아리 다솜입니다. +
+ +
+ 먼저 다솜 35기에 많은 관심을 두고 지원해 주셔서 감사드리며,
+ 내부 서류 평가 결과 및 추후 일정에 관해 안내해드리고자 이메일을 발송하게 되었습니다.
+ 서류 전형 결과는 아래 버튼 혹은 홈페이지를 통해 확인이 가능합니다. +
+ +
+ + 서류 결과 확인하기 + + + +
+ +
+ 또한, 문의 사항은 본 메일에 회신 또는 아래 번호로 편하게 연락 부탁드립니다.
+ 010-6361-3481 +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/email-template.html b/src/main/resources/templates/final-result-email.html similarity index 89% rename from src/main/resources/templates/email-template.html rename to src/main/resources/templates/final-result-email.html index 89465a3..98f6eeb 100644 --- a/src/main/resources/templates/email-template.html +++ b/src/main/resources/templates/final-result-email.html @@ -78,8 +78,10 @@ line-height: 2.5; text-align: right; margin-bottom: 40px; - color: white !important;" - th:utext="${emailContent}"> + color: white !important;"> + 먼저 다솜 35기에 많은 관심을 두고 지원해 주셔서 감사드리며,
+ 최종 면접 결과 및 추후 일정에 관해 안내해드리고자 이메일을 발송하게 되었습니다.
+ 최종 면접 결과는 아래 버튼 혹은 홈페이지를 통해 확인이 가능합니다.
- - + 최종 결과 확인하기 "; - String expectedTemplate = "email-template"; - String expectedHtmlBody = "Document Pass"; - when(templateEngine.process(eq(expectedTemplate), any(Context.class))).thenReturn(expectedHtmlBody); + when(templateEngine.process(eq(expectedTemplate.getTemplateName()), any(Context.class))).thenReturn(expectedHtmlBody); - // when + //when emailService.sendEmail(to, name, mailType); // then + // 비동기 처리를 위해 잠시 대기 후 검증 ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(MimeMessage.class); - verify(javaMailSender).send(messageCaptor.capture()); + verify(javaMailSender, timeout(1000)).send(messageCaptor.capture()); - MimeMessage sentMessage = messageCaptor.getValue(); - assertNotNull(sentMessage); - verify(templateEngine).process(eq(expectedTemplate), any(Context.class)); - } + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(Context.class); + verify(templateEngine).process(eq(expectedTemplate.getTemplateName()), contextCaptor.capture()); + Context capturedContext = contextCaptor.getValue(); - @Test - @DisplayName("최종 합격 메일 발송 테스트") - void sendEmail_finalResult() throws MessagingException { - // given - String to = "applicant@example.com"; - String name = "지원자"; - MailType mailType = MailType.FINAL_RESULT; + assertEquals(name, capturedContext.getVariable("name")); + assertEquals("https://dmu-dasom.or.kr/recruit/result", capturedContext.getVariable("buttonUrl")); - String expectedTemplate = "email-template"; - String expectedHtmlBody = "Final Pass"; - when(templateEngine.process(eq(expectedTemplate), any(Context.class))).thenReturn(expectedHtmlBody); - - // when - emailService.sendEmail(to, name, mailType); + MimeMessage capturedMessage = messageCaptor.getValue(); + verify(capturedMessage).setSubject(expectedTemplate.getSubject(), "UTF-8"); + } - // then - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(MimeMessage.class); - verify(javaMailSender).send(messageCaptor.capture()); + @Test + @DisplayName("성공 - 서류 결과 메일 발송 테스트") + void sendDocumentResultMessage_Success() throws Exception { + testSendEmailSuccess(MailType.DOCUMENT_RESULT); + } - MimeMessage sentMessage = messageCaptor.getValue(); - assertNotNull(sentMessage); - verify(templateEngine).process(eq(expectedTemplate), any(Context.class)); + @Test + @DisplayName("성공 - 최종 결과 메일 발송 테스트") + void sendFinalResultMessage_Success() throws Exception { + testSendEmailSuccess(MailType.FINAL_RESULT); } @Test - @DisplayName("잘못된 MailType 처리 테스트") - void sendEmail_invalidMailType() { - // given + @DisplayName("실패 - MailType이 null일 경우, 예외 발생 테스트") + void sendEmail_nullMailType_shouldNotSend() { + //given String to = "applicant@example.com"; String name = "지원자"; - // when & then - CustomException exception = assertThrows(CustomException.class, () -> { - emailService.sendEmail(to, name, null); - }); + // when + emailService.sendEmail(to, name, null); - assertEquals(ErrorCode.MAIL_TYPE_NOT_VALID, exception.getErrorCode()); + // then + verify(javaMailSender, never()).send(any(MimeMessage.class)); + verify(emailLogService, timeout(1000)).logEmailSending(eq(to), any(), any()); } } \ No newline at end of file