Skip to content

Commit

Permalink
Merge pull request #57 from 9oormthon-univ/52-feature-send-notificati…
Browse files Browse the repository at this point in the history
…on-email-upon-donation-completion

#52 - By Using Springboot mail Library Implement Sending Notification…
  • Loading branch information
hyeneung authored Dec 6, 2024
2 parents db8df01 + 67efc64 commit 24b0f83
Show file tree
Hide file tree
Showing 12 changed files with 290 additions and 12 deletions.
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

implementation 'org.springframework.boot:spring-boot-starter-mail'

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import com.example.mymoo.domain.donationusage.dto.request.DonationUsageCreateRequestDto;
import com.example.mymoo.domain.donationusage.dto.request.DonationUsageUpdateMessageRequestDto;
import com.example.mymoo.domain.donationusage.entity.DonationUsage;
import com.example.mymoo.domain.donationusage.service.DonationUsageService;
import com.example.mymoo.domain.email.EmailClient;
import com.example.mymoo.domain.email.dto.EmailSendDTO;
import com.example.mymoo.global.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand All @@ -12,18 +15,15 @@
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PatchMapping;
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;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/donation-usages")
@RequiredArgsConstructor
public class DonationUsageController {

private final DonationUsageService donationUsageService;
private final EmailClient emailClient;
@Operation(
summary = "[음식점] 후원 금액 사용 처리",
description = "음식점 계정에서 QR 코드 인식 시 해당 API를 호출합니다.",
Expand All @@ -38,8 +38,9 @@ public ResponseEntity<Void> useDonation(
@Valid @RequestBody DonationUsageCreateRequestDto donationUsageCreateRequestDto
) {
Long storeAccountId = userDetails.getAccountId();
donationUsageService.useDonation(storeAccountId, donationUsageCreateRequestDto);
// TODO - 알림 기능 추가
DonationUsage donationUsage = donationUsageService.useDonation(storeAccountId, donationUsageCreateRequestDto);
// 후원 완료 후 알림 메일 전송
emailClient.sendDonationUsageMail(EmailSendDTO.from(donationUsage));
return ResponseEntity
.status(HttpStatus.CREATED)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import com.example.mymoo.domain.donationusage.dto.request.DonationUsageCreateRequestDto;
import com.example.mymoo.domain.donationusage.dto.request.DonationUsageUpdateMessageRequestDto;
import com.example.mymoo.domain.donationusage.entity.DonationUsage;

public interface DonationUsageService {
void useDonation(
DonationUsage useDonation(
Long storeAccountId,
DonationUsageCreateRequestDto donationUsageCreateRequestDto
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class DonationUsageServiceImpl implements DonationUsageService {
private final StoreRepository storeRepository;

@Override
public void useDonation(
public DonationUsage useDonation(
final Long storeAccountId,
final DonationUsageCreateRequestDto donationUsageCreateRequestDto
) {
Expand Down Expand Up @@ -77,7 +77,7 @@ public void useDonation(
// store 계정의 point 증가. 향후 현금으로 바꿀 수 있음
storeUsingDonation.getAccount().chargePoint(donation.getPoint());

donationUsageRepository.save(
return donationUsageRepository.save(
DonationUsage.builder()
.child(child)
.donation(donation)
Expand Down
56 changes: 56 additions & 0 deletions src/main/java/com/example/mymoo/domain/email/EmailClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.example.mymoo.domain.email;

import com.example.mymoo.domain.donationusage.entity.DonationUsage;
import com.example.mymoo.domain.email.dto.EmailSendDTO;
import com.example.mymoo.domain.email.exception.EmailException;
import com.example.mymoo.domain.email.exception.EmailExceptionDetails;
import jakarta.annotation.PostConstruct;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Slf4j
@Service
@RequiredArgsConstructor
public class EmailClient {

private final JavaMailSender javaMailSender;
private final SpringTemplateEngine templateEngine;

public void sendDonationUsageMail(EmailSendDTO send){
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
try{
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setTo(send.getEmail());
mimeMessageHelper.setSubject("[마이무] 감사합니다! 당신의 후원이 사용되었습니다!");
mimeMessageHelper.setText(setContext("email", send), true);
javaMailSender.send(mimeMessage);
log.info("이메일 전송 성공");
} catch (MessagingException e){
log.info("이메일 전송 실패");
throw new EmailException(EmailExceptionDetails.EMAIL_SEND_FAILED);
}
}

public String setContext(String type, EmailSendDTO send){
Context context = new Context();
context.setVariable("storeName", send.getStoreName());
context.setVariable("usedPrice", send.getUsedPrice());
context.setVariable("childName", send.getChildName());
context.setVariable("usedTime", send.getUsedTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
context.setVariable("donatedTime", send.getDonatedTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return templateEngine.process(type, context);
}


}
35 changes: 35 additions & 0 deletions src/main/java/com/example/mymoo/domain/email/dto/EmailSendDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.example.mymoo.domain.email.dto;

import com.example.mymoo.domain.donationusage.entity.DonationUsage;
import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

@Data @Builder
public class EmailSendDTO {
private String email;
private String storeName;
private Long usedPrice;
private String childName;
private LocalDateTime usedTime;
private LocalDateTime donatedTime;

public static EmailSendDTO from(DonationUsage donationUsage){

String email = donationUsage.getDonation().getAccount().getEmail();

if (email.contains("_")){
email = email.substring(email.lastIndexOf("_")+1);
}

return EmailSendDTO.builder()
.email(email)
.storeName(donationUsage.getDonation().getStore().getName())
.usedPrice(donationUsage.getDonation().getPoint())
.childName(donationUsage.getChild().getAccount().getNickname())
.usedTime(donationUsage.getModifiedAt())
.donatedTime(donationUsage.getCreatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.mymoo.domain.email.exception;

import com.example.mymoo.global.exception.CustomException;

public class EmailException extends CustomException {
public EmailException(EmailExceptionDetails paymentExceptionDetails){
super(paymentExceptionDetails);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.mymoo.domain.email.exception;

import com.example.mymoo.global.exception.ExceptionDetails;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum EmailExceptionDetails implements ExceptionDetails {
// 가게 id가 store 테이블에 존재하지 않을 때
EMAIL_SEND_FAILED(HttpStatus.BAD_REQUEST, "이메일 전송이 실패했습니다."),
;

private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
public enum StoreExceptionDetails implements ExceptionDetails {
// 가게 id가 store 테이블에 존재하지 않을 때
STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 가게 id입니다."),
NOT_ENOUGH_STORE_POINT(HttpStatus.NOT_FOUND, "후원된 포인트가 부족합니다."),
NOT_ENOUGH_STORE_POINT(HttpStatus.BAD_REQUEST, "후원된 포인트가 부족합니다."),
QUERY_PARAMETER_INVALID(HttpStatus.BAD_REQUEST, "쿼리파라미터 변수 입력이 잘못됐습니다."),
;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.example.mymoo.global.auth.controller;

import com.example.mymoo.domain.donationusage.entity.DonationUsage;
import com.example.mymoo.domain.email.EmailClient;
import com.example.mymoo.domain.email.dto.EmailSendDTO;
import com.example.mymoo.global.auth.dto.request.AccountLoginRequestDto;
import com.example.mymoo.global.auth.dto.request.TokenRefreshRequestDto;
import com.example.mymoo.global.auth.dto.response.AccountLoginResponseDto;
Expand Down
67 changes: 67 additions & 0 deletions src/main/java/com/example/mymoo/global/config/EmailConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.example.mymoo.global.config;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import java.util.Properties;

@Configuration
@RequiredArgsConstructor
public class EmailConfig {
private static final String MAIL_SMTP_AUTH = "mail.smtp.auth";
private static final String MAIL_DEBUG = "mail.smtp.debug";
private static final String MAIL_CONNECTION_TIMEOUT = "mail.smtp.connectiontimeout";
private static final String MAIL_SMTP_STARTTLS_ENABLE = "mail.smtp.starttls.enable";

// SMTP 서버
@Value("${spring.mail.host}")
private String host;

// 계정
@Value("${spring.mail.username}")
private String username;

// 비밀번호
@Value("${spring.mail.password}")
private String password;

// 포트번호
@Value("${spring.mail.port}")
private int port;

@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;

// @Value("${spring.mail.properties.mail.smtp.debug}")
// private boolean debug;

@Value("${spring.mail.properties.mail.smtp.timeout}")
private int connectionTimeout;

@Value("${spring.mail.properties.mail.starttls.enable}")
private boolean startTlsEnable;

@Bean
public JavaMailSender javaMailService() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setHost(host);
javaMailSender.setUsername(username);
javaMailSender.setPassword(password);
javaMailSender.setPort(port);

Properties properties = javaMailSender.getJavaMailProperties();
properties.put(MAIL_SMTP_AUTH, auth);
// properties.put(MAIL_DEBUG, debug);
properties.put(MAIL_CONNECTION_TIMEOUT, connectionTimeout);
properties.put(MAIL_SMTP_STARTTLS_ENABLE, startTlsEnable);

javaMailSender.setJavaMailProperties(properties);
javaMailSender.setDefaultEncoding("UTF-8");

return javaMailSender;
}
}
85 changes: 85 additions & 0 deletions src/main/resources/templates/email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>&#54980;&#50896; &#49324;&#50857; &#45236;&#50669; &#50504;&#45236;</title>
<link th:href="@{css/email.css}" rel="stylesheet">
</head>
<body style="font-family: 'Pretendard', sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;">
<div class="receipt-container" style="background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
width: 360px;
padding: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);">
<div class="receipt-header" style="text-align: center;
margin-bottom: 20px;">
<img width="107" alt="KakaoTalk_20241206_203440105" src="https://github.com/user-attachments/assets/576bd699-c42e-4138-b128-88a44f1f2f05">
<h1 class="receipt-title" style="font-size: 18px;
font-weight: bold;
color: #333;
margin: 0;">&#47560;&#51060;&#47924; &#44208;&#51228; &#45236;&#50669; &#50504;&#45236;</h1>
</div>
<div class="receipt-body" style="margin-top: 20px;">
<div class="receipt-item" style="display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #ddd;
font-size: 14px;">
<span class="item-label" style="color: #555;">&#49324;&#50857;&#46108; &#44032;&#44172;</span>
<span class="item-value" th:text="${storeName}" style="color: #000;
font-weight: bold;">15,000&#50896;</span>
</div>
<div class="receipt-item" style="display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #ddd;
font-size: 14px;">
<span class="item-label" style="color: #555;">&#44552;&#50529;</span>
<span class="item-value" th:text="${usedPrice}" style="color: #000;
font-weight: bold;">15,000&#50896;</span>
</div>
<div class="receipt-item" style="display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #ddd;
font-size: 14px;">
<span class="item-label" style="color: #555;">&#49324;&#50857;&#54620; &#50500;&#46041;</span>
<span class="item-value" th:text="${childName}" style="color: #000;
font-weight: bold;">&#55148;&#47581;&#51060;</span>
</div>
<div class="receipt-item" style="display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #ddd;
font-size: 14px;">
<span class="item-label" style="color: #555;">&#49324;&#50857;&#54620; &#49884;&#44036;</span>
<span class="item-value" th:text="${usedTime}" style="color: #000;
font-weight: bold;">2024-11-12 13:30</span>
</div>
<div class="receipt-item" style="display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #ddd;
font-size: 14px;">
<span class="item-label" style="color: #555;">&#54980;&#50896;&#54620; &#49884;&#44036;</span>
<span class="item-value" th:text="${donatedTime}" style="color: #000;
font-weight: bold;">2024-12-05 14:30</span>
</div>
</div>
</div>
</body>
</html>

0 comments on commit 24b0f83

Please sign in to comment.