Skip to content

Commit a60bb5e

Browse files
authored
Merge pull request #235 from CEOS-Developers/dev
merge Dev
2 parents 9995fa4 + 039b5cc commit a60bb5e

11 files changed

Lines changed: 209 additions & 17 deletions

File tree

src/main/java/ceos/backend/domain/application/helper/ApplicationHelper.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
import ceos.backend.global.common.dto.AwsSESMail;
1212
import ceos.backend.global.common.dto.SlackUnavailableReason;
1313
import ceos.backend.global.common.event.Event;
14-
import java.util.List;
15-
import java.util.UUID;
1614
import lombok.RequiredArgsConstructor;
1715
import lombok.extern.slf4j.Slf4j;
1816
import org.springframework.stereotype.Component;
1917

18+
import java.util.List;
19+
import java.util.UUID;
20+
2021
@Slf4j
2122
@Component
2223
@RequiredArgsConstructor
@@ -74,4 +75,14 @@ public Application getApplicationByUuidAndEmail(String uuid, String email) {
7475
throw ApplicantNotFound.EXCEPTION;
7576
});
7677
}
78+
79+
public Application getApplicationByUuidAndEmailForUpdate(String uuid, String email) {
80+
return applicationRepository
81+
.findByUuidAndEmailWithPessimisticLock(uuid, email)
82+
.orElseThrow(
83+
() -> {
84+
throw ApplicantNotFound.EXCEPTION;
85+
});
86+
}
87+
7788
}

src/main/java/ceos/backend/domain/application/repository/ApplicationRepository.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import ceos.backend.domain.application.domain.Application;
55
import ceos.backend.domain.application.domain.Pass;
66
import ceos.backend.global.common.entity.Part;
7-
import java.util.Optional;
8-
97
import jakarta.persistence.LockModeType;
108
import org.springframework.data.domain.Page;
119
import org.springframework.data.domain.PageRequest;
@@ -14,6 +12,8 @@
1412
import org.springframework.data.jpa.repository.Query;
1513
import org.springframework.data.repository.query.Param;
1614

