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/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()); + } }