-
Notifications
You must be signed in to change notification settings - Fork 1
[COT-264] Feature: 메일 전송 API 구현 #353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
f42569d
4dadf05
404a338
7a455b6
1372640
8061844
06bfef5
40e41e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package org.cotato.csquiz.api.recruitment.dto; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
|
||
public record RequestNotificationRequest( | ||
@NotNull | ||
Integer generationNumber | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package org.cotato.csquiz.common.config; | ||
|
||
import com.amazonaws.auth.AWSStaticCredentialsProvider; | ||
import com.amazonaws.auth.BasicAWSCredentials; | ||
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; | ||
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
public class AwsSesConfig { | ||
|
||
@Value("${cloud.aws.credentials.accessKey}") | ||
private String accessKey; | ||
@Value("${cloud.aws.credentials.secretKey}") | ||
private String secretKey; | ||
@Value("${cloud.aws.region.static}") | ||
private String region; | ||
Comment on lines
+11
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 환경변수에 있는 값들 이렇게 Bean으로 등록해서 관리할 수 있는데 외부에서 주입 필요할땐 다음에 이렇게 쓰면 좋을 것 같아요 @Getter
@Setter
@ConfigurationProperties(prefix = "cloud.aws.s3")
public class S3Properties {
private String accessKey;
private String secretKey;
private String region;
private String bucketName;
} |
||
|
||
|
||
@Bean | ||
public AmazonSimpleEmailService amazonSimpleEmailService() { | ||
final BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); | ||
final AWSStaticCredentialsProvider awsStaticCredentialsProvider = new AWSStaticCredentialsProvider( | ||
basicAWSCredentials | ||
); | ||
|
||
return AmazonSimpleEmailServiceClientBuilder.standard() | ||
.withCredentials(awsStaticCredentialsProvider) | ||
.withRegion(region) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,4 +20,15 @@ public Executor quizSendThreadPoolExecutor() { | |
taskExecutor.initialize(); | ||
return taskExecutor; | ||
} | ||
|
||
@Bean("emailSendThreadPoolExecutor") | ||
public Executor emailSendThreadPoolExecutor() { | ||
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); | ||
taskExecutor.setCorePoolSize(20); | ||
taskExecutor.setMaxPoolSize(100); | ||
taskExecutor.setQueueCapacity(10000); | ||
Comment on lines
+27
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요거 각 필드 개수 선정한 기준이 있나여? |
||
taskExecutor.setThreadNamePrefix("email-send-thread-"); | ||
taskExecutor.initialize(); | ||
return taskExecutor; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package org.cotato.csquiz.common.email; | ||
|
||
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService; | ||
import com.amazonaws.services.simpleemail.model.Body; | ||
import com.amazonaws.services.simpleemail.model.Content; | ||
import com.amazonaws.services.simpleemail.model.Destination; | ||
import com.amazonaws.services.simpleemail.model.Message; | ||
import com.amazonaws.services.simpleemail.model.SendEmailRequest; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class AwsMailSender { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 구체적으로 어느 서비스를 활용한 구현체인지 알아야하니까 Ses Email Sender로 가시죠 !! |
||
|
||
private final AmazonSimpleEmailService ses; | ||
|
||
@Value("${cloud.aws.ses.emailAddress}") | ||
private String from; | ||
Comment on lines
+22
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 딱 이런 부분에서 위에 언급한 것처럼 yaml 파일 상수로 가져와서 쓰면 좋을 것 같아요 |
||
|
||
public void sendRawMessageBody(String recipient, String htmlBody, String subject) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요기 recipient가 Email 형식인지 검증 안해도 괜찮을까여? |
||
Content subjectContent = new Content(subject); | ||
subjectContent.setCharset("UTF-8"); | ||
|
||
Content bodyContent = new Content(htmlBody); | ||
bodyContent.setCharset("UTF-8"); | ||
Body messageBody = createHtmlBody(bodyContent); | ||
|
||
SendEmailRequest req = new SendEmailRequest( | ||
from, | ||
new Destination(List.of(recipient)), | ||
new Message( | ||
new Content(subject), | ||
messageBody | ||
) | ||
); | ||
ses.sendEmail(req); | ||
} | ||
|
||
private Body createHtmlBody(Content content) { | ||
Body messageBody = new Body(); | ||
messageBody.setHtml(content); | ||
return messageBody; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package org.cotato.csquiz.domain.recruitment.email; | ||
|
||
public record EmailContent( | ||
String subject, | ||
String htmlBody | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package org.cotato.csquiz.domain.recruitment.email; | ||
|
||
public class RecruitmentEmailFactory { | ||
private static final String LINK_URL = "https://www.cotato.kr"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요것도 환경변수로 넣어서 QA랑 main에서 활용을 구분하면 좋을 것 같습니다 ! |
||
|
||
public static EmailContent createForGeneration(int generation) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기수를 위해서 만든다? 메서드명이 너무 모호한 것 같아요 모집 안내 전용인가..? 용도에 맞게 구체화되었으면 좋겠습니다 ! |
||
String subject = generation + "기 모집이 시작됐습니다."; | ||
|
||
String htmlBody = """ | ||
<html> | ||
<body style="margin:0;padding:0;font-family:Arial,sans-serif;background:#f9f9f9;"> | ||
<!-- HEADER --> | ||
<div style="background:#004cab;color:#fff;padding:20px;text-align:center;"> | ||
<h1>코테이토 %d기 모집 안내</h1> | ||
</div> | ||
<!-- BODY --> | ||
<div style="padding:20px;background:#fff;"> | ||
<p style="font-size:16px;line-height:1.5;"> | ||
코테이토 %d기 모집이 시작됐습니다! | ||
</p> | ||
<p style="font-size:14px;"> | ||
아래 버튼을 눌러 모집 신청을 진행해주세요. | ||
</p> | ||
<div style="text-align:center;margin:30px 0;"> | ||
<a href="%s" | ||
style="display:inline-block; | ||
padding:12px 24px; | ||
background:#0066cc; | ||
color:#fff; | ||
text-decoration:none; | ||
border-radius:4px; | ||
font-weight:bold;"> | ||
모집 신청하러 가기 | ||
</a> | ||
</div> | ||
</div> | ||
<!-- FOOTER --> | ||
<div style="background:#f1f1f1;color:#777;padding:10px;text-align:center;font-size:12px;"> | ||
© 2025 코테이토. All rights reserved. | ||
</div> | ||
</body> | ||
</html> | ||
""".formatted(generation, generation, LINK_URL); | ||
|
||
return new EmailContent(subject, htmlBody); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package org.cotato.csquiz.domain.recruitment.repository; | ||
|
||
import java.sql.PreparedStatement; | ||
import java.sql.SQLException; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import org.cotato.csquiz.domain.recruitment.entity.RecruitmentNotificationEmailLog; | ||
import org.springframework.jdbc.core.BatchPreparedStatementSetter; | ||
import org.springframework.jdbc.core.JdbcTemplate; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Repository | ||
@RequiredArgsConstructor | ||
public class RecruitmentNotificationEmailLogJdbcRepository { | ||
|
||
private static final int BATCH_SIZE = 1000; | ||
|
||
private final JdbcTemplate jdbcTemplate; | ||
|
||
public void saveAllWithBatch(List<RecruitmentNotificationEmailLog> logs) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. update 를 하는 부분인데 transaction 안걸어도 될까여? |
||
|
||
// language=MySQL | ||
final String SQL = "INSERT INTO recruitment_notification_email_log " | ||
+ "(receiver_id, notification_id, send_success, created_at, modified_at) " | ||
+ "VALUES (?, ?, ?, now(), now())"; | ||
|
||
for (int start = 0; start < logs.size(); start += BATCH_SIZE) { | ||
int end = Math.min(start + BATCH_SIZE, logs.size()); | ||
List<RecruitmentNotificationEmailLog> chunk = logs.subList(start, end); | ||
|
||
jdbcTemplate.batchUpdate(SQL, new BatchPreparedStatementSetter() { | ||
@Override | ||
public void setValues(PreparedStatement ps, int i) throws SQLException { | ||
var log = chunk.get(i); | ||
ps.setLong(1, log.getReceiver().getId()); | ||
ps.setLong(2, log.getNotification().getId()); | ||
ps.setBoolean(3, log.getSendSuccess()); | ||
} | ||
|
||
@Override | ||
public int getBatchSize() { | ||
return chunk.size(); | ||
} | ||
}); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package org.cotato.csquiz.domain.recruitment.repository; | ||
|
||
import org.cotato.csquiz.domain.recruitment.entity.RecruitmentNotification; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
public interface RecruitmentNotificationRepository extends JpaRepository<RecruitmentNotification, Long> { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,20 @@ | ||
package org.cotato.csquiz.domain.recruitment.repository; | ||
|
||
import java.util.List; | ||
import org.cotato.csquiz.domain.recruitment.entity.RecruitmentNotificationRequester; | ||
import org.cotato.csquiz.domain.recruitment.enums.SendStatus; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.data.jpa.repository.Modifying; | ||
import org.springframework.data.jpa.repository.Query; | ||
import org.springframework.data.repository.query.Param; | ||
|
||
public interface RecruitmentNotificationRequesterRepository extends | ||
JpaRepository<RecruitmentNotificationRequester, Long> { | ||
boolean existsByEmailAndSendStatus(String recruitEmail, SendStatus sendStatus); | ||
|
||
List<RecruitmentNotificationRequester> findAllBySendStatusIn(List<SendStatus> status); | ||
|
||
@Modifying | ||
@Query("update RecruitmentNotificationRequester r set r.sendStatus = :status where r.id in :ids") | ||
void updateSendStatusByIds(@Param("status") SendStatus sendStatus, @Param("ids") List<Long> ids); | ||
Comment on lines
+17
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. update 쿼리인데 트랜잭션없어도 괜찮을까여? |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요건 왜 맨날 순서가 바뀌지 .. 둘이 쓰는 코드 포매터가 다른가