Skip to content
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

release 4.0.3 #288

Merged
merged 24 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
416840f
[feat]: 최종 합격 발송 메일링(첨부파일 포함) 및 retry 반영
BlackBean99 Sep 23, 2024
4173966
[feat]: EmailTemplate Static 주석 / rename class
BlackBean99 Sep 26, 2024
c492ed2
refactor: finalDiscussion 디폴트 값 수정
rlajm1203 Sep 26, 2024
3688272
Merge branch 'develop' into feature/BE-104
rlajm1203 Sep 26, 2024
762c9dc
feat: getApplicantsByYear 메소드 구현
rlajm1203 Sep 26, 2024
1b177bb
refactor: scheduled cron value 수정 및 템플릿 오타 수정
rlajm1203 Sep 26, 2024
b09bf2e
refactor: commons email value 들 수정
rlajm1203 Sep 26, 2024
e349983
refactor: finalDiscussionCron 필드 추가
rlajm1203 Sep 26, 2024
bca5c1c
refactor: ReadEmailTemplateExceptionDocs.class 주석 처리
rlajm1203 Sep 26, 2024
edfbe77
refactor: 따옴표 제거
rlajm1203 Sep 26, 2024
61d3f32
docs: 최종합격자 대상 에코노베이션 포트폴리오 pdf attachment
rlajm1203 Sep 26, 2024
1590d1e
refactor: iteration 횟수 수정
rlajm1203 Sep 27, 2024
58ca411
Merge pull request #276 from JNU-econovation/feature/BE-104
rlajm1203 Sep 27, 2024
c7671c0
refactor: attatchment가 없는 경우를 편별하는 로직 변경
rlajm1203 Sep 27, 2024
d16d35a
refactor: portfolio COPY 추가
rlajm1203 Sep 27, 2024
b0c2c36
Merge pull request #283 from JNU-econovation/feature/BE-104
rlajm1203 Sep 27, 2024
4dcf20e
refactor: portfolio.pdf 삭제
rlajm1203 Sep 27, 2024
ba41bca
refactor: attatchment 유무 체크 로직 수정
rlajm1203 Sep 27, 2024
d1d38a4
remove: 포트폴리오 삭제
rlajm1203 Sep 27, 2024
2374460
docs: 포트폴리오 이동
rlajm1203 Sep 27, 2024
e92eeff
Merge pull request #284 from JNU-econovation/feature/BE-104
rlajm1203 Sep 27, 2024
194b844
refactor: 포트폴리오 파일 경로 환경변수로 설정
rlajm1203 Sep 27, 2024
43640e1
refactor: 포트폴리오 파일 경로 환경변수로 설정
rlajm1203 Sep 27, 2024
4f4599e
Merge pull request #285 from JNU-econovation/feature/BE-104
rlajm1203 Sep 27, 2024
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 @@ -10,10 +10,12 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.filter.ForwardedHeaderFilter;

@SpringBootApplication
@RequiredArgsConstructor
@EnableScheduling
@ComponentScan(basePackages = {"com.econovation"})
@Slf4j
public class RecruitApplication implements ApplicationListener<ApplicationReadyEvent> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ public PageInfo getPageInfo(Integer year, Integer page, String searchKeyword) {
return new PageInfo(totalCount, page);
}

@Override
public List<MongoAnswer> getApplicantsByYear(Integer year) {
return answerAdaptor.findByYear(year);
}

