Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.oracle.database.jdbc:ojdbc11'
Expand All @@ -53,6 +55,9 @@ dependencies {
implementation 'com.google.auth:google-auth-library-oauth2-http:0.20.0'





implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package dmu.dasom.api.domain.applicant.repository;

import dmu.dasom.api.domain.applicant.entity.Applicant;
import dmu.dasom.api.domain.applicant.enums.ApplicantStatus;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface ApplicantRepository extends JpaRepository<Applicant, Long> {

@Query("SELECT a FROM Applicant a ORDER BY a.id DESC")
Page<Applicant> findAllWithPageRequest(final Pageable pageable);

// 상태별 지원자 조회
List<Applicant> findByStatus(ApplicantStatus status);

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@ public interface ApplicantService {

ApplicantDetailsResponseDto updateApplicantStatus(final Long id, final ApplicantStatusUpdateRequestDto request);

void sendDocumentPassEmailsToApplicants();

void sendFinalPassEmailsToDocumentPassApplicants();

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,30 @@
import dmu.dasom.api.domain.applicant.dto.ApplicantResponseDto;
import dmu.dasom.api.domain.applicant.dto.ApplicantStatusUpdateRequestDto;
import dmu.dasom.api.domain.applicant.entity.Applicant;
import dmu.dasom.api.domain.applicant.enums.ApplicantStatus;
import dmu.dasom.api.domain.applicant.repository.ApplicantRepository;
import dmu.dasom.api.domain.common.exception.CustomException;
import dmu.dasom.api.domain.common.exception.ErrorCode;
import dmu.dasom.api.domain.email.service.EmailService;
import dmu.dasom.api.global.dto.PageResponse;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class ApplicantServiceImpl implements ApplicantService {

private final static int DEFAULT_PAGE_SIZE = 20;

private final ApplicantRepository applicantRepository;
private final EmailService emailService;

// 지원자 저장
@Override
Expand Down Expand Up @@ -55,6 +63,56 @@ public ApplicantDetailsResponseDto updateApplicantStatus(final Long id, final Ap
return applicant.toApplicantDetailsResponse();
}

// 지원자 이메일 보내기
@Override
public void sendDocumentPassEmailsToApplicants(){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AdminController 코멘트와 마찬가지로 메일 발송 메소드를 하나로 정의하고 발송 타입에 따라 다르게 동작되도록 구현하면 좋을 것 같습니다.

List<Applicant> applicants = applicantRepository.findAll();

if(applicants.isEmpty()) {
throw new CustomException(ErrorCode.EMPTY_RESULT);
}

String subject = "다솜 서류 지원 결과";

for(Applicant applicant : applicants){
try {
emailService.sendEmail(
applicant.getEmail(),
subject,
"document-pass-template",
applicant.getName()
);
log.info("이메일 전송 완료: {}", applicant.getEmail());
} catch (MessagingException e) {
log.error("이메일 전송 실패: {}", applicant.getEmail(), e);
}
}
}

@Override
public void sendFinalPassEmailsToDocumentPassApplicants() {
List<Applicant> applicants = applicantRepository.findByStatus(ApplicantStatus.DOCUMENT_PASSED);

if(applicants.isEmpty()){
throw new CustomException(ErrorCode.EMPTY_RESULT);
}

String subject = "다솜 최종 합격 결과";
for (Applicant applicant : applicants) {
try {
emailService.sendEmail(
applicant.getEmail(),
subject,
"final-pass-template",
applicant.getName()
);
log.info("이메일 전송 완료: {}", applicant.getEmail());
} catch (MessagingException e) {
log.error("이메일 전송 실패: {}", applicant.getEmail(), e);
}
}
}

// Repository에서 ID로 지원자 조회
private Applicant findById(final Long id) {
return applicantRepository.findById(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public enum ErrorCode {
INTERNAL_SERVER_ERROR(500, "C009", "서버에 문제가 발생하였습니다."),
NOT_FOUND(404, "C010", "해당 리소스를 찾을 수 없습니다."),
WRITE_FAIL(400, "C011", "데이터를 쓰는데 실패하였습니다."),
EMPTY_RESULT(400, "C012", "조회 결과가 없습니다.")
EMPTY_RESULT(400, "C012", "조회 결과가 없습니다."),
SEND_EMAIL_FAIL(400, "C014", "이메일 전송에 실패하였습니다."),
;

private final int status;
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/dmu/dasom/api/domain/email/service/EmailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dmu.dasom.api.domain.email.service;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@RequiredArgsConstructor
@Service
public class EmailService {

private JavaMailSender javaMailSender;
private TemplateEngine templateEngine;
@Value("${spring.mail.username}")
private String from;

public void sendEmail(String to, String subject, String templateName, String name) throws MessagingException {
// HTML 템플릿에 전달할 데이터 설정
Context context = new Context();
context.setVariable("name", name); // 지원자 이름 전달

// HTML 템플릿 처리
String htmlBody = templateEngine.process(templateName, context);

// 이메일 생성 및 전송
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);

javaMailSender.send(message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,48 @@ public ResponseEntity<ApplicantDetailsResponseDto> updateApplicantStatus(@PathVa
return ResponseEntity.ok(applicantService.updateApplicantStatus(id, request));
}

@Operation(summary = "서류 결과 메일 전송")
Copy link
Member

@ysw789 ysw789 Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

서류, 최종 결과 메일 발송 메소드를 따로 나누지 않고 메일 발송 메소드 식으로 하나로 합쳐 클라이언트 요청에 따라 다르게 처리할 수 있을 것 같네요.
Enum으로 타입을 정의해놓고 클라이언트로부터 요청과 함께 메일 발송 타입을 전달받으면 그에 따라 발송되는 식으로 하는 건 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is good idea!!

@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "메일 전송 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "전송 실패",
value = "{ \"code\": \"C014\", \"message\": \"이메일 전송에 실패하였습니다.\" }"
)
}
)
)
})
@PostMapping("/applicants/send-document-pass-email")
public ResponseEntity<String> sendDocumentPassEmails() {
applicantService.sendDocumentPassEmailsToApplicants();
return ResponseEntity.ok("이메일 전송 성공");
}

@Operation(summary = "최종 결과 메일 전송")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "메일 전송 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "전송 실패",
value = "{ \"code\": \"C014\", \"message\": \"이메일 전송에 실패하였습니다.\" }"
)
}
)
)
})
@PostMapping("/applicants/send-final-pass-email")
public ResponseEntity<String> sendFinalPassEmail(){
applicantService.sendFinalPassEmailsToDocumentPassApplicants();
return ResponseEntity.ok("이메일 전송 성공");
}

}
11 changes: 11 additions & 0 deletions src/main/resources/application-credentials.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ spring:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
mail:
host: smtp.gmail.com
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
jwt:
secret: ${JWT_SECRET}
access-token-expiration: ${JWT_ACCESS_TOKEN_EXPIRATION}
Expand Down
79 changes: 79 additions & 0 deletions src/main/resources/template/email-template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>지원 결과 확인</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #1a1a1a;
color: #ffffff;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #1a1a1a;
border-radius: 8px;
text-align: center;
}
.header {
font-size: 24px;
font-weight: bold;
color: #00B493;
margin-bottom: 20px;
}
.sub-header {
font-size: 16px;
background-color: #00B493;
padding: 10px;
border-radius: 5px;
margin-bottom: 20px;
}
.content {
font-size: 14px;
line-height: 1.8;
}
.content p {
margin-bottom: 10px;
}
.highlight {
color: #00B493;
}
.button {
display: inline-block;
background-color: #00B493;
color: #ffffff;
padding: 12px 24px;
border-radius: 5px;
text-decoration: none;
font-size: 16px;
}
.button:hover {
background-color: #00B493;
}
</style>
</head>
<body>
<div class="container">
<!-- Header Section -->
<div class="header">DASOM</div>

<!-- Sub-header Section -->
<div class="sub-header">컴퓨터소프트웨어공학과 전공동아리 다솜<br>34기 서류 합격자 조회</div>

<!-- Content Section -->
<div class="content">
<p>안녕하세요, <span class="highlight" th:text="${name}"></span>님.</p>
<p>학번 마지막 <span class="highlight">4자리</span> + 전화번호 마지막 <span class="highlight">4자리</span>를 입력하여<br>지원 결과를 확인할 수 있습니다.</p>
<p>예시) <span class="highlight">08470542</span></p>

<!-- Button -->
<a href="https://example.com/result" class="button">결과 확인하기</a>
</div>
</div>
</body>
</html>