diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 41f1afb..7c8fae9 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -20,6 +20,7 @@ env: FCM_PROJECT_ID: ${{ secrets.FCM_PROJECT_ID }} AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }} permissions: contents: read diff --git a/.gitignore b/.gitignore index 653ea46..61c2862 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ logs/*.log .env fluent-bit/fluent-bit.conf fluent-bit/fluent-bit.yaml + +src/main/resources/key/AuthKey_FH4V72Y38Q.p8 \ No newline at end of file diff --git a/build.gradle b/build.gradle index 91fc4f4..ff45e8d 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation "org.springframework.retry:spring-retry" implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-mail' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/java/earlybird/earlybird/email/SendEmailService.java b/src/main/java/earlybird/earlybird/email/SendEmailService.java new file mode 100644 index 0000000..307abc5 --- /dev/null +++ b/src/main/java/earlybird/earlybird/email/SendEmailService.java @@ -0,0 +1,78 @@ +package earlybird.earlybird.email; + +import static earlybird.earlybird.promotion.email.entity.PromotionEmailMessageType.*; + +import earlybird.earlybird.common.util.LocalDateTimeUtil; +import earlybird.earlybird.promotion.email.entity.PromotionEmailMessageType; +import earlybird.earlybird.promotion.email.entity.PromotionEmailVerification; + +import jakarta.mail.Message; +import jakarta.mail.internet.MimeMessage; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Slf4j +@RequiredArgsConstructor +@Service +public class SendEmailService { + + private final JavaMailSender javaMailSender; + + @Retryable(maxAttempts = 5, backoff = @Backoff(delay = 1000)) + @Async + public void send( + PromotionEmailVerification promotionEmailVerification, + PromotionEmailMessageType promotionEmailMessageType) { + try { + MimeMessage message = javaMailSender.createMimeMessage(); + + message.addRecipients(Message.RecipientType.TO, promotionEmailVerification.getEmail()); + message.setFrom("earlybirdteam2024@gmail.com"); + + message.setSubject(promotionEmailMessageType.getTitle()); + + String verificationUrl = getVerificationUrl(promotionEmailVerification); + + String messageText = getMessageText(promotionEmailMessageType, verificationUrl); + + message.setText(messageText, "utf-8", "html"); + + javaMailSender.send(message); + promotionEmailVerification.setSentAt(LocalDateTimeUtil.getLocalDateTimeNow()); + + log.info( + "Promotion code email sent successfully to: {}", + promotionEmailVerification.getEmail()); + + } catch (Exception e) { + log.error( + "Failed to send promotion code email to: {}", + promotionEmailVerification.getEmail(), + e); + throw new RuntimeException("Failed to send email", e); + } + } + + private static String getMessageText( + PromotionEmailMessageType promotionEmailMessageType, String verificationUrl) { + + if (promotionEmailMessageType.equals(BERKELEY_6_MONTH_FREE)) + return String.format( + promotionEmailMessageType.getMessageText(), verificationUrl, verificationUrl); + else throw new IllegalArgumentException(); + } + + private String getVerificationUrl(PromotionEmailVerification promotionEmailVerification) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("https://earlybirdteam.com/promotion/apple/univ/email?code="); + stringBuilder.append(promotionEmailVerification.getPromotionUrlUuid().getUuid()); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/earlybird/earlybird/error/ErrorCode.java b/src/main/java/earlybird/earlybird/error/ErrorCode.java index 5435f7b..f8121e9 100644 --- a/src/main/java/earlybird/earlybird/error/ErrorCode.java +++ b/src/main/java/earlybird/earlybird/error/ErrorCode.java @@ -21,7 +21,12 @@ public enum ErrorCode { HttpStatus.BAD_REQUEST, "요청한 알림 ID에 해당하는 디바이스 토큰과 요청한 디바이스 토큰이 일치하지 않습니다."), APPOINTMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 약속입니다."), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 유저입니다."), - DELETED_APPOINTMENT_EXCEPTION(HttpStatus.NOT_FOUND, "삭제된 일정입니다."); + DELETED_APPOINTMENT_EXCEPTION(HttpStatus.NOT_FOUND, "삭제된 일정입니다."), + APPLE_PROMOTION_URL_LIST_IS_EMPTY_EXCEPTION( + HttpStatus.INTERNAL_SERVER_ERROR, "발급 가능한 애플 프로모션 URL이 없습니다."), + INVALID_PROMOTION_EMAIL_EXCEPTION(HttpStatus.BAD_REQUEST, "프로모션 대상 이메일이 아닙니다."), + PROMOTION_EMAIL_DOMAIN_NAME_IS_ALREADY_EXISTS_EXCEPTION( + HttpStatus.BAD_REQUEST, "프로모션 이메일 도메인이 이미 존재합니다."); private final HttpStatus status; private final String message; diff --git a/src/main/java/earlybird/earlybird/error/exception/ApplePromotionUrlListIsEmptyException.java b/src/main/java/earlybird/earlybird/error/exception/ApplePromotionUrlListIsEmptyException.java new file mode 100644 index 0000000..9eb564f --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/ApplePromotionUrlListIsEmptyException.java @@ -0,0 +1,9 @@ +package earlybird.earlybird.error.exception; + +import earlybird.earlybird.error.ErrorCode; + +public class ApplePromotionUrlListIsEmptyException extends BusinessBaseException { + public ApplePromotionUrlListIsEmptyException() { + super(ErrorCode.APPLE_PROMOTION_URL_LIST_IS_EMPTY_EXCEPTION); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/InvalidPromotionEmailException.java b/src/main/java/earlybird/earlybird/error/exception/InvalidPromotionEmailException.java new file mode 100644 index 0000000..86a5d3c --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/InvalidPromotionEmailException.java @@ -0,0 +1,9 @@ +package earlybird.earlybird.error.exception; + +import earlybird.earlybird.error.ErrorCode; + +public class InvalidPromotionEmailException extends BusinessBaseException { + public InvalidPromotionEmailException() { + super(ErrorCode.INVALID_PROMOTION_EMAIL_EXCEPTION); + } +} diff --git a/src/main/java/earlybird/earlybird/error/exception/PromotionEmailDomainNameIsAlreadyExistsException.java b/src/main/java/earlybird/earlybird/error/exception/PromotionEmailDomainNameIsAlreadyExistsException.java new file mode 100644 index 0000000..f3261aa --- /dev/null +++ b/src/main/java/earlybird/earlybird/error/exception/PromotionEmailDomainNameIsAlreadyExistsException.java @@ -0,0 +1,9 @@ +package earlybird.earlybird.error.exception; + +import static earlybird.earlybird.error.ErrorCode.PROMOTION_EMAIL_DOMAIN_NAME_IS_ALREADY_EXISTS_EXCEPTION; + +public class PromotionEmailDomainNameIsAlreadyExistsException extends BusinessBaseException { + public PromotionEmailDomainNameIsAlreadyExistsException() { + super(PROMOTION_EMAIL_DOMAIN_NAME_IS_ALREADY_EXISTS_EXCEPTION); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/apple/ApplePromotionUrl.java b/src/main/java/earlybird/earlybird/promotion/apple/ApplePromotionUrl.java new file mode 100644 index 0000000..21eb50b --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/apple/ApplePromotionUrl.java @@ -0,0 +1,46 @@ +package earlybird.earlybird.promotion.apple; + +import earlybird.earlybird.common.BaseTimeEntity; +import earlybird.earlybird.promotion.entity.PromotionCampaign; + +import jakarta.persistence.*; + +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@Table(name = "apple_promotion_urls") +@Entity +public class ApplePromotionUrl extends BaseTimeEntity { + + @Column(name = "apple_promotion_urls_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + // 애플에서 제공하는 프로모션 URL + @Column(name = "apple_promotion_urls_url", nullable = false, unique = true) + private String url; + + // 설명 + @Column(name = "apple_promotion_urls_description") + private String description; + + // URL 발급 여부 + // 발급된 URL은 다시 사용하면 안 됨 + @Column(name = "apple_promotion_urls_is_used", nullable = false) + private Boolean isUsed; + + // 프로모션 URL 만료 시간 + @Column(name = "apple_promotion_urls_expired_at", nullable = false) + private LocalDateTime expiredAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "promotion_campaigns_id", nullable = false) + private PromotionCampaign promotionCampaign; + + public void setUsed() { + this.isUsed = true; + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/apple/ApplePromotionUrlRepository.java b/src/main/java/earlybird/earlybird/promotion/apple/ApplePromotionUrlRepository.java new file mode 100644 index 0000000..4e322ef --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/apple/ApplePromotionUrlRepository.java @@ -0,0 +1,17 @@ +package earlybird.earlybird.promotion.apple; + +import jakarta.persistence.LockModeType; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; + +import java.util.List; +import java.util.Optional; + +public interface ApplePromotionUrlRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + List findAllByIsUsedFalseAndPromotionCampaignId(Long promotionCampaignId); + + Optional findByUrl(String url); +} diff --git a/src/main/java/earlybird/earlybird/promotion/apple/service/GetApplePromotionUrlService.java b/src/main/java/earlybird/earlybird/promotion/apple/service/GetApplePromotionUrlService.java new file mode 100644 index 0000000..6ce5cf6 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/apple/service/GetApplePromotionUrlService.java @@ -0,0 +1,87 @@ +package earlybird.earlybird.promotion.apple.service; + +import earlybird.earlybird.common.util.LocalDateTimeUtil; +import earlybird.earlybird.error.exception.ApplePromotionUrlListIsEmptyException; +import earlybird.earlybird.promotion.apple.ApplePromotionUrl; +import earlybird.earlybird.promotion.apple.ApplePromotionUrlRepository; +import earlybird.earlybird.promotion.apple.service.response.GetApplePromotionUrlServiceResponse; +import earlybird.earlybird.promotion.email.entity.PromotionEmailVerification; +import earlybird.earlybird.promotion.email.repository.PromotionEmailVerificationRepository; +import earlybird.earlybird.promotion.entity.PromotionUrlUuid; +import earlybird.earlybird.promotion.repository.PromotionUrlUuidRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.UUID; + +@RequiredArgsConstructor +@Service +public class GetApplePromotionUrlService { + + private final ApplePromotionUrlRepository applePromotionUrlRepository; + private final PromotionEmailVerificationRepository promotionEmailVerificationRepository; + private final PromotionUrlUuidRepository promotionUrlUuidRepository; + + @Transactional + public GetApplePromotionUrlServiceResponse getPromotionUrl(Long promotionCampaignId) { + + List urls = + applePromotionUrlRepository.findAllByIsUsedFalseAndPromotionCampaignId( + promotionCampaignId); + + if (urls.isEmpty()) { + throw new ApplePromotionUrlListIsEmptyException(); + } + + ApplePromotionUrl promotionUrl = + urls.stream() + .filter( + url -> + url.getExpiredAt() + .isAfter(LocalDateTimeUtil.getLocalDateTimeNow())) + .findFirst() + .orElseThrow(ApplePromotionUrlListIsEmptyException::new); + + promotionUrl.setUsed(); + return new GetApplePromotionUrlServiceResponse(promotionUrl.getUrl()); + } + + @Transactional + public GetApplePromotionUrlServiceResponse getPromotionUrlByUuid(String promotionCodeUuid) { + + UUID uuid = UUID.fromString(promotionCodeUuid); + + PromotionUrlUuid promotionUrlUuid = + promotionUrlUuidRepository.findByUuid(uuid).orElseThrow(); + + PromotionEmailVerification promotionEmailVerification = + promotionEmailVerificationRepository + .findByPromotionUrlUuid(promotionUrlUuid) + .orElseThrow(); + + String promotionUrl = + promotionEmailVerification.getEmailPromotionCodeIssuance().getPromotionCode(); + + if (!promotionEmailVerification.getVerificationIsSuccess()) { + setStatusToSuccess(promotionEmailVerification); + setUsed(promotionUrl); + } + + return GetApplePromotionUrlServiceResponse.builder().promotionUrl(promotionUrl).build(); + } + + private void setUsed(String promotionUrl) { + ApplePromotionUrl applePromotionUrl = + applePromotionUrlRepository.findByUrl(promotionUrl).orElseThrow(); + applePromotionUrl.setUsed(); + } + + private void setStatusToSuccess(PromotionEmailVerification promotionEmailVerification) { + promotionEmailVerification.setVerifiedAt(LocalDateTimeUtil.getLocalDateTimeNow()); + promotionEmailVerification.setVerificationIsSuccess(true); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/apple/service/response/GetApplePromotionUrlServiceResponse.java b/src/main/java/earlybird/earlybird/promotion/apple/service/response/GetApplePromotionUrlServiceResponse.java new file mode 100644 index 0000000..92a652d --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/apple/service/response/GetApplePromotionUrlServiceResponse.java @@ -0,0 +1,12 @@ +package earlybird.earlybird.promotion.apple.service.response; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class GetApplePromotionUrlServiceResponse { + private final String promotionUrl; +} diff --git a/src/main/java/earlybird/earlybird/promotion/controller/PromotionCampaignController.java b/src/main/java/earlybird/earlybird/promotion/controller/PromotionCampaignController.java new file mode 100644 index 0000000..4b343f7 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/controller/PromotionCampaignController.java @@ -0,0 +1,35 @@ +package earlybird.earlybird.promotion.controller; + +import earlybird.earlybird.promotion.controller.request.CreatePromotionCampaignRequest; +import earlybird.earlybird.promotion.controller.response.CreatePromotionCampaignResponse; +import earlybird.earlybird.promotion.service.CreatePromotionCampaignService; +import earlybird.earlybird.promotion.service.request.CreatePromotionCampaignServiceRequest; +import earlybird.earlybird.promotion.service.response.CreatePromotionCampaignServiceResponse; + +import lombok.RequiredArgsConstructor; + +import org.springframework.http.ResponseEntity; +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; + +@RequiredArgsConstructor +@RequestMapping("/api/v1/promotion/campaign") +@RestController +public class PromotionCampaignController { + + private final CreatePromotionCampaignService createPromotionCampaignService; + + @PostMapping + public ResponseEntity createPromotionCampaign( + @RequestBody CreatePromotionCampaignRequest request) { + CreatePromotionCampaignServiceRequest serviceRequest = + CreatePromotionCampaignServiceRequest.from(request); + CreatePromotionCampaignServiceResponse serviceResponse = + createPromotionCampaignService.create(serviceRequest); + CreatePromotionCampaignResponse response = + CreatePromotionCampaignResponse.from(serviceResponse); + return ResponseEntity.ok().body(response); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/controller/request/CreatePromotionCampaignRequest.java b/src/main/java/earlybird/earlybird/promotion/controller/request/CreatePromotionCampaignRequest.java new file mode 100644 index 0000000..0324a9a --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/controller/request/CreatePromotionCampaignRequest.java @@ -0,0 +1,42 @@ +package earlybird.earlybird.promotion.controller.request; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import earlybird.earlybird.promotion.entity.PromotionCampaignType; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor +public class CreatePromotionCampaignRequest { + + @NotBlank private String promotionCampaignName; + + @NotBlank private String promotionCampaignDescription; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime startTime; + + @NotNull + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + private LocalDateTime endTime; + + @NotNull private Long perUserLimit; + + @NotNull private Long totalIssueLimit; + + @NotNull private PromotionCampaignType promotionCampaignType; +} diff --git a/src/main/java/earlybird/earlybird/promotion/controller/response/CreatePromotionCampaignResponse.java b/src/main/java/earlybird/earlybird/promotion/controller/response/CreatePromotionCampaignResponse.java new file mode 100644 index 0000000..1acbc0b --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/controller/response/CreatePromotionCampaignResponse.java @@ -0,0 +1,21 @@ +package earlybird.earlybird.promotion.controller.response; + +import earlybird.earlybird.promotion.service.response.CreatePromotionCampaignServiceResponse; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class CreatePromotionCampaignResponse { + private final Long promotionCampaignId; + + public static CreatePromotionCampaignResponse from( + CreatePromotionCampaignServiceResponse response) { + return CreatePromotionCampaignResponse.builder() + .promotionCampaignId(response.getPromotionCampaignId()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/controller/EmailPromotionController.java b/src/main/java/earlybird/earlybird/promotion/email/controller/EmailPromotionController.java new file mode 100644 index 0000000..167bb4b --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/controller/EmailPromotionController.java @@ -0,0 +1,64 @@ +package earlybird.earlybird.promotion.email.controller; + +import earlybird.earlybird.promotion.apple.service.GetApplePromotionUrlService; +import earlybird.earlybird.promotion.apple.service.response.GetApplePromotionUrlServiceResponse; +import earlybird.earlybird.promotion.email.controller.request.AddEmailPromotionAddressDomainRequest; +import earlybird.earlybird.promotion.email.controller.request.UnivEmailPromotionVerificationRequest; +import earlybird.earlybird.promotion.email.service.AddEmailPromotionAddressDomainService; +import earlybird.earlybird.promotion.email.service.SendPromotionEmailService; +import earlybird.earlybird.promotion.email.service.request.AddEmailPromotionAddressDomainServiceRequest; +import earlybird.earlybird.promotion.email.service.request.SendVerificationEmailServiceRequest; + +import jakarta.servlet.http.HttpServletResponse; + +import lombok.RequiredArgsConstructor; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; + +@RequiredArgsConstructor +@RestController +public class EmailPromotionController { + + private final SendPromotionEmailService sendPromotionEmailService; + private final AddEmailPromotionAddressDomainService addEmailPromotionAddressDomainService; + private final GetApplePromotionUrlService getApplePromotionUrlService; + + @PostMapping("/api/v1/promotion/apple/univ/email") + public ResponseEntity univPromotionVerifyEmail( + @RequestBody UnivEmailPromotionVerificationRequest request) { + + SendVerificationEmailServiceRequest serviceRequest = + SendVerificationEmailServiceRequest.from(request); + sendPromotionEmailService.sendVerificationEmail(serviceRequest); + + return null; + } + + @PostMapping("/api/v1/promotion/email/domain") + public ResponseEntity addEmailPromotionAddressDomain( + @RequestBody AddEmailPromotionAddressDomainRequest request) { + + AddEmailPromotionAddressDomainServiceRequest serviceRequest = + AddEmailPromotionAddressDomainServiceRequest.from(request); + + addEmailPromotionAddressDomainService.add(serviceRequest); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/promotion/apple/univ/email") + public void redirectToPromotionPage( + @RequestParam(name = "code") String promotionCodeUuid, + HttpServletResponse servletResponse) + throws IOException { + + GetApplePromotionUrlServiceResponse serviceResponse = + getApplePromotionUrlService.getPromotionUrlByUuid(promotionCodeUuid); + + String promotionUrl = serviceResponse.getPromotionUrl(); + servletResponse.sendRedirect(promotionUrl); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/controller/request/AddEmailPromotionAddressDomainRequest.java b/src/main/java/earlybird/earlybird/promotion/email/controller/request/AddEmailPromotionAddressDomainRequest.java new file mode 100644 index 0000000..ec46a18 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/controller/request/AddEmailPromotionAddressDomainRequest.java @@ -0,0 +1,16 @@ +package earlybird.earlybird.promotion.email.controller.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class AddEmailPromotionAddressDomainRequest { + + @NotBlank private String domain; + + @NotNull private Long promotionCampaignId; +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/controller/request/UnivEmailPromotionVerificationRequest.java b/src/main/java/earlybird/earlybird/promotion/email/controller/request/UnivEmailPromotionVerificationRequest.java new file mode 100644 index 0000000..688e9ce --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/controller/request/UnivEmailPromotionVerificationRequest.java @@ -0,0 +1,20 @@ +package earlybird.earlybird.promotion.email.controller.request; + +import earlybird.earlybird.promotion.email.entity.PromotionEmailMessageType; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class UnivEmailPromotionVerificationRequest { + + @NotBlank private String email; + + @NotNull private Long promotionCampaignId; + + @NotNull private PromotionEmailMessageType promotionEmailMessageType; +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/entity/EmailPromotionAddressDomain.java b/src/main/java/earlybird/earlybird/promotion/email/entity/EmailPromotionAddressDomain.java new file mode 100644 index 0000000..7d42974 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/entity/EmailPromotionAddressDomain.java @@ -0,0 +1,41 @@ +package earlybird.earlybird.promotion.email.entity; + +import earlybird.earlybird.common.BaseTimeEntity; +import earlybird.earlybird.promotion.entity.PromotionCampaign; + +import jakarta.persistence.*; + +import lombok.*; + +/** 프로모션 대상 이메일의 도메인을 관리하는 엔티티 */ +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table( + name = "email_promotion_address_domains", + uniqueConstraints = { + @UniqueConstraint( + name = "uniqueDomainNameAndPromotionCampaignId", + columnNames = { + "email_promotion_address_domains_domain_name", + "promotion_campaigns_id" + }) + }) +@Entity +public class EmailPromotionAddressDomain extends BaseTimeEntity { + + @Column(name = "email_promotion_address_domains_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + // 프로모션 대상 이메일 주소의 도메인 이름 + @Column(name = "email_promotion_address_domains_domain_name", nullable = false) + private String domainName; + + // 소속 프로모션 캠페인 + @JoinColumn(name = "promotion_campaigns_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private PromotionCampaign promotionCampaign; +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/entity/EmailPromotionCodeIssuance.java b/src/main/java/earlybird/earlybird/promotion/email/entity/EmailPromotionCodeIssuance.java new file mode 100644 index 0000000..6a38773 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/entity/EmailPromotionCodeIssuance.java @@ -0,0 +1,37 @@ +package earlybird.earlybird.promotion.email.entity; + +import earlybird.earlybird.common.BaseTimeEntity; +import earlybird.earlybird.promotion.entity.PromotionCampaign; + +import jakarta.persistence.*; + +import lombok.*; + +/** 프로모션 코드 발급 내역을 관리하는 엔티티 */ +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table(name = "email_promotion_code_issuances") +@Entity +public class EmailPromotionCodeIssuance extends BaseTimeEntity { + + @Column(name = "email_promotion_code_issuances_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + // 소속 프로모션 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "promotion_campaigns_id", nullable = false) + private PromotionCampaign promotionCampaign; + + // 발급한 프로모션 코드 + @Column(name = "email_promotion_code_issuances_promotion_code", nullable = false) + private String promotionCode; + + // 발급한 프로모션 코드 사용 여부 + // 애플에서 발급한 특가 할인 URL은 방문했을 경우 TRUE로 저장 + @Column(name = "email_promotion_code_issuances_code_is_used", nullable = false) + private Boolean promotionCodeIsUsed; +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/entity/PromotionEmailMessageType.java b/src/main/java/earlybird/earlybird/promotion/email/entity/PromotionEmailMessageType.java new file mode 100644 index 0000000..d8340d2 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/entity/PromotionEmailMessageType.java @@ -0,0 +1,39 @@ +package earlybird.earlybird.promotion.email.entity; + +import lombok.Getter; + +public enum PromotionEmailMessageType { + BERKELEY_6_MONTH_FREE( + "EarlyBird 6-month free trial for Berkeley students", + """ + Hi, this is Young from the EarlyBird team.

+ We’re offering Berkeley students a 6-month free trial of EarlyBird:
+ %s
+ It’s a self-care app that helps you stop procrastinating and do better in school.

+ Would love to hear your thoughts anytime.

+ Best,
+ Young
+ Founder, EarlyBird + """), + BERKELEY_30_DAY_FREE( + "EarlyBird 30-day free trial for Berkeley students", + """ + Hi, this is Young from the EarlyBird team.

+ We’re offering Berkeley students a 30-day free trial of EarlyBird:
+ %s
+ It’s a self-care app that helps you stop procrastinating and do better in school.

+ Would love to hear your thoughts anytime.

+ Best,
+ Young
+ Founder, EarlyBird + """); + + @Getter private final String title; + + @Getter private final String messageText; + + PromotionEmailMessageType(String title, String messageText) { + this.title = title; + this.messageText = messageText; + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/entity/PromotionEmailVerification.java b/src/main/java/earlybird/earlybird/promotion/email/entity/PromotionEmailVerification.java new file mode 100644 index 0000000..cef54f7 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/entity/PromotionEmailVerification.java @@ -0,0 +1,62 @@ +package earlybird.earlybird.promotion.email.entity; + +import earlybird.earlybird.common.BaseTimeEntity; +import earlybird.earlybird.promotion.entity.PromotionCampaign; +import earlybird.earlybird.promotion.entity.PromotionUrlUuid; + +import jakarta.persistence.*; + +import lombok.*; + +import java.time.LocalDateTime; + +/** 프로모션 이메일 인증 내역을 관리하는 엔티티 */ +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table(name = "promotion_email_verifications") +@Entity +public class PromotionEmailVerification extends BaseTimeEntity { + + @Column(name = "promotion_email_verifications_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + // 소속 프로모션 캠페인 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "promotion_campaigns_id", nullable = false) + private PromotionCampaign promotionCampaign; + + // 클라이언트 이메일로 전송한 URL 주소에 들어 있는 UUID + @OneToOne + @JoinColumn(name = "promotion_url_uuid_promotion_url_uuids_id", nullable = false) + private PromotionUrlUuid promotionUrlUuid; + + // 인증 대상 이메일 + @Column(name = "promotion_email_verifications_email", nullable = false) + private String email; + + // 인증 성공 여부 + @Setter + @Column(name = "promotion_email_verifications_is_success", nullable = false) + private Boolean verificationIsSuccess; + + // 프로모션 코드 발급 정보 + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "email_promotion_code_issuances_id", nullable = false) + private EmailPromotionCodeIssuance emailPromotionCodeIssuance; + + // 시간대 : 한국 + // 인증 이메일 전송 일시 + @Setter + @Column(name = "promotion_email_verification_email_sent_at") + private LocalDateTime sentAt; + + // 시간대 : 한국 + // 인증 전에는 NULL + @Setter + @Column(name = "promotion_email_verifications_verfied_at") + private LocalDateTime verifiedAt; +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/repository/EmailPromotionAddressDomainRepository.java b/src/main/java/earlybird/earlybird/promotion/email/repository/EmailPromotionAddressDomainRepository.java new file mode 100644 index 0000000..ad61cc1 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/repository/EmailPromotionAddressDomainRepository.java @@ -0,0 +1,18 @@ +package earlybird.earlybird.promotion.email.repository; + +import earlybird.earlybird.promotion.email.entity.EmailPromotionAddressDomain; +import earlybird.earlybird.promotion.entity.PromotionCampaign; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface EmailPromotionAddressDomainRepository + extends JpaRepository { + + List findAllByPromotionCampaign( + PromotionCampaign promotionCampaign); + + Boolean existsByDomainNameAndPromotionCampaign( + String domainName, PromotionCampaign promotionCampaign); +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/repository/EmailPromotionCodeIssuanceRepository.java b/src/main/java/earlybird/earlybird/promotion/email/repository/EmailPromotionCodeIssuanceRepository.java new file mode 100644 index 0000000..33e6d00 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/repository/EmailPromotionCodeIssuanceRepository.java @@ -0,0 +1,8 @@ +package earlybird.earlybird.promotion.email.repository; + +import earlybird.earlybird.promotion.email.entity.EmailPromotionCodeIssuance; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EmailPromotionCodeIssuanceRepository + extends JpaRepository {} diff --git a/src/main/java/earlybird/earlybird/promotion/email/repository/PromotionEmailVerificationRepository.java b/src/main/java/earlybird/earlybird/promotion/email/repository/PromotionEmailVerificationRepository.java new file mode 100644 index 0000000..72ccb1d --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/repository/PromotionEmailVerificationRepository.java @@ -0,0 +1,14 @@ +package earlybird.earlybird.promotion.email.repository; + +import earlybird.earlybird.promotion.email.entity.PromotionEmailVerification; +import earlybird.earlybird.promotion.entity.PromotionUrlUuid; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface PromotionEmailVerificationRepository + extends JpaRepository { + + Optional findByPromotionUrlUuid(PromotionUrlUuid promotionUrlUuid); +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/service/AddEmailPromotionAddressDomainService.java b/src/main/java/earlybird/earlybird/promotion/email/service/AddEmailPromotionAddressDomainService.java new file mode 100644 index 0000000..2371714 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/service/AddEmailPromotionAddressDomainService.java @@ -0,0 +1,43 @@ +package earlybird.earlybird.promotion.email.service; + +import earlybird.earlybird.error.exception.PromotionEmailDomainNameIsAlreadyExistsException; +import earlybird.earlybird.promotion.email.entity.EmailPromotionAddressDomain; +import earlybird.earlybird.promotion.email.repository.EmailPromotionAddressDomainRepository; +import earlybird.earlybird.promotion.email.service.request.AddEmailPromotionAddressDomainServiceRequest; +import earlybird.earlybird.promotion.entity.PromotionCampaign; +import earlybird.earlybird.promotion.service.GetPromotionCampaignService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class AddEmailPromotionAddressDomainService { + + private final EmailPromotionAddressDomainRepository domainRepository; + private final GetPromotionCampaignService getPromotionCampaignService; + + @Transactional + public void add(AddEmailPromotionAddressDomainServiceRequest request) { + PromotionCampaign promotionCampaign = + getPromotionCampaignService.findById(request.getPromotionCampaignId()); + + Boolean domainIsAlreadyExists = + domainRepository.existsByDomainNameAndPromotionCampaign( + request.getDomain(), promotionCampaign); + + if (domainIsAlreadyExists) { + throw new PromotionEmailDomainNameIsAlreadyExistsException(); + } + + EmailPromotionAddressDomain emailPromotionAddressDomain = + EmailPromotionAddressDomain.builder() + .domainName(request.getDomain()) + .promotionCampaign(promotionCampaign) + .build(); + + domainRepository.save(emailPromotionAddressDomain); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/service/CheckEmailAddressService.java b/src/main/java/earlybird/earlybird/promotion/email/service/CheckEmailAddressService.java new file mode 100644 index 0000000..0806f8f --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/service/CheckEmailAddressService.java @@ -0,0 +1,57 @@ +package earlybird.earlybird.promotion.email.service; + +import earlybird.earlybird.promotion.email.entity.EmailPromotionAddressDomain; +import earlybird.earlybird.promotion.email.repository.EmailPromotionAddressDomainRepository; +import earlybird.earlybird.promotion.entity.PromotionCampaign; +import earlybird.earlybird.promotion.repository.PromotionCampaignRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class CheckEmailAddressService { + + private final EmailPromotionAddressDomainRepository promotionDomainRepository; + private final PromotionCampaignRepository promotionCampaignRepository; + + @Transactional + public Boolean checkValidPromotionEmail(String email, Long promotionCampaignId) { + checkEmailRegex(email); + PromotionCampaign promotionCampaign = getPromotionCampaignById(promotionCampaignId); + return checkDomainName(email, promotionCampaign); + } + + private void checkEmailRegex(String email) { + String emailRegex = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"; + if (!email.matches(emailRegex)) { + throw new IllegalArgumentException("Invalid email address"); + } + } + + private PromotionCampaign getPromotionCampaignById(Long promotionCampaignId) { + Optional optionalCampaign = + promotionCampaignRepository.findById(promotionCampaignId); + if (optionalCampaign.isEmpty()) { + throw new IllegalArgumentException("Campaign not found"); + } + return optionalCampaign.get(); + } + + private Boolean checkDomainName(String email, PromotionCampaign promotionCampaign) { + List validDomainList = + promotionDomainRepository.findAllByPromotionCampaign(promotionCampaign); + return validDomainList.stream() + .anyMatch( + validDomain -> { + String validDomainName = validDomain.getDomainName(); + String requestDomainName = email.split("@")[1]; + return validDomainName.equals(requestDomainName); + }); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/service/SendPromotionEmailService.java b/src/main/java/earlybird/earlybird/promotion/email/service/SendPromotionEmailService.java new file mode 100644 index 0000000..a0f6aed --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/service/SendPromotionEmailService.java @@ -0,0 +1,105 @@ +package earlybird.earlybird.promotion.email.service; + +import earlybird.earlybird.email.SendEmailService; +import earlybird.earlybird.error.exception.InvalidPromotionEmailException; +import earlybird.earlybird.promotion.apple.service.GetApplePromotionUrlService; +import earlybird.earlybird.promotion.email.entity.EmailPromotionCodeIssuance; +import earlybird.earlybird.promotion.email.entity.PromotionEmailVerification; +import earlybird.earlybird.promotion.email.repository.EmailPromotionCodeIssuanceRepository; +import earlybird.earlybird.promotion.email.repository.PromotionEmailVerificationRepository; +import earlybird.earlybird.promotion.email.service.request.SendVerificationEmailServiceRequest; +import earlybird.earlybird.promotion.entity.PromotionCampaign; +import earlybird.earlybird.promotion.entity.PromotionUrlUuid; +import earlybird.earlybird.promotion.service.CreatePromotionUrlUuidService; +import earlybird.earlybird.promotion.service.GetPromotionCampaignService; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class SendPromotionEmailService { + + private final GetApplePromotionUrlService getApplePromotionUrlService; + private final CheckEmailAddressService checkEmailAddressService; + private final CreatePromotionUrlUuidService createPromotionUrlUuidService; + private final GetPromotionCampaignService getPromotionCampaignService; + private final SendEmailService sendEmailService; + private final PromotionEmailVerificationRepository promotionEmailVerificationRepository; + private final EmailPromotionCodeIssuanceRepository emailPromotionCodeIssuanceRepository; + + @Transactional + public void sendVerificationEmail(SendVerificationEmailServiceRequest request) { + /** + * TODO - 이메일 유효성 검사 (완) - URL을 구성할 UUID 발급 (완) - DB에서 애플 프로모션 URL 하나 조회 - 락을 걸었는데 이거 성능 문제 + * 좀 더 고민 필요 - 이메일로 인증 URL 전송 (여기에 UUID가 들어감) - 비동기 처리 필요 (완) - no-reply@earlybirdteam.com + * 이메일 적용 필요 - 인증 URL을 클릭하면 애플 프로모션 URL로 리다이렉션 (완) - 엣지 케이스 좀 더 고민 필요 - 스프링 트랜잭션에 대한 고민 필요 - + * Retry 로직 도입 고려 (완) - 메일 발송 부분 로직 문제 없나 다시 체크 + */ + checkEmailAddress(request); + + PromotionCampaign promotionCampaign = + getPromotionCampaignService.findById(request.getPromotionCampaignId()); + + PromotionUrlUuid promotionUrlUuid = createPromotionUrlUuidService.create(); + String promotionUrl = + getApplePromotionUrlService + .getPromotionUrl(promotionCampaign.getId()) + .getPromotionUrl(); + + EmailPromotionCodeIssuance emailPromotionCodeIssuance = + createEmailPromotionCodeIssuance(promotionCampaign, promotionUrl); + + PromotionEmailVerification promotionEmailVerification = + createPromotionEmailVerification( + promotionCampaign, + promotionUrlUuid, + request.getEmail(), + emailPromotionCodeIssuance); + + sendEmailService.send(promotionEmailVerification, request.getPromotionEmailMessageType()); + } + + private void checkEmailAddress(SendVerificationEmailServiceRequest request) { + Boolean isValidDomain = + checkEmailAddressService.checkValidPromotionEmail( + request.getEmail(), request.getPromotionCampaignId()); + + if (!isValidDomain) { + throw new InvalidPromotionEmailException(); + } + } + + private EmailPromotionCodeIssuance createEmailPromotionCodeIssuance( + PromotionCampaign promotionCampaign, String promotionCode) { + + EmailPromotionCodeIssuance emailPromotionCodeIssuance = + EmailPromotionCodeIssuance.builder() + .promotionCampaign(promotionCampaign) + .promotionCode(promotionCode) + .promotionCodeIsUsed(false) + .build(); + + return emailPromotionCodeIssuanceRepository.save(emailPromotionCodeIssuance); + } + + private PromotionEmailVerification createPromotionEmailVerification( + PromotionCampaign promotionCampaign, + PromotionUrlUuid promotionUrlUuid, + String email, + EmailPromotionCodeIssuance emailPromotionCodeIssuance) { + + PromotionEmailVerification promotionEmailVerification = + PromotionEmailVerification.builder() + .promotionCampaign(promotionCampaign) + .promotionUrlUuid(promotionUrlUuid) + .verificationIsSuccess(false) + .email(email) + .emailPromotionCodeIssuance(emailPromotionCodeIssuance) + .build(); + + return promotionEmailVerificationRepository.save(promotionEmailVerification); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/service/request/AddEmailPromotionAddressDomainServiceRequest.java b/src/main/java/earlybird/earlybird/promotion/email/service/request/AddEmailPromotionAddressDomainServiceRequest.java new file mode 100644 index 0000000..2f5023d --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/service/request/AddEmailPromotionAddressDomainServiceRequest.java @@ -0,0 +1,23 @@ +package earlybird.earlybird.promotion.email.service.request; + +import earlybird.earlybird.promotion.email.controller.request.AddEmailPromotionAddressDomainRequest; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class AddEmailPromotionAddressDomainServiceRequest { + private final String domain; + private final Long promotionCampaignId; + + public static AddEmailPromotionAddressDomainServiceRequest from( + AddEmailPromotionAddressDomainRequest request) { + return AddEmailPromotionAddressDomainServiceRequest.builder() + .domain(request.getDomain()) + .promotionCampaignId(request.getPromotionCampaignId()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/email/service/request/SendVerificationEmailServiceRequest.java b/src/main/java/earlybird/earlybird/promotion/email/service/request/SendVerificationEmailServiceRequest.java new file mode 100644 index 0000000..fcdd0b3 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/email/service/request/SendVerificationEmailServiceRequest.java @@ -0,0 +1,26 @@ +package earlybird.earlybird.promotion.email.service.request; + +import earlybird.earlybird.promotion.email.controller.request.UnivEmailPromotionVerificationRequest; +import earlybird.earlybird.promotion.email.entity.PromotionEmailMessageType; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Builder +@RequiredArgsConstructor +public class SendVerificationEmailServiceRequest { + private final String email; + private final Long promotionCampaignId; + private final PromotionEmailMessageType promotionEmailMessageType; + + public static SendVerificationEmailServiceRequest from( + UnivEmailPromotionVerificationRequest request) { + return SendVerificationEmailServiceRequest.builder() + .email(request.getEmail()) + .promotionCampaignId(request.getPromotionCampaignId()) + .promotionEmailMessageType(request.getPromotionEmailMessageType()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaign.java b/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaign.java new file mode 100644 index 0000000..3802229 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaign.java @@ -0,0 +1,56 @@ +package earlybird.earlybird.promotion.entity; + +import earlybird.earlybird.common.BaseTimeEntity; + +import jakarta.persistence.*; + +import lombok.*; + +import java.time.LocalDateTime; + +/** 프로모션 정책(기간, 인당 횟수 제한 등)을 관리하는 엔티티 */ +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table(name = "promotion_campaigns") +@Entity +public class PromotionCampaign extends BaseTimeEntity { + + @Column(name = "promotion_campaigns_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + // 프로모션 캠페인 이름 + @Column(name = "promotion_campaigns_name", nullable = false) + private String name; + + @Column(name = "promotion_campaigns_description") + private String description; + + // 시간대 : 한국 시간대 + @Column(name = "promotion_campaigns_starts_time", nullable = false) + private LocalDateTime startTime; + + // 시간대 : 한국 시간대 + @Column(name = "promotion_campaigns_ends_time", nullable = false) + private LocalDateTime endTime; + + // // 인당 발급 허용 개수 + // @Column(name = "promotion_campaigns_per_user_limit", nullable = false) + // private Long perUserLimit; + // + // // 캠페인 전체 발급 최대치 (없으면 NULL) + // @Column(name = "promotion_campaigns_total_issue_limit") + // private Long totalIssueLimit; + + // 캠페인 타입 + @Enumerated(EnumType.STRING) + @Column(name = "promotion_campaigns_type", nullable = false) + private PromotionCampaignType promotionCampaignType; + + // 캠페인 활성화 여부 + @Column(name = "promotion_campaigns_is_active", nullable = false) + private Boolean active; +} diff --git a/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaignType.java b/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaignType.java new file mode 100644 index 0000000..f89824b --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/entity/PromotionCampaignType.java @@ -0,0 +1,13 @@ +package earlybird.earlybird.promotion.entity; + +import lombok.Getter; + +public enum PromotionCampaignType { + EDU_EMAIL_6_MONTH_FREE("대학생 6개월 무료 프로모션"); + + @Getter private final String description; + + PromotionCampaignType(String description) { + this.description = description; + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/entity/PromotionUrlUuid.java b/src/main/java/earlybird/earlybird/promotion/entity/PromotionUrlUuid.java new file mode 100644 index 0000000..8ed26de --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/entity/PromotionUrlUuid.java @@ -0,0 +1,27 @@ +package earlybird.earlybird.promotion.entity; + +import earlybird.earlybird.common.BaseTimeEntity; + +import jakarta.persistence.*; + +import lombok.*; + +import java.util.UUID; + +/** 프로모션 URL 주소에 들어가는 UUID를 관리하는 엔티티. 한번 사용한 UUID는 다시 사용하면 안 된다. 혹시 모를 중복에 대비하기 위한 엔티티. */ +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +@Table(name = "promotion_url_uuids") +@Entity +public class PromotionUrlUuid extends BaseTimeEntity { + + @Column(name = "promotion_url_uuids_id", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + private Long id; + + @Column(name = "promotion_url_uuids_uuid", nullable = false, unique = true) + private UUID uuid; +} diff --git a/src/main/java/earlybird/earlybird/promotion/repository/PromotionCampaignRepository.java b/src/main/java/earlybird/earlybird/promotion/repository/PromotionCampaignRepository.java new file mode 100644 index 0000000..02bca45 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/repository/PromotionCampaignRepository.java @@ -0,0 +1,7 @@ +package earlybird.earlybird.promotion.repository; + +import earlybird.earlybird.promotion.entity.PromotionCampaign; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PromotionCampaignRepository extends JpaRepository {} diff --git a/src/main/java/earlybird/earlybird/promotion/repository/PromotionUrlUuidRepository.java b/src/main/java/earlybird/earlybird/promotion/repository/PromotionUrlUuidRepository.java new file mode 100644 index 0000000..7746186 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/repository/PromotionUrlUuidRepository.java @@ -0,0 +1,13 @@ +package earlybird.earlybird.promotion.repository; + +import earlybird.earlybird.promotion.entity.PromotionUrlUuid; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface PromotionUrlUuidRepository extends JpaRepository { + + Optional findByUuid(UUID uuid); +} diff --git a/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignService.java b/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignService.java new file mode 100644 index 0000000..820807a --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionCampaignService.java @@ -0,0 +1,40 @@ +package earlybird.earlybird.promotion.service; + +import earlybird.earlybird.promotion.entity.PromotionCampaign; +import earlybird.earlybird.promotion.repository.PromotionCampaignRepository; +import earlybird.earlybird.promotion.service.request.CreatePromotionCampaignServiceRequest; +import earlybird.earlybird.promotion.service.response.CreatePromotionCampaignServiceResponse; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class CreatePromotionCampaignService { + + private final PromotionCampaignRepository promotionCampaignRepository; + + @Transactional + public CreatePromotionCampaignServiceResponse create( + CreatePromotionCampaignServiceRequest request) { + PromotionCampaign promotionCampaign = createPromotionCampaign(request); + PromotionCampaign saved = promotionCampaignRepository.save(promotionCampaign); + return CreatePromotionCampaignServiceResponse.from(saved); + } + + private PromotionCampaign createPromotionCampaign( + CreatePromotionCampaignServiceRequest request) { + return PromotionCampaign.builder() + .name(request.getName()) + .description(request.getDescription()) + .startTime(request.getStartTime()) + .endTime(request.getEndTime()) + // .perUserLimit(request.getPerUserLimit()) + // .totalIssueLimit(request.getTotalIssueLimit()) + .promotionCampaignType(request.getPromotionCampaignType()) + .active(true) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionUrlUuidService.java b/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionUrlUuidService.java new file mode 100644 index 0000000..50f5719 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/service/CreatePromotionUrlUuidService.java @@ -0,0 +1,23 @@ +package earlybird.earlybird.promotion.service; + +import earlybird.earlybird.promotion.entity.PromotionUrlUuid; +import earlybird.earlybird.promotion.repository.PromotionUrlUuidRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@RequiredArgsConstructor +@Service +public class CreatePromotionUrlUuidService { + + private final PromotionUrlUuidRepository urlUuidRepository; + + public PromotionUrlUuid create() { + PromotionUrlUuid promotionUrlUuid = + PromotionUrlUuid.builder().uuid(UUID.randomUUID()).build(); + return urlUuidRepository.save(promotionUrlUuid); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/service/GetPromotionCampaignService.java b/src/main/java/earlybird/earlybird/promotion/service/GetPromotionCampaignService.java new file mode 100644 index 0000000..b6284e4 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/service/GetPromotionCampaignService.java @@ -0,0 +1,19 @@ +package earlybird.earlybird.promotion.service; + +import earlybird.earlybird.promotion.entity.PromotionCampaign; +import earlybird.earlybird.promotion.repository.PromotionCampaignRepository; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class GetPromotionCampaignService { + + private final PromotionCampaignRepository promotionCampaignRepository; + + public PromotionCampaign findById(Long promotionCampaignId) { + return promotionCampaignRepository.findById(promotionCampaignId).orElseThrow(); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequest.java b/src/main/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequest.java new file mode 100644 index 0000000..bee961f --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/service/request/CreatePromotionCampaignServiceRequest.java @@ -0,0 +1,36 @@ +package earlybird.earlybird.promotion.service.request; + +import earlybird.earlybird.promotion.controller.request.CreatePromotionCampaignRequest; +import earlybird.earlybird.promotion.entity.PromotionCampaignType; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Builder +@RequiredArgsConstructor +@Getter +public class CreatePromotionCampaignServiceRequest { + private final String name; + private final String description; + private final LocalDateTime startTime; + private final LocalDateTime endTime; + private final Long perUserLimit; + private final Long totalIssueLimit; + private final PromotionCampaignType promotionCampaignType; + + public static CreatePromotionCampaignServiceRequest from( + CreatePromotionCampaignRequest request) { + return CreatePromotionCampaignServiceRequest.builder() + .name(request.getPromotionCampaignName()) + .description(request.getPromotionCampaignDescription()) + .startTime(request.getStartTime()) + .endTime(request.getEndTime()) + .perUserLimit(request.getPerUserLimit()) + .totalIssueLimit(request.getTotalIssueLimit()) + .promotionCampaignType(request.getPromotionCampaignType()) + .build(); + } +} diff --git a/src/main/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponse.java b/src/main/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponse.java new file mode 100644 index 0000000..00037e2 --- /dev/null +++ b/src/main/java/earlybird/earlybird/promotion/service/response/CreatePromotionCampaignServiceResponse.java @@ -0,0 +1,20 @@ +package earlybird.earlybird.promotion.service.response; + +import earlybird.earlybird.promotion.entity.PromotionCampaign; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Builder +@Getter +@RequiredArgsConstructor +public class CreatePromotionCampaignServiceResponse { + private final Long PromotionCampaignId; + + public static CreatePromotionCampaignServiceResponse from(PromotionCampaign promotionCampaign) { + return CreatePromotionCampaignServiceResponse.builder() + .PromotionCampaignId(promotionCampaign.getId()) + .build(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index d214789..be6bcfc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,6 +5,17 @@ spring: active: ${SPRING_ACTIVE_PROFILE} jwt: secret: ${JWT_SECRET} + mail: + host: smtp.gmail.com + port: 587 + username: earlybirdteam2024 + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true fcm: service-account-file: key/firebase_admin_sdk_private_key.json diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index aaa938b..7d43ee0 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -23,6 +23,17 @@ spring: format_sql: true show_sql: true use_sql_comments: true + mail: + host: localhost + port: 587 + username: test + password: test + properties: + mail: + smtp: + auth: true + starttls: + enable: true fcm: service-account-file: key/firebase_admin_sdk_private_key.json