From 9c2bc5abc81b20975568242023094b07e698abf3 Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Fri, 25 Jul 2025 09:55:04 +0900 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20GATEWAY=5FTIMEOUT=20ErrorCode=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AWS EC2 성능상의 이슈로 통신이 원활하게 이루어지지 않을 경우에 대비한 에러코드 - 로그인 API에서 테스트용으로 추가 --- src/main/java/com/writon/admin/global/error/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/writon/admin/global/error/ErrorCode.java b/src/main/java/com/writon/admin/global/error/ErrorCode.java index 4d232e3..10219e7 100644 --- a/src/main/java/com/writon/admin/global/error/ErrorCode.java +++ b/src/main/java/com/writon/admin/global/error/ErrorCode.java @@ -17,6 +17,7 @@ public enum ErrorCode { METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "405", "허용되지 않은 메소드입니다"), // 405 Method Not Allowed CONFLICT(HttpStatus.CONFLICT, "409", "이미 가입한 사용자입니다"), // 409 Conflict INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "500", "서버에 오류가 발생하였습니다"), // 500 Internal Server Error + GATEWAY_TIMEOUT_ERROR(HttpStatus.GATEWAY_TIMEOUT, "504", "연결 시간을 초과하였습니다"), // 503 Gateway Timeout ETC_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "0314", "사용자 지정 오류"), // auth From 92aa18bf9ba69077aa7348063be7341e4a14e763 Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:09:20 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/writon/admin/domain/controller/AuthController.java | 3 --- .../java/com/writon/admin/domain/service/AuthService.java | 1 - .../com/writon/admin/domain/service/ChallengeService.java | 4 ---- .../com/writon/admin/domain/service/OrganizationService.java | 1 - 4 files changed, 9 deletions(-) diff --git a/src/main/java/com/writon/admin/domain/controller/AuthController.java b/src/main/java/com/writon/admin/domain/controller/AuthController.java index 90ccff6..e4deeca 100644 --- a/src/main/java/com/writon/admin/domain/controller/AuthController.java +++ b/src/main/java/com/writon/admin/domain/controller/AuthController.java @@ -1,16 +1,13 @@ package com.writon.admin.domain.controller; import com.writon.admin.domain.dto.request.auth.LoginRequestDto; -import com.writon.admin.domain.dto.request.auth.ReissueRequestDto; import com.writon.admin.domain.dto.request.auth.SignUpRequestDto; import com.writon.admin.domain.dto.response.auth.LoginResponseDto; -import com.writon.admin.domain.dto.response.auth.ReissueResponseDto; import com.writon.admin.domain.dto.response.auth.SignUpResponseDto; import com.writon.admin.domain.dto.wrapper.auth.LoginResponseWrapper; import com.writon.admin.domain.service.AuthService; import com.writon.admin.global.config.auth.CookieProvider; import com.writon.admin.global.response.SuccessDto; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/com/writon/admin/domain/service/AuthService.java b/src/main/java/com/writon/admin/domain/service/AuthService.java index 0082937..90f221c 100644 --- a/src/main/java/com/writon/admin/domain/service/AuthService.java +++ b/src/main/java/com/writon/admin/domain/service/AuthService.java @@ -27,7 +27,6 @@ import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/writon/admin/domain/service/ChallengeService.java b/src/main/java/com/writon/admin/domain/service/ChallengeService.java index 31643f0..5bf3fbd 100644 --- a/src/main/java/com/writon/admin/domain/service/ChallengeService.java +++ b/src/main/java/com/writon/admin/domain/service/ChallengeService.java @@ -30,13 +30,9 @@ import com.writon.admin.global.error.ErrorCode; import java.time.LocalDate; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import java.util.LinkedHashMap; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/writon/admin/domain/service/OrganizationService.java b/src/main/java/com/writon/admin/domain/service/OrganizationService.java index 008d005..e900bef 100644 --- a/src/main/java/com/writon/admin/domain/service/OrganizationService.java +++ b/src/main/java/com/writon/admin/domain/service/OrganizationService.java @@ -7,7 +7,6 @@ import com.writon.admin.domain.entity.organization.AdminUser; import com.writon.admin.domain.entity.organization.Organization; import com.writon.admin.domain.entity.organization.Position; -import com.writon.admin.domain.repository.organization.AdminUserRepository; import com.writon.admin.domain.repository.organization.OrganizationRepository; import com.writon.admin.domain.repository.organization.PositionRepository; import com.writon.admin.domain.util.TokenUtil; From 2341c1ef09268fa1e5a816283f933b9460541618 Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:09:54 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20Brevo=20=EC=82=AC=EC=9A=A9=ED=95=B4?= =?UTF-8?q?=EC=84=9C=20=EB=A9=94=EC=9D=BC=20=EC=A0=84=EC=86=A1=20=EC=86=8D?= =?UTF-8?q?=EB=8F=84=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 방식: Google SMTP 방식을 사용해 개당 메일을 보내게 되어 전송 속도가 오래 걸림 - 변경 방식: Brevo 방식으로 변경해 전송 속도 개선 - 이전 속도: 5.18s (메일의 개수에 정비레해서 전송 시간이 올라감) - 변경 속도: 507.20ms (전송 프로토콜 갖추는데 오래걸리고 메일의 개수가 늘어나도 전송 속도는 조금씩 올라감) --- build.gradle | 8 +- .../domain/service/ChallengeService.java | 12 ++- .../admin/domain/service/EmailService.java | 100 ++++++++++-------- .../domain/service/ParticipationService.java | 10 +- .../resources/templates/participate_card.html | 38 ------- 5 files changed, 74 insertions(+), 94 deletions(-) delete mode 100644 src/main/resources/templates/participate_card.html diff --git a/build.gradle b/build.gradle index 7b51184..9bf22d4 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,7 @@ java { repositories { mavenCentral() + maven { url 'https://jitpack.io' } } dependencies { @@ -44,10 +45,9 @@ dependencies { implementation 'io.awspring.cloud:spring-cloud-aws-s3:3.1.0' implementation 'javax.xml.bind:jaxb-api:2.3.1' - // mail - implementation 'org.springframework.boot:spring-boot-starter-mail' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // option - implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' // option + // brevo + implementation 'com.sendinblue:sib-api-v3-sdk:7.0.0' + // lombok implementation 'org.projectlombok:lombok' diff --git a/src/main/java/com/writon/admin/domain/service/ChallengeService.java b/src/main/java/com/writon/admin/domain/service/ChallengeService.java index 5bf3fbd..7fc7315 100644 --- a/src/main/java/com/writon/admin/domain/service/ChallengeService.java +++ b/src/main/java/com/writon/admin/domain/service/ChallengeService.java @@ -88,10 +88,12 @@ public CreateChallengeResponseDto createChallenge(CreateChallengeRequestDto requ } // 5. 이메일 전송 & 정보 저장 - for (String email : requestDto.getEmailList()) { - emailService.sendEmail(challenge, email); - emailRepository.save(new Email(email, challenge)); - } + emailService.sendEmail(challenge, requestDto.getEmailList()); + + List emailEntities = requestDto.getEmailList().stream() + .map(email -> new Email(email, challenge)) + .collect(Collectors.toList()); + emailRepository.saveAll(emailEntities); // 6. Response 생성 List challenges = challengeRepository.findByOrganizationId(organization.getId()); @@ -128,7 +130,7 @@ public List getDashboard(Long challengeId) { for (UserChallenge userChallenge : userChallengeList) { List statusList = new ArrayList<>(); List userTemplateList = userTemplateRepository.findByUserChallengeId( - userChallenge.getId()); + userChallenge.getId()); for (ChallengeDay challengeDay : challengeDayList) { // 참여여부 확인과정 diff --git a/src/main/java/com/writon/admin/domain/service/EmailService.java b/src/main/java/com/writon/admin/domain/service/EmailService.java index a8c4272..14c8044 100644 --- a/src/main/java/com/writon/admin/domain/service/EmailService.java +++ b/src/main/java/com/writon/admin/domain/service/EmailService.java @@ -5,70 +5,84 @@ import com.writon.admin.domain.util.TokenUtil; import com.writon.admin.global.error.CustomException; import com.writon.admin.global.error.ErrorCode; -import jakarta.mail.internet.MimeMessage; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import org.thymeleaf.context.Context; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.MimeMessageHelper; -import org.springframework.scheduling.annotation.Async; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.thymeleaf.spring6.SpringTemplateEngine; +import sendinblue.ApiClient; +import sendinblue.ApiException; +import sendinblue.Configuration; +import sendinblue.auth.ApiKeyAuth; +import sibApi.TransactionalEmailsApi; +import sibModel.CreateSmtpEmail; +import sibModel.SendSmtpEmail; +import sibModel.SendSmtpEmailMessageVersions; +import sibModel.SendSmtpEmailTo1; @Service @RequiredArgsConstructor @Slf4j public class EmailService { - private final JavaMailSender javaMailSender; - private final SpringTemplateEngine templateEngine; private final TokenUtil tokenUtil; - @Async - public void sendEmail(Challenge challenge, String email) { - MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + @Value("${email.apiKey}") + private String BREVO_API_KEY; + + @Value("${email.templateId}") + private Long BREVO_TEMPLATE_ID; + + public void sendEmail(Challenge challenge, List emailList) { Organization organization = tokenUtil.getOrganization(); + String baseUrl = "https://www.writon.co.kr/login"; + String link = String.format( + "%s?organization=%s&challengeId=%s", baseUrl, + encodeURIComponent(organization.getName()), + encodeURIComponent(String.valueOf(challenge.getId())) + ); - try { - MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8"); - mimeMessageHelper.setTo(email); - mimeMessageHelper.setSubject(String.format( - "[Writon] %s의 챌린지에 참여해보세요", - organization.getName() - )); // 메일 제목 - mimeMessageHelper.setText( - setContext(organization.getName(), challenge.getName(), challenge.getId(), email), - true - ); // 메일 본문 내용, HTML 여부 - javaMailSender.send(mimeMessage); + ApiClient defaultClient = Configuration.getDefaultApiClient(); - log.info("Succeeded to send Email"); - } catch (Exception e) { - log.info("Failed to send Email"); - throw new CustomException(ErrorCode.EMAIL_SEND_FAILED); - } - } + ApiKeyAuth apiKey = (ApiKeyAuth) defaultClient.getAuthentication("api-key"); + apiKey.setApiKey(BREVO_API_KEY); - //thymeleaf를 통한 html 적용 - public String setContext(String organization, String challenge, Long challengeId, String email) { - Context context = new Context(); - context.setVariable("organization", organization); - context.setVariable("challenge", challenge); - context.setVariable("email", email); - context.setVariable("challengeId", challengeId); + // 수신자 리스트 구성 + List messageVersions = new ArrayList<>(); - String baseUrl = "https://www.writon.co.kr/login"; - String link = String.format("%s?organization=%s&challengeId=%s", baseUrl, - encodeURIComponent(organization), - encodeURIComponent(String.valueOf(challengeId))); - context.setVariable("link", link); + for (String email : emailList) { + messageVersions.add(new SendSmtpEmailMessageVersions() + .to(List.of(new SendSmtpEmailTo1().email(email))) + .params(Map.of( + "ORGANIZATION", organization.getName(), + "CHALLENGE", challenge.getName(), + "EMAIL", email, + "LINK", link + ))); + } - return templateEngine.process("participate_card", context); + TransactionalEmailsApi apiInstance = new TransactionalEmailsApi(); + SendSmtpEmail sendSmtpEmail = new SendSmtpEmail() + // 템플릿 종류 + .templateId(BREVO_TEMPLATE_ID) + // 동적 param값 설정 + .messageVersions(messageVersions); + + try { + CreateSmtpEmail result = apiInstance.sendTransacEmail(sendSmtpEmail); + log.info("Succeded to send Email: {}", result); + } catch (ApiException e) { + log.error("Failed to send Email"); + throw new CustomException(ErrorCode.EMAIL_SEND_FAILED); + } } private String encodeURIComponent(String value) { return URLEncoder.encode(value, StandardCharsets.UTF_8); } -} + +} \ No newline at end of file diff --git a/src/main/java/com/writon/admin/domain/service/ParticipationService.java b/src/main/java/com/writon/admin/domain/service/ParticipationService.java index f6da23e..5dc5259 100644 --- a/src/main/java/com/writon/admin/domain/service/ParticipationService.java +++ b/src/main/java/com/writon/admin/domain/service/ParticipationService.java @@ -116,10 +116,12 @@ public List participate(Long challengeId, List emailList) { Challenge challenge = challengeRepository.findById(challengeId) .orElseThrow(() -> new CustomException(ErrorCode.CHALLENGE_NOT_FOUND)); - for (String email : emailList) { - emailService.sendEmail(challenge, email); - emailRepository.save(new Email(email, challenge)); - } + emailService.sendEmail(challenge, emailList); + + List emailEntities = emailList.stream() + .map(email -> new Email(email, challenge)) + .collect(Collectors.toList()); + emailRepository.saveAll(emailEntities); List sendedEmailList = emailRepository.findByChallengeId(challengeId); if (sendedEmailList.isEmpty()) { diff --git a/src/main/resources/templates/participate_card.html b/src/main/resources/templates/participate_card.html deleted file mode 100644 index fe503e2..0000000 --- a/src/main/resources/templates/participate_card.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - -
- Your Image -
-

- [[${organization}]]에서 [[${email}]]님을
- [[${challenge}]] 챌린지로 초대하였습니다
-

-
- - Your Button Image - -
- - - \ No newline at end of file From d767ec846c5374c8ccabef5d65b965e2f9162198 Mon Sep 17 00:00:00 2001 From: seungzzok <123801984+seungzzok@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:29:32 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20resources=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=EC=97=90=20Github=20=EC=9D=B8=EC=8B=9D=EC=9A=A9=20=EB=8D=94?= =?UTF-8?q?=EB=AF=B8=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/.gitkeep diff --git a/src/main/resources/.gitkeep b/src/main/resources/.gitkeep new file mode 100644 index 0000000..e69de29