Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
import ceos.backend.global.common.dto.AwsSESMail;
import ceos.backend.global.common.dto.SlackUnavailableReason;
import ceos.backend.global.common.event.Event;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.UUID;

@Slf4j
@Component
@RequiredArgsConstructor
Expand Down Expand Up @@ -74,4 +75,14 @@ public Application getApplicationByUuidAndEmail(String uuid, String email) {
throw ApplicantNotFound.EXCEPTION;
});
}

public Application getApplicationByUuidAndEmailForUpdate(String uuid, String email) {
return applicationRepository
.findByUuidAndEmailWithPessimisticLock(uuid, email)
.orElseThrow(
() -> {
throw ApplicantNotFound.EXCEPTION;
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import ceos.backend.domain.application.domain.Application;
import ceos.backend.domain.application.domain.Pass;
import ceos.backend.global.common.entity.Part;
import java.util.Optional;

import jakarta.persistence.LockModeType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
Expand All @@ -14,6 +12,8 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;

public interface ApplicationRepository
extends JpaRepository<Application, Long>, ApplicationRepositoryCustom {
@Query("select distinct a from Application a" + " where a.applicantInfo.email = :email")
Expand All @@ -22,13 +22,23 @@ public interface ApplicationRepository
@Query("select distinct a from Application a" + " where a.applicantInfo.uuid = :uuid")
Optional<Application> findByUuid(@Param("uuid") String uuid);


@Query(
"select a from Application a"
+ " where a.applicantInfo.uuid = :uuid"
+ " and a.applicantInfo.email = :email")
Optional<Application> findByUuidAndEmail(
@Param("uuid") String uuid, @Param("email") String email);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(
"select a from Application a"
+ " where a.applicantInfo.uuid = :uuid"
+ " and a.applicantInfo.email = :email")
Optional<Application> findByUuidAndEmailWithPessimisticLock(
@Param("uuid") String uuid, @Param("email") String email);


@Query("select count(a) > 0 from Application a" + " where a.applicantInfo.email = :email")
boolean existsByEmail(@Param("email") String email);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,12 @@ public void updateInterviewAttendance(
String uuid, String email, UpdateAttendanceRequest request) {
recruitmentValidator.validateBetweenResultDateDocAndResultDateFinal(); // μ„œλ₯˜ 합격 κΈ°κ°„ 검증
applicationValidator.validateApplicantAccessible(uuid, email); // μœ μ € 검증
final Application application = applicationHelper.getApplicationByUuidAndEmail(uuid, email);
final Application application =
applicationHelper.getApplicationByUuidAndEmailForUpdate(uuid, email);
applicationValidator.validateApplicantInterviewCheckStatus(application); // μ„œλ₯˜ν•©κ²©, 인터뷰 체크 검증

if (request.isAvailable()) {
application.updateInterviewCheck(true);
applicationRepository.save(application);
} else {
application.updateUnableReason(request.getReason());
applicationHelper.sendSlackUnableReasonMessage(application, request, false);
Expand All @@ -187,12 +187,12 @@ public void updateParticipationAvailability(
String uuid, String email, UpdateAttendanceRequest request) {
recruitmentValidator.validateFinalResultAbleDuration(); // μ΅œμ’… 합격 κΈ°κ°„ 검증
applicationValidator.validateApplicantAccessible(uuid, email); // μœ μ € 검증
final Application application = applicationHelper.getApplicationByUuidAndEmail(uuid, email);
final Application application =
applicationHelper.getApplicationByUuidAndEmailForUpdate(uuid, email);
applicationValidator.validateApplicantActivityCheckStatus(application); // μœ μ € 확인 μ—¬λΆ€ 검증

if (request.isAvailable()) {
application.updateFinalCheck(true);
applicationRepository.save(application);
} else {
application.updateUnableReason(request.getReason());
applicationHelper.sendSlackUnableReasonMessage(application, request, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import ceos.backend.global.config.jwt.JwtAuthenticationEntryPoint;
import ceos.backend.global.config.jwt.JwtAuthenticationFilter;
import ceos.backend.global.config.jwt.JwtExceptionHandlerFilter;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
Expand All @@ -34,6 +32,9 @@
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

@EnableWebSecurity
@Configuration()
@ConditionalOnDefaultWebSecurity
Expand Down Expand Up @@ -191,8 +192,13 @@ private CorsConfiguration getDefaultCorsConfiguration() {
"http://localhost:8080",
"http://localhost:3000",
"http://localhost:3001",
// ν”„λ‘ νŠΈ ν…ŒμŠ€νŠΈ
"dev-ceos.netlify.app",
"dev-admin-ceos.netlify.app",
// ν”„λ‘ νŠΈ 운영
USER_URL,
ADMIN_URL,
//
SERVER_URL,
DEV_URL));
configuration.setAllowedHeaders(List.of("*"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ceos.backend.global.common.dto.AwsSESMail;
import ceos.backend.global.common.dto.AwsSESPasswordMail;
import ceos.backend.global.common.dto.AwsSESRecruitMail;
import ceos.backend.infra.ses.domain.EmailType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
Expand All @@ -26,22 +27,23 @@ public void handle(AwsSESMail awsSESMail) {
final String SUBJECT =
awsSESMailGenerator.generateApplicationMailSubject(awsSESMail.getGeneration());
final Context CONTEXT = awsSESMailGenerator.generateApplicationMailContext(awsSESMail);
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendApplicationMail", CONTEXT);
awsSesUtils.singleEmailRequest(
TO, SUBJECT, "sendApplicationMail", CONTEXT, EmailType.APPLICATION);
}

@EventListener(AwsSESPasswordMail.class)
public void handle(AwsSESPasswordMail awsSESPasswordMail) {
final String TO = awsSESPasswordMail.getEmail();
final String SUBJECT = awsSESMailGenerator.generatePasswordMailSubject();
final Context CONTEXT = awsSESMailGenerator.generatePasswordMailContext(awsSESPasswordMail);
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendPasswordMail", CONTEXT);
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendPasswordMail", CONTEXT, EmailType.PASSWORD);
}

@EventListener(AwsSESRecruitMail.class)
public void handle(AwsSESRecruitMail awsSESRecruitMail) {
final String TO = awsSESRecruitMail.getEmail();
final String SUBJECT = awsSESMailGenerator.generateRecruitMailSubject();
final Context CONTEXT = awsSESMailGenerator.generateRecruitMailContext(awsSESRecruitMail);
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendRecruitMail", CONTEXT);
awsSesUtils.singleEmailRequest(TO, SUBJECT, "sendRecruitMail", CONTEXT, EmailType.RECRUIT);
}
}
55 changes: 52 additions & 3 deletions src/main/java/ceos/backend/infra/ses/AwsSESUtils.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
package ceos.backend.infra.ses;


import ceos.backend.infra.ses.domain.EmailSendHistory;
import ceos.backend.infra.ses.domain.EmailType;
import ceos.backend.infra.ses.repository.EmailSendHistoryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
import software.amazon.awssdk.services.ses.SesAsyncClient;
import software.amazon.awssdk.services.ses.model.*;

import java.util.concurrent.CompletableFuture;

@Slf4j
@Component
@RequiredArgsConstructor
public class AwsSESUtils {
private final SesAsyncClient sesAsyncClient;
private final SpringTemplateEngine templateEngine;
private final EmailSendHistoryRepository emailSendHistoryRepository;

public void singleEmailRequest(String to, String subject, String template, Context context) {
public void singleEmailRequest(
String to, String subject, String template, Context context, EmailType emailType) {
final String html = templateEngine.process(template, context);

final SendEmailRequest.Builder sendEmailRequestBuilder = SendEmailRequest.builder();
sendEmailRequestBuilder.destination(Destination.builder().toAddresses(to).build());
sendEmailRequestBuilder

SendEmailRequest request = sendEmailRequestBuilder
.message(newMessage(subject, html))
.source("ceos@ceos-sinchon.com")
.build();

sesAsyncClient.sendEmail(sendEmailRequestBuilder.build());
CompletableFuture<SendEmailResponse> future = sesAsyncClient.sendEmail(request);

saveHistory(to, subject, template, emailType, future);
}

private Message newMessage(String subject, String html) {
Expand All @@ -34,4 +46,41 @@ private Message newMessage(String subject, String html) {
.body(Body.builder().html(builder -> builder.data(html)).build())
.build();
}

private void saveHistory(String to, String subject, String template, EmailType emailType, CompletableFuture<SendEmailResponse> future) {
future.whenComplete(
(response, exception) -> {
if (exception != null) {
log.error("Failed to send email to: {}", to, exception);
saveFailureHistory(to, subject, template, emailType, exception);
} else {
log.info("Successfully sent email to: {}, messageId: {}", to, response.messageId());
saveSuccessHistory(to, subject, template, emailType, response.messageId());
}
});
}

private void saveSuccessHistory(
String to, String subject, String template, EmailType emailType, String messageId) {
try {
EmailSendHistory history =
EmailSendHistory.createSuccess(to, subject, template, emailType, messageId);
emailSendHistoryRepository.save(history);
} catch (Exception e) {
log.error("Failed to save email send success history", e);
}
}

private void saveFailureHistory(
String to, String subject, String template, EmailType emailType, Throwable exception) {
try {
EmailSendHistory history =
EmailSendHistory.createFailure(
to, subject, template, emailType, exception.getMessage());
emailSendHistoryRepository.save(history);
} catch (Exception e) {
log.error("Failed to save email send failure history", e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package ceos.backend.infra.ses.domain;


import ceos.backend.global.common.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "email_send_history")
public class EmailSendHistory extends BaseEntity {

@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "recipient_email")
private String recipientEmail;

@Column(name = "subject")
private String subject;

@Column(name = "template_name")
private String templateName;

@Enumerated(EnumType.STRING)
@Column(name = "email_type")
private EmailType emailType;

@Enumerated(EnumType.STRING)
@Column(name = "send_status")
private SendStatus sendStatus;

@Column(name = "message_id", length = 255)
private String messageId;

@Column(name = "error_message", length = 1000)
private String errorMessage;

@Builder(access = AccessLevel.PRIVATE)
private EmailSendHistory(
String recipientEmail,
String subject,
String templateName,
EmailType emailType,
SendStatus sendStatus,
String messageId,
String errorMessage) {
this.recipientEmail = recipientEmail;
this.subject = subject;
this.templateName = templateName;
this.emailType = emailType;
this.sendStatus = sendStatus;
this.messageId = messageId;
this.errorMessage = errorMessage;
}

public static EmailSendHistory createSuccess(
String recipientEmail,
String subject,
String templateName,
EmailType emailType,
String messageId) {
return EmailSendHistory.builder()
.recipientEmail(recipientEmail)
.subject(subject)
.templateName(templateName)
.emailType(emailType)
.sendStatus(SendStatus.SUCCESS)
.messageId(messageId)
.build();
}

public static EmailSendHistory createFailure(
String recipientEmail,
String subject,
String templateName,
EmailType emailType,
String errorMessage) {
return EmailSendHistory.builder()
.recipientEmail(recipientEmail)
.subject(subject)
.templateName(templateName)
.emailType(emailType)
.sendStatus(SendStatus.FAILURE)
.errorMessage(errorMessage)
.build();
}
}
8 changes: 8 additions & 0 deletions src/main/java/ceos/backend/infra/ses/domain/EmailType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package ceos.backend.infra.ses.domain;


public enum EmailType {
APPLICATION, // μ§€μ›μ„œ μ ‘μˆ˜ 확인
PASSWORD, // μž„μ‹œ λΉ„λ°€λ²ˆν˜Έ λ°œκΈ‰
RECRUIT // λ¦¬ν¬λ£¨νŒ… μ•ˆλ‚΄
}
7 changes: 7 additions & 0 deletions src/main/java/ceos/backend/infra/ses/domain/SendStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ceos.backend.infra.ses.domain;


public enum SendStatus {
SUCCESS, // 전솑 성곡
FAILURE // 전솑 μ‹€νŒ¨
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ceos.backend.infra.ses.repository;


import ceos.backend.infra.ses.domain.EmailSendHistory;
import org.springframework.data.jpa.repository.JpaRepository;

public interface EmailSendHistoryRepository extends JpaRepository<EmailSendHistory, Long> {}
2 changes: 1 addition & 1 deletion src/main/resources/templates/component/copyright.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
font-size: 20px;
line-height: 150%;
color: #D6DADF;">
Β© 2016-2025 Ceos ALL RIGHTS RESERVED.
Β© 2016-2026 Ceos ALL RIGHTS RESERVED.
</span>
</div>
</html>