From 49052f4f7231be1f2c60e404ab4b48714f4e3305 Mon Sep 17 00:00:00 2001 From: Jeong-Yunmi Date: Sun, 5 Apr 2026 10:45:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Feat/#101]=20=EB=88=84=EB=9D=BD=EB=90=9C?= =?UTF-8?q?=20=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../habit/repository/HabitRepository.java | 4 ++ .../domain/habit/service/HabitScheduler.java | 38 ++++++++++++- .../domain/habit/service/HabitService.java | 18 +++++++ .../PushNotificationScheduler.java | 53 +++++++++++++++++++ 4 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java b/src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java index 16a3109..1425b2f 100644 --- a/src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java +++ b/src/main/java/com/swyp/server/domain/habit/repository/HabitRepository.java @@ -39,6 +39,10 @@ List findAllByUserIdAndStatusOrderByIsCompletedAscIdDesc( List findAllByUserIdsAndStatusOptional( @Param("userIds") List userIds, @Param("status") RewardStatus status); + @EntityGraph(attributePaths = "user") + @Query("SELECT h FROM Habit h WHERE h.status = 'IN_PROGRESS' AND h.expiredAt < :now") + List findExpiredHabits(@Param("now") LocalDateTime now); + @Modifying(clearAutomatically = true) @Query( "UPDATE Habit h " diff --git a/src/main/java/com/swyp/server/domain/habit/service/HabitScheduler.java b/src/main/java/com/swyp/server/domain/habit/service/HabitScheduler.java index 6af6f52..d8c569d 100644 --- a/src/main/java/com/swyp/server/domain/habit/service/HabitScheduler.java +++ b/src/main/java/com/swyp/server/domain/habit/service/HabitScheduler.java @@ -1,8 +1,15 @@ package com.swyp.server.domain.habit.service; +import com.swyp.server.domain.family.service.FamilyRelationService; +import com.swyp.server.domain.habit.entity.Habit; import com.swyp.server.domain.habit.repository.HabitRepository; +import com.swyp.server.domain.user.entity.User; +import com.swyp.server.domain.user.entity.UserType; +import com.swyp.server.global.notification.NotificationService; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; @@ -15,12 +22,41 @@ public class HabitScheduler { private final HabitRepository habitRepository; + private final NotificationService notificationService; + private final FamilyRelationService familyRelationService; @Transactional @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") public void checkExpiredHabits() { log.info("만료된 습관 체크 스케줄러 시작"); - habitRepository.updateExpiredHabitsStatus(LocalDateTime.now(ZoneId.of("Asia/Seoul"))); + LocalDateTime now = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + + List expiredHabits = habitRepository.findExpiredHabits(now); + + expiredHabits.stream() + .filter(h -> h.getUser().getUserType() == UserType.CHILD) + .forEach( + h -> { + notificationService.sendToUser( + h.getUser().getId(), + "해봄", + "보상을 받을 수 있어요. 지금 바로 확인해 볼까요?", + Map.of()); + + List parentIds = + familyRelationService + .getConnectedMembers(h.getUser().getId()) + .stream() + .filter(m -> m.getUserType() == UserType.PARENT) + .map(User::getId) + .toList(); + if (!parentIds.isEmpty()) { + notificationService.sendToUsers( + parentIds, "해봄", "자녀가 습관을 완료했어요. 보상을 줄 시간이에요!", Map.of()); + } + }); + + habitRepository.updateExpiredHabitsStatus(now); } @Transactional diff --git a/src/main/java/com/swyp/server/domain/habit/service/HabitService.java b/src/main/java/com/swyp/server/domain/habit/service/HabitService.java index 1d8fad2..9ef3671 100644 --- a/src/main/java/com/swyp/server/domain/habit/service/HabitService.java +++ b/src/main/java/com/swyp/server/domain/habit/service/HabitService.java @@ -107,6 +107,24 @@ public HabitRewardListResponse getHabitRewards(Long userId, RewardStatus status) targetUserIds.addAll(childIds); } + if (user.getUserType() == UserType.PARENT) { + List childIds = + familyRelationService.getConnectedMembers(userId).stream() + .filter(m -> m.getUserType() == UserType.CHILD) + .map(User::getId) + .toList(); + if (!childIds.isEmpty()) { + TransactionSynchronizationManager.registerSynchronization( + new TransactionSynchronization() { + @Override + public void afterCommit() { + notificationService.sendToUsers( + childIds, "해봄", "새로운 보상이 추가됐어요! 지금 바로 수락해 볼까요?", Map.of()); + } + }); + } + } + if (targetUserIds.isEmpty()) return HabitRewardListResponse.empty(); RewardStatus filteredStatus = status.isAll() ? null : status; diff --git a/src/main/java/com/swyp/server/global/notification/PushNotificationScheduler.java b/src/main/java/com/swyp/server/global/notification/PushNotificationScheduler.java index 1f0647e..a03aa5e 100644 --- a/src/main/java/com/swyp/server/global/notification/PushNotificationScheduler.java +++ b/src/main/java/com/swyp/server/global/notification/PushNotificationScheduler.java @@ -78,4 +78,57 @@ public void sendHabitNotRegisteredNotification() { targetIds, "해봄", "아직 등록된 습관이 없어요. 지금 습관을 추가해 볼까요?", Map.of()); log.info("Habit not registered notification sent to {} users", targetIds.size()); } + + // 매일 13시 - 할 일 미완료 1차 (모든 유저) + @Scheduled(cron = "0 0 13 * * *", zone = "Asia/Seoul") + public void sendTodoIncompleteFirstNotification() { + LocalDate today = LocalDate.now(SEOUL_ZONE); + List users = userRepository.findAllActive(); + List userIds = users.stream().map(User::getId).toList(); + if (userIds.isEmpty()) return; + List targetIds = todoRepository.findUserIdsWithIncompleteTodo(userIds, today); + notificationService.sendToUsers( + targetIds, "해봄", "아직 완료하지 않은 할 일이 있어요. 지금 확인해 볼까요?", Map.of()); + log.info("Todo incomplete 1st notification sent to {} users", targetIds.size()); + } + + // 매일 12시 - 습관 미완료 1차 (모든 유저) + @Scheduled(cron = "0 0 12 * * *", zone = "Asia/Seoul") + public void sendHabitIncompleteFirstNotification() { + List users = userRepository.findAllActive(); + List userIds = users.stream().map(User::getId).toList(); + if (userIds.isEmpty()) return; + List targetIds = habitRepository.findUserIdsWithIncompleteHabit(userIds); + notificationService.sendToUsers( + targetIds, "해봄", "아직 완료하지 않은 습관이 있어요. 지금 바로 실천해 볼까요?", Map.of()); + log.info("Habit incomplete 1st notification sent to {} users", targetIds.size()); + } + + // 매주 일요일 20시 - 성장탭 확인 알림 + @Scheduled(cron = "0 0 20 * * SUN", zone = "Asia/Seoul") + public void sendWeeklyGrowthNotification() { + List users = userRepository.findAllActive(); + List childIds = + users.stream() + .filter(u -> u.getUserType() == UserType.CHILD) + .map(User::getId) + .toList(); + List parentIds = + users.stream() + .filter(u -> u.getUserType() == UserType.PARENT) + .map(User::getId) + .toList(); + if (!childIds.isEmpty()) { + notificationService.sendToUsers( + childIds, "해봄", "이번 주가 마무리 됐어요. 나의 성장을 확인해 보세요!", Map.of()); + } + if (!parentIds.isEmpty()) { + notificationService.sendToUsers( + parentIds, "해봄", "이번 주가 마무리 됐어요. 자녀의 성장을 확인해 보세요!", Map.of()); + } + log.info( + "Weekly growth notification sent to {} children, {} parents", + childIds.size(), + parentIds.size()); + } } From 5eb8a2685a3c91c48ded510af33fc4878b410df8 Mon Sep 17 00:00:00 2001 From: Jeong-Yunmi Date: Sun, 5 Apr 2026 10:56:06 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Feat/#101]=20=EB=B3=B4=EC=83=81=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=20=EC=B6=94=EA=B0=80=EB=90=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/habit/service/HabitService.java | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/main/java/com/swyp/server/domain/habit/service/HabitService.java b/src/main/java/com/swyp/server/domain/habit/service/HabitService.java index 9ef3671..1d8fad2 100644 --- a/src/main/java/com/swyp/server/domain/habit/service/HabitService.java +++ b/src/main/java/com/swyp/server/domain/habit/service/HabitService.java @@ -107,24 +107,6 @@ public HabitRewardListResponse getHabitRewards(Long userId, RewardStatus status) targetUserIds.addAll(childIds); } - if (user.getUserType() == UserType.PARENT) { - List childIds = - familyRelationService.getConnectedMembers(userId).stream() - .filter(m -> m.getUserType() == UserType.CHILD) - .map(User::getId) - .toList(); - if (!childIds.isEmpty()) { - TransactionSynchronizationManager.registerSynchronization( - new TransactionSynchronization() { - @Override - public void afterCommit() { - notificationService.sendToUsers( - childIds, "해봄", "새로운 보상이 추가됐어요! 지금 바로 수락해 볼까요?", Map.of()); - } - }); - } - } - if (targetUserIds.isEmpty()) return HabitRewardListResponse.empty(); RewardStatus filteredStatus = status.isAll() ? null : status;