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/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..7fc7315 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; @@ -92,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()); @@ -132,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/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; 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/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 diff --git a/src/main/resources/.gitkeep b/src/main/resources/.gitkeep new file mode 100644 index 0000000..e69de29 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