Skip to content

Commit

Permalink
Merge pull request #288 from JNU-econovation/develop
Browse files Browse the repository at this point in the history
release 4.0.3
  • Loading branch information
rlajm1203 authored Sep 29, 2024
2 parents c1d0c10 + 4f4599e commit 316912b
Show file tree
Hide file tree
Showing 30 changed files with 805 additions and 5 deletions.
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

0 comments on commit 316912b

Please sign in to comment.