15+
import java.util.Optional;
16+
1717
public interface ApplicationRepository
1818
extends JpaRepository<Application, Long>, ApplicationRepositoryCustom {
1919
@Query("select distinct a from Application a" + " where a.applicantInfo.email = :email")
@@ -22,13 +22,23 @@ public interface ApplicationRepository
2222
@Query("select distinct a from Application a" + " where a.applicantInfo.uuid = :uuid")
2323
Optional<Application> findByUuid(@Param("uuid") String uuid);
2424

25+
2526
@Query(
2627
"select a from Application a"
2728
+ " where a.applicantInfo.uuid = :uuid"
2829
+ " and a.applicantInfo.email = :email")
2930
Optional<Application> findByUuidAndEmail(
3031
@Param("uuid") String uuid, @Param("email") String email);
3132

33+
@Lock(LockModeType.PESSIMISTIC_WRITE)
34+
@Query(
35+
"select a from Application a"
36+
+ " where a.applicantInfo.uuid = :uuid"
37+
+ " and a.applicantInfo.email = :email")
38+
Optional<Application> findByUuidAndEmailWithPessimisticLock(
39+
@Param("uuid") String uuid, @Param("email") String email);
40+
41+
3242
@Query("select count(a) > 0 from Application a" + " where a.applicantInfo.email = :email")
3343
boolean existsByEmail(@Param("email") String email);
3444

src/main/java/ceos/backend/domain/application/service/ApplicationService.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,12 @@ public void updateInterviewAttendance(
158158
String uuid, String email, UpdateAttendanceRequest request) {
159159
recruitmentValidator.validateBetweenResultDateDocAndResultDateFinal(); // 서류 합격 기간 검증
160160
applicationValidator.validateApplicantAccessible(uuid, email); // 유저 검증
161-
final Application application = applicationHelper.getApplicationByUuidAndEmail(uuid, email);
161+
final Application application =
162+
applicationHelper.getApplicationByUuidAndEmailForUpdate(uuid, email);
162163
applicationValidator.validateApplicantInterviewCheckStatus(application); // 서류합격, 인터뷰 체크 검증
163164

164165
if (request.isAvailable()) {
165166
application.updateInterviewCheck(true);
166-
applicationRepository.save(application);
167167
} else {
168168
application.updateUnableReason(request.getReason());
169169
applicationHelper.sendSlackUnableReasonMessage(application, request, false);
@@ -187,12 +187,12 @@ public void updateParticipationAvailability(
187187
String uuid, String email, UpdateAttendanceRequest request) {
188188
recruitmentValidator.validateFinalResultAbleDuration(); // 최종 합격 기간 검증
189189
applicationValidator.validateApplicantAccessible(uuid, email); // 유저 검증
190-
final Application application = applicationHelper.getApplicationByUuidAndEmail(uuid, email);
190+
final Application application =
191+
applicationHelper.getApplicationByUuidAndEmailForUpdate(uuid, email);
191192
applicationValidator.validateApplicantActivityCheckStatus(application); // 유저 확인 여부 검증
192193

193194
if (request.isAvailable()) {
194195
application.updateFinalCheck(true);
195-
applicationRepository.save(application);
196196
} else {
197197
application.updateUnableReason(request.getReason());
198198
applicationHelper.sendSlackUnableReasonMessage(application, request, true);

src/main/java/ceos/backend/global/config/WebSecurityConfig.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import ceos.backend.global.config.jwt.JwtAuthenticationEntryPoint;
88
import ceos.backend.global.config.jwt.JwtAuthenticationFilter;
99
import ceos.backend.global.config.jwt.JwtExceptionHandlerFilter;
10-
import java.util.Arrays;
11-
import java.util.List;
1210
import lombok.RequiredArgsConstructor;
1311
import org.springframework.beans.factory.annotation.Value;
1412
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
@@ -34,6 +32,9 @@
3432
import org.springframework.web.cors.CorsUtils;
3533
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
3634

35+
import java.util.Arrays;
36+
import java.util.List;
37+
3738
@EnableWebSecurity
3839
@Configuration()
3940
@ConditionalOnDefaultWebSecurity
@@ -191,8 +192,13 @@ private CorsConfiguration getDefaultCorsConfiguration() {
191192
"http://localhost:8080",
192193
"http://localhost:3000",
193194
"http://localhost:3001",
195+
// 프론트 테스트
196+
"dev-ceos.netlify.app",
197+
"dev-admin-ceos.netlify.app",
198+
// 프론트 운영
194199
USER_URL,
195200
ADMIN_URL,
201+
//
196202
SERVER_URL,
197203
DEV_URL));
198204
configuration.setAllowedHeaders(List.of("*"));

src/main/java/ceos/backend/infra/ses/AwsSESSendMailHandler.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import ceos.backend.global.common.dto.AwsSESMail;
66
import ceos.backend.global.common.dto.AwsSESPasswordMail;
77
import ceos.backend.global.common.dto.AwsSESRecruitMail;
8+
import ceos.backend.infra.ses.domain.EmailType;
89
import lombok.RequiredArgsConstructor;
910
import lombok.extern.slf4j.Slf4j;
1011
import org.springframework.context.event.EventListener;
@@ -26,22 +27,23 @@ public void handle(AwsSESMail awsSESMail) {
2627
final String SUBJECT =
2728
awsSESMailGenerator.generateApplicationMailSubject(awsSESMail.getGeneration());
2829
final Context CONTEXT = awsSESMailGenerator.generateApplicationMailContext(awsSESMail);
29-
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendApplicationMail", CONTEXT);
30+
awsSesUtils.singleEmailRequest(
31+
TO, SUBJECT, "sendApplicationMail", CONTEXT, EmailType.APPLICATION);
3032
}
3133

3234
@EventListener(AwsSESPasswordMail.class)
3335
public void handle(AwsSESPasswordMail awsSESPasswordMail) {
3436
final String TO = awsSESPasswordMail.getEmail();
3537
final String SUBJECT = awsSESMailGenerator.generatePasswordMailSubject();
3638
final Context CONTEXT = awsSESMailGenerator.generatePasswordMailContext(awsSESPasswordMail);
37-
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendPasswordMail", CONTEXT);
39+
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendPasswordMail", CONTEXT, EmailType.PASSWORD);
3840
}
3941

4042
@EventListener(AwsSESRecruitMail.class)
4143
public void handle(AwsSESRecruitMail awsSESRecruitMail) {
4244
final String TO = awsSESRecruitMail.getEmail();
4345
final String SUBJECT = awsSESMailGenerator.generateRecruitMailSubject();
4446
final Context CONTEXT = awsSESMailGenerator.generateRecruitMailContext(awsSESRecruitMail);
45-
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendRecruitMail", CONTEXT);
47+
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendRecruitMail", CONTEXT, EmailType.RECRUIT);
4648
}
4749
}
Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,42 @@
11
package ceos.backend.infra.ses;
22

33

4+
import ceos.backend.infra.ses.domain.EmailSendHistory;
5+
import ceos.backend.infra.ses.domain.EmailType;
6+
import ceos.backend.infra.ses.repository.EmailSendHistoryRepository;
47
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
59
import org.springframework.stereotype.Component;
610
import org.thymeleaf.context.Context;
711
import org.thymeleaf.spring6.SpringTemplateEngine;
812
import software.amazon.awssdk.services.ses.SesAsyncClient;
913
import software.amazon.awssdk.services.ses.model.*;
1014

15+
import java.util.concurrent.CompletableFuture;
16+
17+
@Slf4j
1118
@Component
1219
@RequiredArgsConstructor
1320
public class AwsSESUtils {
1421
private final SesAsyncClient sesAsyncClient;
1522
private final SpringTemplateEngine templateEngine;
23+
private final EmailSendHistoryRepository emailSendHistoryRepository;
1624

17-
public void singleEmailRequest(String to, String subject, String template, Context context) {
25+
public void singleEmailRequest(
26+
String to, String subject, String template, Context context, EmailType emailType) {
1827
final String html = templateEngine.process(template, context);
1928

2029
final SendEmailRequest.Builder sendEmailRequestBuilder = SendEmailRequest.builder();
2130
sendEmailRequestBuilder.destination(Destination.builder().toAddresses(to).build());
22-
sendEmailRequestBuilder
31+
32+
SendEmailRequest request = sendEmailRequestBuilder
2333
.message(newMessage(subject, html))
2434
.source("ceos@ceos-sinchon.com")
2535
.build();
2636

27-
sesAsyncClient.sendEmail(sendEmailRequestBuilder.build());
37+
CompletableFuture<SendEmailResponse> future = sesAsyncClient.sendEmail(request);
38+
39+
saveHistory(to, subject, template, emailType, future);
2840
}
2941

3042
private Message newMessage(String subject, String html) {
@@ -34,4 +46,41 @@ private Message newMessage(String subject, String html) {
3446
.body(Body.builder().html(builder -> builder.data(html)).build())
3547
.build();
3648
}
49+
50+
private void saveHistory(String to, String subject, String template, EmailType emailType, CompletableFuture<SendEmailResponse> future) {
51+
future.whenComplete(
52+
(response, exception) -> {
53+
if (exception != null) {
54+
log.error("Failed to send email to: {}", to, exception);
55+
saveFailureHistory(to, subject, template, emailType, exception);
56+
} else {
57+
log.info("Successfully sent email to: {}, messageId: {}", to, response.messageId());
58+
saveSuccessHistory(to, subject, template, emailType, response.messageId());
59+
}
60+
});
61+
}
62+
63+
private void saveSuccessHistory(
64+
String to, String subject, String template, EmailType emailType, String messageId) {
65+
try {
66+
EmailSendHistory history =
67+
EmailSendHistory.createSuccess(to, subject, template, emailType, messageId);
68+
emailSendHistoryRepository.save(history);
69+
} catch (Exception e) {
70+
log.error("Failed to save email send success history", e);
71+
}
72+
}
73+
74+
private void saveFailureHistory(
75+
String to, String subject, String template, EmailType emailType, Throwable exception) {
76+
try {
77+
EmailSendHistory history =
78+
EmailSendHistory.createFailure(
79+
to, subject, template, emailType, exception.getMessage());
80+
emailSendHistoryRepository.save(history);
81+
} catch (Exception e) {
82+
log.error("Failed to save email send failure history", e);
83+
}
84+
}
85+
3786
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package ceos.backend.infra.ses.domain;
2+
3+
4+
import ceos.backend.global.common.entity.BaseEntity;
5+
import jakarta.persistence.*;
6+
import lombok.AccessLevel;
7+
import lombok.Builder;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
11+
@Entity
12+
@Getter
13+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
14+
@Table(name = "email_send_history")
15+
public class EmailSendHistory extends BaseEntity {
16+
17+
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
18+
private Long id;
19+
20+
@Column(name = "recipient_email")
21+
private String recipientEmail;
22+
23+
@Column(name = "subject")
24+
private String subject;
25+
26+
@Column(name = "template_name")
27+
private String templateName;
28+
29+
@Enumerated(EnumType.STRING)
30+
@Column(name = "email_type")
31+
private EmailType emailType;
32+
33+
@Enumerated(EnumType.STRING)
34+
@Column(name = "send_status")
35+
private SendStatus sendStatus;
36+
37+
@Column(name = "message_id", length = 255)
38+
private String messageId;
39+
40+
@Column(name = "error_message", length = 1000)
41+
private String errorMessage;
42+
43+
@Builder(access = AccessLevel.PRIVATE)
44+
private EmailSendHistory(
45+
String recipientEmail,
46+
String subject,
47+
String templateName,
48+
EmailType emailType,
49+
SendStatus sendStatus,
50+
String messageId,
51+
String errorMessage) {
52+
this.recipientEmail = recipientEmail;
53+
this.subject = subject;
54+
this.templateName = templateName;
55+
this.emailType = emailType;
56+
this.sendStatus = sendStatus;
57+
this.messageId = messageId;
58+
this.errorMessage = errorMessage;
59+
}
60+
61+
public static EmailSendHistory createSuccess(
62+
String recipientEmail,
63+
String subject,
64+
String templateName,
65+
EmailType emailType,
66+
String messageId) {
67+
return EmailSendHistory.builder()
68+
.recipientEmail(recipientEmail)
69+
.subject(subject)
70+
.templateName(templateName)
71+
.emailType(emailType)
72+
.sendStatus(SendStatus.SUCCESS)
73+
.messageId(messageId)
74+
.build();
75+
}
76+
77+
public static EmailSendHistory createFailure(
78+
String recipientEmail,
79+
String subject,
80+
String templateName,
81+
EmailType emailType,
82+
String errorMessage) {
83+
return EmailSendHistory.builder()
84+
.recipientEmail(recipientEmail)
85+
.subject(subject)
86+
.templateName(templateName)
87+
.emailType(emailType)
88+
.sendStatus(SendStatus.FAILURE)
89+
.errorMessage(errorMessage)
90+
.build();
91+
}
92+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package ceos.backend.infra.ses.domain;
2+
3+
4+
public enum EmailType {
5+
APPLICATION, // 지원서 접수 확인
6+
PASSWORD, // 임시 비밀번호 발급
7+
RECRUIT // 리크루팅 안내
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package ceos.backend.infra.ses.domain;
2+
3+
4+
public enum SendStatus {
5+
SUCCESS, // 전송 성공
6+
FAILURE // 전송 실패
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package ceos.backend.infra.ses.repository;
2+
3+
4+
import ceos.backend.infra.ses.domain.EmailSendHistory;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
public interface EmailSendHistoryRepository extends JpaRepository<EmailSendHistory, Long> {}

0 commit comments

Comments
 (0)