Skip to content

Commit db28bfc

Browse files
committed
feat: 메일 발송 기능 구현
1 parent 82c0d1a commit db28bfc

File tree

8 files changed

+193
-1
lines changed

8 files changed

+193
-1
lines changed

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ dependencies {
3939
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
4040
implementation 'org.springframework.boot:spring-boot-starter-security'
4141
implementation 'org.springframework.boot:spring-boot-starter-validation'
42+
implementation 'org.springframework.boot:spring-boot-starter-mail'
4243
implementation 'org.springframework.boot:spring-boot-starter-web'
44+
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
4345
compileOnly 'org.projectlombok:lombok'
4446
developmentOnly 'org.springframework.boot:spring-boot-devtools'
4547
runtimeOnly 'com.oracle.database.jdbc:ojdbc11'
@@ -53,6 +55,9 @@ dependencies {
5355
implementation 'com.google.auth:google-auth-library-oauth2-http:0.20.0'
5456

5557

58+
59+
60+
5661
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
5762
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
5863
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'

src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ public interface ApplicantService {
1616

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

19+
void sendEmailsToApplicants();
20+
1921
}

src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,26 @@
88
import dmu.dasom.api.domain.applicant.repository.ApplicantRepository;
99
import dmu.dasom.api.domain.common.exception.CustomException;
1010
import dmu.dasom.api.domain.common.exception.ErrorCode;
11+
import dmu.dasom.api.domain.email.service.EmailService;
1112
import dmu.dasom.api.global.dto.PageResponse;
13+
import jakarta.mail.MessagingException;
1214
import lombok.RequiredArgsConstructor;
15+
import lombok.extern.slf4j.Slf4j;
1316
import org.springframework.data.domain.Page;
1417
import org.springframework.data.domain.PageRequest;
1518
import org.springframework.stereotype.Service;
1619

20+
import java.util.List;
21+
22+
@Slf4j
1723
@RequiredArgsConstructor
1824
@Service
1925
public class ApplicantServiceImpl implements ApplicantService {
2026

2127
private final static int DEFAULT_PAGE_SIZE = 20;
2228

2329
private final ApplicantRepository applicantRepository;
30+
private final EmailService emailService;
2431

2532
// 지원자 저장
2633
@Override
@@ -55,6 +62,27 @@ public ApplicantDetailsResponseDto updateApplicantStatus(final Long id, final Ap
5562
return applicant.toApplicantDetailsResponse();
5663
}
5764

65+
// 지원자 이메일 보내기
66+
@Override
67+
public void sendEmailsToApplicants(){
68+
List<Applicant> applicants = applicantRepository.findAll();
69+
70+
if(applicants.isEmpty()) {
71+
throw new CustomException(ErrorCode.EMPTY_RESULT);
72+
}
73+
74+
String subject = "다솜 지원 결과";
75+
76+
for(Applicant applicant : applicants){
77+
try {
78+
emailService.sendEmail(applicant.getEmail(), subject, applicant.getName());
79+
log.info("HTML 이메일 전송 완료: {}", applicant.getEmail());
80+
} catch (MessagingException e) {
81+
log.error("이메일 전송 실패: {}", applicant.getEmail(), e);
82+
}
83+
}
84+
}
85+
5886
// Repository에서 ID로 지원자 조회
5987
private Applicant findById(final Long id) {
6088
return applicantRepository.findById(id)

src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ public enum ErrorCode {
1818
INTERNAL_SERVER_ERROR(500, "C009", "서버에 문제가 발생하였습니다."),
1919
NOT_FOUND(404, "C010", "해당 리소스를 찾을 수 없습니다."),
2020
WRITE_FAIL(400, "C011", "데이터를 쓰는데 실패하였습니다."),
21-
EMPTY_RESULT(400, "C012", "조회 결과가 없습니다.")
21+
EMPTY_RESULT(400, "C012", "조회 결과가 없습니다."),
22+
SEND_EMAIL_FAIL(400, "C013", "이메일 전송에 실패하였습니다."),
2223
;
2324

2425
private final int status;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package dmu.dasom.api.domain.email.service;
2+
3+
import jakarta.mail.MessagingException;
4+
import jakarta.mail.internet.MimeMessage;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.mail.SimpleMailMessage;
9+
import org.springframework.mail.javamail.JavaMailSender;
10+
import org.springframework.mail.javamail.MimeMessageHelper;
11+
import org.springframework.stereotype.Service;
12+
import org.thymeleaf.TemplateEngine;
13+
import org.thymeleaf.context.Context;
14+
15+
@RequiredArgsConstructor
16+
@Service
17+
public class EmailService {
18+
19+
private JavaMailSender javaMailSender;
20+
private TemplateEngine templateEngine;
21+
@Value("${spring.mail.username}")
22+
private String from;
23+
24+
public void sendEmail(String to, String subject, String name) throws MessagingException {
25+
// HTML 템플릿에 전달할 데이터 설정
26+
Context context = new Context();
27+
context.setVariable("name", name); // 지원자 이름 전달
28+
29+
// HTML 템플릿 처리
30+
String htmlBody = templateEngine.process("email-template", context);
31+
32+
// 이메일 생성 및 전송
33+
MimeMessage message = javaMailSender.createMimeMessage();
34+
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
35+
36+
helper.setTo(to);
37+
helper.setSubject(subject);
38+
helper.setText(htmlBody, true);
39+
helper.setFrom(from);
40+
41+
javaMailSender.send(message);
42+
}
43+
44+
}

src/main/java/dmu/dasom/api/global/admin/controller/AdminController.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,26 @@ public ResponseEntity<ApplicantDetailsResponseDto> updateApplicantStatus(@PathVa
7979
return ResponseEntity.ok(applicantService.updateApplicantStatus(id, request));
8080
}
8181

82+
@Operation(summary = "지원자 메일 전송")
83+
@ApiResponses(value = {
84+
@ApiResponse(responseCode = "200", description = "메일 전송 성공"),
85+
@ApiResponse(responseCode = "400", description = "잘못된 요청",
86+
content = @Content(
87+
mediaType = "application/json",
88+
schema = @Schema(implementation = ErrorResponse.class),
89+
examples = {
90+
@ExampleObject(
91+
name = "전송 실패",
92+
value = "{ \"code\": \"C013\", \"message\": \"이메일 전송에 실패하였습니다.\" }"
93+
)
94+
}
95+
)
96+
)
97+
})
98+
@PostMapping("/applicants/send-email")
99+
public ResponseEntity<String> sendEmailsToApplicants() {
100+
applicantService.sendEmailsToApplicants();
101+
return ResponseEntity.ok("이메일 전송 성공");
102+
}
103+
82104
}

src/main/resources/application-credentials.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ spring:
1515
redis:
1616
host: ${REDIS_HOST}
1717
port: ${REDIS_PORT}
18+
mail:
19+
host: smtp.gmail.com
20+
port: 587
21+
username: ${MAIL_USERNAME}
22+
password: ${MAIL_PASSWORD}
23+
properties:
24+
mail:
25+
smtp:
26+
auth: true
27+
starttls:
28+
enable: true
1829
jwt:
1930
secret: ${JWT_SECRET}
2031
access-token-expiration: ${JWT_ACCESS_TOKEN_EXPIRATION}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<!DOCTYPE html>
2+
<html lang="ko">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>지원 결과 확인</title>
7+
<style>
8+
body {
9+
font-family: Arial, sans-serif;
10+
background-color: #1a1a1a;
11+
color: #ffffff;
12+
margin: 0;
13+
padding: 0;
14+
}
15+
.container {
16+
max-width: 600px;
17+
margin: 0 auto;
18+
padding: 20px;
19+
background-color: #1a1a1a;
20+
border-radius: 8px;
21+
text-align: center;
22+
}
23+
.header {
24+
font-size: 24px;
25+
font-weight: bold;
26+
color: #4caf50;
27+
margin-bottom: 20px;
28+
}
29+
.sub-header {
30+
font-size: 16px;
31+
background-color: #4caf50;
32+
padding: 10px;
33+
border-radius: 5px;
34+
margin-bottom: 20px;
35+
}
36+
.content {
37+
font-size: 14px;
38+
line-height: 1.8;
39+
}
40+
.content p {
41+
margin-bottom: 10px;
42+
}
43+
.highlight {
44+
color: #4caf50;
45+
}
46+
.button {
47+
display: inline-block;
48+
background-color: #4caf50;
49+
color: #ffffff;
50+
padding: 12px 24px;
51+
border-radius: 5px;
52+
text-decoration: none;
53+
font-size: 16px;
54+
}
55+
.button:hover {
56+
background-color: #45a049;
57+
}
58+
</style>
59+
</head>
60+
<body>
61+
<div class="container">
62+
<!-- Header Section -->
63+
<div class="header">DASOM</div>
64+
65+
<!-- Sub-header Section -->
66+
<div class="sub-header">컴퓨터 소프트웨어 공학과 전공 동아리 다솜<br>34기 합격자 조회</div>
67+
68+
<!-- Content Section -->
69+
<div class="content">
70+
<p>안녕하세요, <span class="highlight" th:text="${name}"></span>님.</p>
71+
<p>학번 마지막 <span class="highlight">4자리</span> + 전화번호 마지막 <span class="highlight">4자리</span>를 입력하여<br>지원 결과를 확인할 수 있습니다.</p>
72+
<p>예시) <span class="highlight">08470542</span></p>
73+
74+
<!-- Button -->
75+
<a href="https://example.com/result" class="button">결과 확인하기</a>
76+
</div>
77+
</div>
78+
</body>
79+
</html>

0 commit comments

Comments
 (0)