Skip to content
2 changes: 2 additions & 0 deletions src/main/java/com/recyclestudy/RecyclestudyApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableAsync
@EnableScheduling
@SpringBootApplication
public class RecyclestudyApplication {

Expand Down
41 changes: 41 additions & 0 deletions src/main/java/com/recyclestudy/email/DeviceAuthEmailSender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.recyclestudy.email;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceAuthEmailSender {

private final EmailSender emailSender;
private final TemplateEngine templateEngine;

@Value("${auth.base-url}")
private String baseUrl;

@Async
public void sendDeviceAuthMail(final String email, final String deviceId) {
final String authUrl = createAuthUrl(email, deviceId);
final String message = createMessage(authUrl);

emailSender.send(email, "[Recycle Study] 디바이스 인증을 완료해주세요.", message);

log.info("인증 메일 발송 성공: {}", email);
}

private String createAuthUrl(final String email, final String deviceId) {
return String.format("%s/api/v1/device/auth?email=%s&identifier=%s", baseUrl, email, deviceId);
}

private String createMessage(final String authUrl) {
final Context context = new Context();
context.setVariable("authUrl", authUrl);
return templateEngine.process("auth_email", context);
}
}
37 changes: 37 additions & 0 deletions src/main/java/com/recyclestudy/email/EmailSender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.recyclestudy.email;

import com.recyclestudy.exception.EmailSendException;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class EmailSender {

private final JavaMailSender javaMailSender;

public void send(final String targetEmail, final String subject, final String content) {
try {
final MimeMessage mimeMessage = javaMailSender.createMimeMessage();
final MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, false, "UTF-8");

helper.setTo(targetEmail);
helper.setSubject(subject);
helper.setText(content, true);

javaMailSender.send(mimeMessage);

log.info("메일 발송 성공: email={}", targetEmail);

} catch (MessagingException e) {
log.error("메일 발송 실패: email={}", targetEmail, e);
throw new EmailSendException("메일 전송 중 오류가 발생했습니다.", e);
}
}
}
64 changes: 0 additions & 64 deletions src/main/java/com/recyclestudy/email/EmailService.java

This file was deleted.

80 changes: 80 additions & 0 deletions src/main/java/com/recyclestudy/email/ReviewEmailSender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.recyclestudy.email;

import com.recyclestudy.member.domain.Email;
import com.recyclestudy.review.domain.NotificationStatus;
import com.recyclestudy.review.domain.ReviewURL;
import com.recyclestudy.review.service.NotificationHistoryService;
import com.recyclestudy.review.service.ReviewCycleService;
import com.recyclestudy.review.service.input.ReviewSendInput;
import com.recyclestudy.review.service.output.ReviewSendOutput;
import com.recyclestudy.review.service.output.ReviewSendOutput.ReviewSendElement;
import java.time.Clock;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