@Transactional(readOnly = true)
public List<MongoAnswer> execute(
Integer page,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ List<Map<String, Object>> execute(
AnswersResponseDto search(Integer page, String searchKeyword);

List<GetApplicantsStatusResponse> getApplicantsStatus(Integer year, String sortType);

List<MongoAnswer> getApplicantsByYear(Integer year);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* 다음 기수 서류 합격자 관련 자동화 시 반영할 코드입니다. package
* com.econovation.recruit.api.email.handler; @Component @RequiredArgsConstructor @Slf4j public
* class ApplicantRegisterEventConfirmEmailHandler { private final CommonsEmailSender emailSender;
* private final CardLoadPort cardLoadPort; private final AnswerAdaptor
* answerAdaptor; @Value("${econovation.year}") private Integer year;
*
* <p>private final ApplicantQueryUseCase applicantQueryUseCase;
*
* <p>private final ExecutorService executor =
* Executors.newCachedThreadPool(); @Async @TransactionalEventListener( classes =
* ApplicantRegisterEvent.class, phase = TransactionPhase.AFTER_COMMIT) @Transactional(propagation =
* Propagation.REQUIRES_NEW) public void handle(EmailSendEvent event) throws IOException {
*
* <p>applicantQueryUseCase.getApplicantsByYear(year); EmailTemplateType emailTemplateType =
* event.getEmailTemplateType();
*
* <p>// Resource Loader setting classPath // String path = new File(".").getCanonicalPath(); // 현재
* 작업 디렉토리를 가져옴 // String dir = path + "/documentPassed.csv"; // CSVReader csvReader = new
* CSVReaderBuilder(new FileReader(dir)).withSkipLines(1).build(); try { csvReader.forEach( line ->
* CompletableFuture.runAsync( () -> { String template = event.getMessage(); switch
* (emailTemplateType) { case DOCUMENT_PASS: template = generatePassedTemplates(line, template);
* break; case DOCUMENT_FAIL: template = generateFailedTemplate(line, template); break; case
* INTERVIEW_PASS: template = generateInterviewPassedTemplate(line, template); break; case
* INTERVIEW_FAIL: template = generateInterviewFailureTemplate(line, template); break; default:
* log.error("이메일 발송 완료 : {}", emailTemplateType.name()); } emailSender.send( line[3], "에코노베이션 신입 모집
* 서류전형 결과 안내", template); }, executor)); } catch (Exception e) { // Transactional Outbox Pattern을
* 사용하면서 발생하는 예외를 잡아서 처리 log.error("서류 합격자 이메일 발송 실패"); } }
*/
/**
* 서류 합격자 이메일 템플릿
*
* @param line ( 번호, 이름, 합격여부, 이메일, 면접 날짜, 면접 시간, 오픈채팅방 링크, 오픈채팅방 입장 마감일)
*//*

private String generatePassedTemplates(String[] line, String template) {
return template.replace("%YEAR%", year.toString())
.replace("%NAME%", line[1]) // 이름
.replace("%DATE%", line[4]) // 면접 날짜
.replace("%TIME%", line[5]) // 면접 시간
.replace("%LINK%", line[6]) // 오픈채팅방 링크
.replace("%KAKAOTALK_ENTRANCE_ENDDATE%", line[4]); // 오픈채팅방 입장 마감일
}
*/
/**
* 탈락 지원자 이메일 템플릿
*
* @param line ( 번호,이름,합격 상태,메일 주소)
*//*

private String generateFailedTemplate(String[] line, String template) {
return template.replace("%NAME%", line[1]);
}

*/
/**
* 면접 합격자 이메일 템플릿
*
* @param line ( 번호, 이름, 합격여부, 이메일 오픈채팅방 입장 마감일), file ( 에코노베이션 포트폴리오 )
*//*

private String generateInterviewPassedTemplate(String[] line, String template) {
return template.replace("%YEAR%", year.toString()).replace("%NAME%", line[1]); // 이름
}

*/
/**
* 면접 탈락자 이메일 템플릿
*
* @param line ( 번호, 이름, 합격여부, 이메일)
*//*

private String generateInterviewFailureTemplate(String[] line, String template) {
return template.replace("%NAME%", line[1]);
}
}
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package com.econovation.recruit.api.email.service;


import com.econovation.recruit.api.applicant.usecase.ApplicantQueryUseCase;
import com.econovation.recruitdomain.domains.applicant.domain.MongoAnswer;
import com.econovation.recruitdomain.domains.applicant.domain.state.PassStates;
import com.econovation.recruitinfrastructure.apache.CommonsEmailSender;
import java.io.File;
import java.util.*;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Component
@Slf4j
@RequiredArgsConstructor
public class FinalEmailDiscussionEmailScheduler {
private final CommonsEmailSender emailSender;
private final ApplicantQueryUseCase applicantQueryUseCase;
private final Integer MAX_EMAIL_SEND_RETRY = 10;

@Value("${econovation.year}")
private Integer year;

private File attachment;

@Value("${econovation.file.path.portfolio}")
private String filePath;

@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 30000))
@SneakyThrows
@Async
@Scheduled(cron = "${econovation.recruit.period.finalDiscussionCron}", zone = "Asia/Seoul")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle() {
int startIndex = 0;
int batchSize = 14;
List<MongoAnswer> applicants = applicantQueryUseCase.getApplicantsByYear(year);
Queue<MongoAnswer> failQueue = new LinkedList<>();
Map<MongoAnswer, Integer> retryCounts = new HashMap<>(); // Map to track retry counts

int iteration = 0;
int limit = 1;
do {
List<MongoAnswer> batch = new ArrayList<>(batchSize);
for (MongoAnswer applicant : applicants) {
batch.add(applicant);
if (batch.size() == batchSize) {
processBatch(batch, failQueue, retryCounts);
batch.clear();
TimeUnit.MILLISECONDS.sleep(300);
}
}
if (!batch.isEmpty()) {
processBatch(batch, failQueue, retryCounts);
batch.clear();
TimeUnit.MILLISECONDS.sleep(300);
}
if (startIndex >= 10) break;
startIndex++;
iteration++;
} while (iteration<limit);
failOver(failQueue, retryCounts);
}

// 포트폴리오 파일을 가져오는 메서드
private File getPortfolioFile() {
// applicant에서 포트폴리오 파일 경로나 ID 등을 이용해 파일을 가져오는 로직
return new File(filePath);
}

private void processBatch(
List<MongoAnswer> batch,
Queue<MongoAnswer> failQueue,
Map<MongoAnswer, Integer> retryCounts) {
for (MongoAnswer applicant : batch) {
try {
// 이메일 템플릿 생성
String template = generateEmailTemplate(applicant);
attachment = getPortfolioFile();

// 이메일 발송 및 실패 처리
boolean result =
sendEmailWithRetry(applicant, template, attachment, retryCounts, failQueue);
if (!result) {
failQueue.add(applicant);
retryCounts.put(applicant, retryCounts.getOrDefault(applicant, 0) + 1);
}
} catch (Exception e) {
log.error(
"Email sending failed for {}: {}",
applicant.getQna().get("email").toString(),
e.getMessage());
failQueue.add(applicant);
retryCounts.put(applicant, retryCounts.getOrDefault(applicant, 0) + 1);
}
}
}

private void failOver(Queue<MongoAnswer> failQueue, Map<MongoAnswer, Integer> retryCounts)
throws InterruptedException {
while (!failQueue.isEmpty()) {
int queueSize = failQueue.size();
for (int i = 0; i < queueSize; i++) {
MongoAnswer applicant = failQueue.poll();
int retryCount = retryCounts.getOrDefault(applicant, 0);

if (retryCount >= MAX_EMAIL_SEND_RETRY) {
log.error("최대 10번 retry 실패시: {}", applicant.getQna().get("email").toString());
continue;
}

try {
// 이메일 템플릿 생성
String template = generateEmailTemplate(applicant);

// 이메일 발송 및 실패 처리
boolean result =
sendEmailWithRetry(
applicant, template, attachment, retryCounts, failQueue);
if (!result) {
retryCounts.put(applicant, retryCount + 1);
failQueue.add(applicant);
log.warn(
"Retry failed for email: {} (Attempt {})",
applicant.getQna().get("email").toString(),
retryCount + 1);
}
} catch (Exception e) {
log.error(
"Retry exception for email {}: {}",
applicant.getQna().get("email").toString(),
e.getMessage());
retryCounts.put(applicant, retryCount + 1);
failQueue.add(applicant);
}
}
TimeUnit.SECONDS.sleep(1);
}
}

// 이메일 템플릿 생성 메서드
private String generateEmailTemplate(MongoAnswer applicant) {
PassStates passState = applicant.getApplicantState().getPassStateToEnum();
String template = "";
switch (passState) {
case FINAL_PASSED:
template = generateFinalPassedTemplate(applicant);
break;
case FINAL_FAILED:
template = generateFinalFailedTemplate(applicant);
break;
default:
log.error("잘못된 상태 처리: {}", applicant.getId());
}
return template;
}

// 이메일 발송 및 실패 처리 메서드
private boolean sendEmailWithRetry(
MongoAnswer applicant,
String template,
File attachment,
Map<MongoAnswer, Integer> retryCounts,
Queue<MongoAnswer> failQueue) {
boolean result;
if (attachment.exists()) {
result =
emailSender.sendEmailWithAttachment(
applicant.getQna().get("email").toString(),
"에코노베이션 신입 모집 최종 결과 안내",
template,
attachment);
} else {
result =
emailSender.sendEmail(
applicant.getQna().get("email").toString(),
"에코노베이션 신입 모집 최종 결과 안내",
template);
log.error("attachment 가 첨부되지 않았습니다. file dir : " + attachment.getAbsolutePath());
}

if (!result) {
retryCounts.put(applicant, retryCounts.getOrDefault(applicant, 0) + 1);
failQueue.add(applicant);
}

return result;
}
/** 면접 합격자 이메일 템플릿 */
private String generateFinalPassedTemplate(MongoAnswer applicant) {
String template =
"<img alt='econo-3d-logo' width='114' height='143' style='color:transparent; margin:auto;' src='https://recruit.econovation.kr/images/econo-3d-logo.png'><br><br>안녕하세요, NAME님.<br><br>에코노베이션에 관심을 가지고 지원해 주셔서 감사합니다.<br><br>에코노베이션 28기 신입 모집에 최종 합격하신 것을 축하드립니다!<br><br>사전에 안내해 드린 대로 OT가 진행될 예정입니다.<br><br>OT는 대면으로 진행되며, 일정에 대해 잘 숙지하시고 반드시 참여해주시기를 바랍니다.<br><br>에코노베이션에 대한 소개를 담은 포트폴리오를 아래에 첨부하였으니 OT 시작 전 확인해주시기를 바랍니다.<br><br>에코노베이션은 다양한 프로젝트와 스터디에 GitHub을 사용하고 있으니 원활한 동아리 활동을 위해 OT 전 <b>꼭 Github에 가입해주시길 바랍니다.</b><br><br>메일 확인 후 참석 여부에 대한 회신 부탁드립니다. 예) 확인, 참석합니다.<br><br>--OT--<br><br><b>일시: 10월 2일 수요일 19:00 ~ 21:00<br><br>장소 : 전남대학교 정보전산원 1층 109호</b><br><br>";
return template.replace("NAME", applicant.getQna().get("name").toString());
}

/** 면접 탈락자 이메일 템플릿 ) */
private String generateFinalFailedTemplate(MongoAnswer applicant) {
String template =
"<img alt='econo-3d-logo' width='114' height='143' style='color:transparent; margin:auto;' src='https://recruit.econovation.kr/images/econo-3d-logo.png'><br><br>안녕하세요 NAME님. 전남대학교 IT 개발 동아리 에코노베이션입니다.<br><br>먼저 에코노베이션 28기 신입 모집에 관심을 가지고 지원해주셔서 진심으로 감사드립니다.<br><br>혹시 이번 모집 과정 중 저희가 의도치 않게 불편을 드린 점은 없었는지 여러모로 마음이 쓰입니다.아쉽게도 이번에는 좋은 결과를 전해드리지 못하게 되었습니다.<br><br>열정을 가지고 지원해 주신 모든 분과 함께할 수 있기를 바라고 있습니다만, 선발 규모 대비 많은 분이 지원해 주셔서 모든 분께 기회를 드릴 수 없었던 점 양해 부탁드립니다.<br><br>앞으로도 에코노베이션에 많은 관심을 가져주시기 바라며, 더 좋은 기회에 다시 만나 뵐 수 있기를 바라겠습니다.<br><br>감사합니다.";
return template.replace("NAME", applicant.getQna().get("name").toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.econovation.recruit.api.email_template.controller;

//import com.econovation.recruit.api.email_template.docs.ReadEmailTemplateExceptionDocs;
import com.econovation.recruit.api.email_template.dto.EmailTemplateRequestDto;
import com.econovation.recruit.api.email_template.usecase.EmailTemplateLoadUseCase;
import com.econovation.recruit.api.email_template.usecase.EmailTemplateRegisterUseCase;
import com.econovation.recruitcommon.annotation.ApiErrorExceptionsExample;
import com.econovation.recruitdomain.domains.email_template.domain.EmailTemplate;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "[1.0]. 지원서 API", description = "지원서 관련 API")
public class EmailTemplateController {

private final EmailTemplateRegisterUseCase emailTemplateRegisterUseCase;
private final EmailTemplateLoadUseCase emailTemplateLoadUseCase;

@Operation(summary = "이메일 템플릿 조회", description = "이메일 템플릿을 id로 조회합니다.")
// @ApiErrorExceptionsExample(ReadEmailTemplateExceptionDocs.class)
@GetMapping("/email-templates/{emailTemplateId}")
public ResponseEntity<EmailTemplate> getApplicantById(Long applicantId) {
return new ResponseEntity<>(emailTemplateLoadUseCase.findById(applicantId), HttpStatus.OK);
}

@Operation(summary = "이메일 템플릿 생성", description = "이메일 템플릿을 생성합니다.")
@PostMapping("/email-templates")
public ResponseEntity<EmailTemplate> createApplicant(
@RequestBody EmailTemplateRequestDto emailTemplateRequestDto) {
return new ResponseEntity<>(
emailTemplateRegisterUseCase.execute(emailTemplateRequestDto), HttpStatus.OK);
}
// @ApiErrorExceptionsExample(CreateEmailTemplateExceptionDocs.class)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.econovation.recruit.api.email_template.docs;

import com.econovation.recruitcommon.annotation.ExceptionDoc;
import com.econovation.recruitcommon.annotation.ExplainError;
import com.econovation.recruitcommon.exception.RecruitCodeException;
import com.econovation.recruitcommon.interfaces.SwaggerExampleExceptions;
import com.econovation.recruitdomain.domains.email_template.exception.EmailTemplateErrorCode;

@ExceptionDoc
public class CreateEmailTemplateExceptionDocs implements SwaggerExampleExceptions {
@ExplainError("메일 발송 템플릿_포맷_오류")
public RecruitCodeException 메일_발송_템플릿_포맷_오류 =
new RecruitCodeException(EmailTemplateErrorCode.EMAIL_TEMPLATE_INVALID_FORMAT);
}
Loading
Loading