Skip to content

Commit

Permalink
feat: ✨ 매월 목표 금액 설정 공지 푸시 알림 배치 (#141)
Browse files Browse the repository at this point in the history
* rename: notification writer -> daily_spending_notify_writer

* fix: announcement enum class not_announce 타입 필터링

* fix: daily_notication dto 범용성 확장하여 announce_notification_dto로 수정

* feat: 매월 목표 금액 설정 공지 writer 작성

* feat: monthly_target_amount_notify_config job $ step impl

* feat: 매월 정기 목표 금액 설정 알림 스케줄 설정

* fix: 목표 금액 알림 메시지 조회 시, title 해당 월 삽입되도록 수정

* fix: monthly_target_amount title %s누락 수정

* fix: announce notification dto 내부에서 월별 목표 금액 title 생성 시, 분기 처리
  • Loading branch information
psychology50 authored Jul 25, 2024
1 parent 6bd74c2 commit 9175dfa
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,46 @@
import kr.co.pennyway.domain.domains.notification.type.Announcement;
import lombok.Builder;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

@Builder
public record DailySpendingNotification(
public record AnnounceNotificationDto(
Long userId,
String title,
String content,
Set<String> deviceTokens
) {
public DailySpendingNotification {
public AnnounceNotificationDto {
Objects.requireNonNull(userId, "userId must not be null");
Objects.requireNonNull(title, "title must not be null");
Objects.requireNonNull(content, "content must not be null");
Objects.requireNonNull(deviceTokens, "deviceTokens must not be null");
}

/**
* {@link DeviceTokenOwner}를 DailySpendingNotification DTO로 변환하는 정적 팩토리 메서드
* <p>
* DeviceToken은 List로 변환되어 멤버 변수로 관리하게 된다.
*/
public static DailySpendingNotification from(DeviceTokenOwner owner) {
Announcement announcement = Announcement.DAILY_SPENDING;
public static AnnounceNotificationDto from(DeviceTokenOwner owner, Announcement announcement) {
Set<String> deviceTokens = new HashSet<>();
deviceTokens.add(owner.deviceToken());

return DailySpendingNotification.builder()
return AnnounceNotificationDto.builder()
.userId(owner.userId())
.title(announcement.createFormattedTitle(owner.name()))
.content(announcement.getContent())
.title(createFormattedTitle(owner, announcement))
.content(announcement.createFormattedContent(owner.name()))
.deviceTokens(deviceTokens)
.build();
}

private static String createFormattedTitle(DeviceTokenOwner owner, Announcement announcement) {
if (announcement.equals(Announcement.MONTHLY_TARGET_AMOUNT)) {
return announcement.createFormattedTitle(String.valueOf(LocalDateTime.now().getMonthValue()));
}

return announcement.createFormattedTitle(owner.name());
}

public void addDeviceToken(String deviceToken) {
deviceTokens.add(deviceToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import kr.co.pennyway.batch.common.dto.DeviceTokenOwner;
import kr.co.pennyway.batch.reader.ActiveDeviceTokenReader;
import kr.co.pennyway.batch.writer.NotificationWriter;
import kr.co.pennyway.batch.writer.DailySpendingNotifyWriter;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
Expand All @@ -19,7 +19,7 @@
public class DailySpendingNotifyConfig {
private final JobRepository jobRepository;
private final ActiveDeviceTokenReader reader;
private final NotificationWriter writer;
private final DailySpendingNotifyWriter writer;

@Bean
public Job dailyNotificationJob(PlatformTransactionManager transactionManager) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package kr.co.pennyway.batch.job;

import kr.co.pennyway.batch.common.dto.DeviceTokenOwner;
import kr.co.pennyway.batch.reader.ActiveDeviceTokenReader;
import kr.co.pennyway.batch.writer.MonthlyTotalAmountNotifyWriter;
import lombok.RequiredArgsConstructor;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
@RequiredArgsConstructor
public class MonthlyTargetAmountNotifyConfig {
private final JobRepository jobRepository;
private final ActiveDeviceTokenReader reader;
private final MonthlyTotalAmountNotifyWriter writer;

@Bean
public Job monthlyNotificationJob(PlatformTransactionManager transactionManager) {
return new JobBuilder("monthlyNotificationJob", jobRepository)
.start(monthlyNotificationStep(transactionManager))
.on("FAILED")
.stopAndRestart(monthlyNotificationStep(transactionManager))
.on("*")
.end()
.end()
.build();
}

@Bean
@JobScope
public Step monthlyNotificationStep(PlatformTransactionManager transactionManager) {
return new StepBuilder("sendMonthlyNotifyStep", jobRepository)
.<DeviceTokenOwner, DeviceTokenOwner>chunk(1000, transactionManager)
.reader(reader.querydslNoOffsetPagingItemReader())
.writer(writer)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
public class SpendingNotifyScheduler {
private final JobLauncher jobLauncher;
private final Job dailyNotificationJob;
private final Job monthlyNotificationJob;

@Scheduled(cron = "0 0 20 * * ?")
public void runDailyNotificationJob() {
Expand All @@ -33,4 +34,18 @@ public void runDailyNotificationJob() {
log.error("Failed to run dailyNotificationJob", e);
}
}

@Scheduled(cron = "0 0 10 1 * ?")
public void runMonthlyNotificationJob() {
JobParameters jobParameters = new JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters();

try {
jobLauncher.run(monthlyNotificationJob, jobParameters);
} catch (JobExecutionAlreadyRunningException | JobRestartException
| JobInstanceAlreadyCompleteException | JobParametersInvalidException e) {
log.error("Failed to run monthlyNotificationJob", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package kr.co.pennyway.batch.writer;

import kr.co.pennyway.batch.common.dto.DailySpendingNotification;
import kr.co.pennyway.batch.common.dto.AnnounceNotificationDto;
import kr.co.pennyway.batch.common.dto.DeviceTokenOwner;
import kr.co.pennyway.domain.domains.notification.repository.NotificationRepository;
import kr.co.pennyway.domain.domains.notification.type.Announcement;
Expand All @@ -23,7 +23,7 @@
@Slf4j
@Component
@RequiredArgsConstructor
public class NotificationWriter implements ItemWriter<DeviceTokenOwner> {
public class DailySpendingNotifyWriter implements ItemWriter<DeviceTokenOwner> {
private final NotificationRepository notificationRepository;
private final ApplicationEventPublisher publisher;

Expand All @@ -33,17 +33,17 @@ public class NotificationWriter implements ItemWriter<DeviceTokenOwner> {
public void write(@NonNull Chunk<? extends DeviceTokenOwner> owners) throws Exception {
log.info("Writer 실행: {}", owners.size());

Map<Long, DailySpendingNotification> notificationMap = new HashMap<>();
Map<Long, AnnounceNotificationDto> notificationMap = new HashMap<>();

for (DeviceTokenOwner owner : owners) {
notificationMap.computeIfAbsent(owner.userId(), k -> DailySpendingNotification.from(owner)).addDeviceToken(owner.deviceToken());
notificationMap.computeIfAbsent(owner.userId(), k -> AnnounceNotificationDto.from(owner, Announcement.DAILY_SPENDING)).addDeviceToken(owner.deviceToken());
}

List<Long> userIds = new ArrayList<>(notificationMap.keySet());

notificationRepository.saveDailySpendingAnnounceInBulk(userIds, Announcement.DAILY_SPENDING);

for (DailySpendingNotification notification : notificationMap.values()) {
for (AnnounceNotificationDto notification : notificationMap.values()) {
publisher.publishEvent(NotificationEvent.of(notification.title(), notification.content(), notification.deviceTokensForList(), ""));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package kr.co.pennyway.batch.writer;

import kr.co.pennyway.batch.common.dto.AnnounceNotificationDto;
import kr.co.pennyway.batch.common.dto.DeviceTokenOwner;
import kr.co.pennyway.domain.domains.notification.repository.NotificationRepository;
import kr.co.pennyway.domain.domains.notification.type.Announcement;
import kr.co.pennyway.infra.common.event.NotificationEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Component
@RequiredArgsConstructor
public class MonthlyTotalAmountNotifyWriter implements ItemWriter<DeviceTokenOwner> {
private final NotificationRepository notificationRepository;
private final ApplicationEventPublisher publisher;

@Override
@StepScope
@Transactional
public void write(@NonNull Chunk<? extends DeviceTokenOwner> owners) throws Exception {
log.info("Writer 실행: {}", owners.size());

Map<Long, AnnounceNotificationDto> notificationMap = new HashMap<>();

for (DeviceTokenOwner owner : owners) {
notificationMap.computeIfAbsent(owner.userId(), k -> AnnounceNotificationDto.from(owner, Announcement.MONTHLY_TARGET_AMOUNT)).addDeviceToken(owner.deviceToken());
}

List<Long> userIds = new ArrayList<>(notificationMap.keySet());

notificationRepository.saveDailySpendingAnnounceInBulk(userIds, Announcement.MONTHLY_TARGET_AMOUNT);

for (AnnounceNotificationDto notification : notificationMap.values()) {
publisher.publishEvent(NotificationEvent.of(notification.title(), notification.content(), notification.deviceTokensForList(), ""));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,19 @@ public String toString() {
* @apiNote 이 메서드는 {@link NoticeType#ANNOUNCEMENT} 타입에 대해서만 동작한다. 다른 타입의 알림을 포맷팅해야 하는 경우 해당 메서드를 확장해야 한다.
*/
public String createFormattedTitle() {
if (type.equals(NoticeType.ANNOUNCEMENT)) {
return announcement.createFormattedTitle(receiverName);
if (!type.equals(NoticeType.ANNOUNCEMENT)) {
return ""; // TODO: 알림 종류가 신규로 추가될 때, 해당 로직을 구현해야 함.
}
return ""; // TODO: 알림 종류가 신규로 추가될 때, 해당 로직을 구현해야 함.

return formatAnnouncementTitle();
}

private String formatAnnouncementTitle() {
if (announcement.equals(Announcement.MONTHLY_TARGET_AMOUNT)) {
return announcement.createFormattedTitle(String.valueOf(getCreatedAt().getMonthValue()));
}

return announcement.createFormattedTitle(receiverName);
}

/**
Expand All @@ -86,10 +95,11 @@ public String createFormattedTitle() {
* @apiNote 이 메서드는 {@link NoticeType#ANNOUNCEMENT} 타입에 대해서만 동작한다. 다른 타입의 알림을 포맷팅해야 하는 경우 해당 메서드를 확장해야 한다.
*/
public String createFormattedContent() {
if (type.equals(NoticeType.ANNOUNCEMENT)) {
return announcement.createFormattedContent(receiverName);
if (!type.equals(NoticeType.ANNOUNCEMENT)) {
return ""; // TODO: 알림 종류가 신규로 추가될 때, 해당 로직을 구현해야 함.
}
return ""; // TODO: 알림 종류가 신규로 추가될 때, 해당 로직을 구현해야 함.

return announcement.createFormattedContent(receiverName);
}

public static class Builder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public enum Announcement implements LegacyCommonType {

// 정기 지출 알림
DAILY_SPENDING("1", "%s님, 3분 카레보다 빨리 끝나요!", "많은 친구들이 소비 기록에 참여하고 있어요👀"),
MONTHLY_TARGET_AMOUNT("2", "6월의 첫 시작! 두구두구..🥁", "%s님의 이번 달 목표 소비 금액은?");
MONTHLY_TARGET_AMOUNT("2", "%s월의 첫 시작! 두구두구..🥁", "%s님의 이번 달 목표 소비 금액은?");

private final String code;
private final String title;
Expand Down Expand Up @@ -62,5 +62,9 @@ private void validateName(String name) {
if (!StringUtils.hasText(name)) {
throw new IllegalArgumentException("name must not be empty");
}

if (this == NOT_ANNOUNCE) {
throw new IllegalArgumentException("NOT_ANNOUNCE type is not allowed");
}
}
}

0 comments on commit 9175dfa

Please sign in to comment.