@Service
@RequiredArgsConstructor
@Slf4j
public class ReviewEmailSender {

private final EmailSender emailSender;
private final TemplateEngine templateEngine;
private final ReviewCycleService reviewCycleService;
private final NotificationHistoryService notificationHistoryService;
private final Clock clock;

@Scheduled(cron = "0 0 8 * * *", zone = "Asia/Seoul")
public void sendReviewMail() {

final LocalDate targetDate = LocalDate.now(clock);
final LocalTime targetTime = LocalTime.of(8, 0);

final ReviewSendOutput targetReviewCycle = reviewCycleService.findTargetReviewCycle(
ReviewSendInput.from(targetDate, targetTime));

final List<ReviewSendElement> elements = targetReviewCycle.elements();
log.info("복습 메일 발송 시작: 대상 {}명", elements.size());

int successCount = 0;
int failCount = 0;

for (final ReviewSendElement element : elements) {
final String message = createMessage(element.targetUrls());
final Email targetEmail = element.email();

final boolean success = sendToTargetEmail(targetEmail, message);

if (success) {
notificationHistoryService.saveAll(element.reviewCycleIds(), NotificationStatus.SENT);
successCount++;
} else {
notificationHistoryService.saveAll(element.reviewCycleIds(), NotificationStatus.FAILED);
failCount++;
}
}

log.info("복습 메일 발송 처리 완료: 성공 {}명, 실패 {}명", successCount, failCount);
}

private boolean sendToTargetEmail(final Email targetEmail, final String message) {
try {
emailSender.send(targetEmail.getValue(), "[Recycle Study] 오늘의 복습 목록이 도착했습니다", message);
return true;
} catch (final Exception e) {
return false;
}
}

private String createMessage(final List<ReviewURL> targetUrls) {
final Context context = new Context();
context.setVariable("targetUrls", targetUrls);
return templateEngine.process("review_email", context);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.recyclestudy.member.controller;

import com.recyclestudy.email.EmailService;
import com.recyclestudy.email.DeviceAuthEmailSender;
import com.recyclestudy.member.controller.request.MemberSaveRequest;
import com.recyclestudy.member.controller.response.MemberFindResponse;
import com.recyclestudy.member.controller.response.MemberSaveResponse;
Expand All @@ -25,14 +25,14 @@
public class MemberController {

private final MemberService memberService;
private final EmailService emailService;
private final DeviceAuthEmailSender deviceAuthEmailSender;

@PostMapping
public ResponseEntity<MemberSaveResponse> saveMember(@RequestBody final MemberSaveRequest request) {
final MemberSaveInput input = request.toInput();
final MemberSaveOutput output = memberService.saveDevice(input);

emailService.sendDeviceAuthMail(output.email().getValue(), output.identifier().getValue());
deviceAuthEmailSender.sendDeviceAuthMail(output.email().getValue(), output.identifier().getValue());

final MemberSaveResponse response = MemberSaveResponse.from(output);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public class ActivationExpiredDateTime {

private static final Duration EXPIRE_TIME_RATE = Duration.ofMinutes(5);

private LocalDateTime value;

public static ActivationExpiredDateTime create(final LocalDateTime currentTime) {
validateNotNull(currentTime);
return new ActivationExpiredDateTime(currentTime.plusMinutes(EXPIRE_TIME_RATE.toMinutes()));
Expand All @@ -35,8 +37,6 @@ private static void validateNotNull(final LocalDateTime currentTime) {
.validate();
}

private LocalDateTime value;

public void checkExpired(final LocalDateTime currentTime) {
if (currentTime.isAfter(value)) {
throw new DeviceActivationExpiredException("인증 유효 시간이 만료되었습니다.");
Expand Down
28 changes: 14 additions & 14 deletions src/main/java/com/recyclestudy/member/domain/Device.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,6 @@
@Getter
public class Device extends BaseEntity {

public static Device withoutId(
final Member member,
final DeviceIdentifier deviceIdentifier,
final boolean isActive,
final ActivationExpiredDateTime activationExpiresAt
) {
NullValidator.builder()
.add(Fields.member, member)
.add(Fields.identifier, deviceIdentifier)
.add(Fields.activationExpiresAt, activationExpiresAt)
.validate();
return new Device(member, deviceIdentifier, isActive, activationExpiresAt);
}

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;
Expand All @@ -55,6 +41,20 @@ public static Device withoutId(
@AttributeOverride(name = "value", column = @Column(name = "activation_expires_at", nullable = false))
private ActivationExpiredDateTime activationExpiresAt;

public static Device withoutId(
final Member member,
final DeviceIdentifier deviceIdentifier,
final boolean isActive,
final ActivationExpiredDateTime activationExpiresAt
) {
NullValidator.builder()
.add(Fields.member, member)
.add(Fields.identifier, deviceIdentifier)
.add(Fields.activationExpiresAt, activationExpiresAt)
.validate();
return new Device(member, deviceIdentifier, isActive, activationExpiresAt);
}

public void activate(final LocalDateTime currentTime) {
activationExpiresAt.checkExpired(currentTime);
this.isActive = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
@EqualsAndHashCode
public class DeviceIdentifier {

private String value;

public static DeviceIdentifier from(final String value) {
validateNotNull(value);
return new DeviceIdentifier(value);
Expand All @@ -34,6 +36,4 @@ private static void validateNotNull(final String value) {
.add(Fields.value, value)
.validate();
}

private String value;
}
4 changes: 2 additions & 2 deletions src/main/java/com/recyclestudy/member/domain/Email.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class Email {
private static final String EMAIL_FORMAT = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_FORMAT);

private String value;

public static Email from(final String value) {
validateNotNull(value);
validateEmailFormat(value);
Expand All @@ -40,6 +42,4 @@ private static void validateEmailFormat(final String emailValue) {
throw new IllegalArgumentException("유효하지 않은 이메일 형식입니다.");
}
}

private String value;
}
8 changes: 4 additions & 4 deletions src/main/java/com/recyclestudy/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
@Getter
public class Member extends BaseEntity {

@Embedded
@AttributeOverride(name = "value", column = @Column(name = "email", nullable = false, unique = true))
private Email email;

public static Member withoutId(final Email email) {
validateNotNull(email);
return new Member(email);
Expand All @@ -32,10 +36,6 @@ private static void validateNotNull(final Email email) {
.validate();
}

@Embedded
@AttributeOverride(name = "value", column = @Column(name = "email", nullable = false, unique = true))
private Email email;

public boolean hasEmail(final Email email) {
return this.email.equals(email);
}
Expand Down
Loading