From 699ae87214530a5f9c7e0c438d17198298f90973 Mon Sep 17 00:00:00 2001 From: jeyong Date: Sat, 21 Mar 2026 15:26:10 +0900 Subject: [PATCH 1/4] Update DailyWorkSchedule and NotificationSyncService to handle nullable clock times and improve vacation notification logic --- src/main/kotlin/com/moa/entity/DailyWorkSchedule.kt | 4 ++-- src/main/kotlin/com/moa/service/WorkdayService.kt | 2 +- .../com/moa/service/notification/NotificationSyncService.kt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/moa/entity/DailyWorkSchedule.kt b/src/main/kotlin/com/moa/entity/DailyWorkSchedule.kt index 7167664..d2c6869 100644 --- a/src/main/kotlin/com/moa/entity/DailyWorkSchedule.kt +++ b/src/main/kotlin/com/moa/entity/DailyWorkSchedule.kt @@ -18,10 +18,10 @@ class DailyWorkSchedule( var type: DailyWorkScheduleType, @Column(nullable = true) - var clockInTime: LocalTime, + var clockInTime: LocalTime?, @Column(nullable = true) - var clockOutTime: LocalTime, + var clockOutTime: LocalTime?, ) : BaseEntity() { @Id diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 339a7c7..7a0d599 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -163,7 +163,7 @@ class WorkdayService( DailyWorkScheduleType.VACATION -> resolveVacationTimes(memberId, date, req) - DailyWorkScheduleType.NONE -> throw BadRequestException(ErrorCode.INVALID_WORKDAY_INPUT) + DailyWorkScheduleType.NONE -> null to null } val workSchedule = dailyWorkScheduleRepository.findByMemberIdAndDate(memberId, date) diff --git a/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt b/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt index 50694b2..942b93e 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt @@ -36,11 +36,11 @@ class NotificationSyncService( return } - if (type == DailyWorkScheduleType.VACATION) { + if (type == DailyWorkScheduleType.VACATION || type == DailyWorkScheduleType.NONE) { pendingLogs.forEach { it.status = NotificationStatus.CANCELLED } log.info( - "Cancelled {} pending notifications for member {} on {} (vacation)", - pendingLogs.size, memberId, date, + "Cancelled {} pending notifications for member {} on {} ({})", + pendingLogs.size, memberId, date, type, ) return } From cb53744ca7d4e88bc957753f93c1502a9eb324a0 Mon Sep 17 00:00:00 2001 From: jeyong Date: Sat, 21 Mar 2026 15:28:24 +0900 Subject: [PATCH 2/4] Update NotificationBatchService and WorkdayService to handle work suppression logic for vacation and none types --- src/main/kotlin/com/moa/service/WorkdayService.kt | 2 +- .../moa/service/notification/NotificationBatchService.kt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 7a0d599..70393c4 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -200,7 +200,7 @@ class WorkdayService( fun patchClockOut(memberId: Long, date: LocalDate, req: WorkdayEditRequest): WorkdayResponse { val workSchedule = dailyWorkScheduleRepository.findByMemberIdAndDate(memberId, date) ?.also { - if (it.type == DailyWorkScheduleType.VACATION) { + if (it.type == DailyWorkScheduleType.VACATION || it.type == DailyWorkScheduleType.NONE) { throw BadRequestException(ErrorCode.INVALID_WORKDAY_INPUT) } } diff --git a/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt index 289a63d..ff7e691 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt @@ -91,7 +91,7 @@ class NotificationBatchService( if (!context.hasAgreedToAll(memberId, requiredCodes)) return null if (!context.isNotificationEnabled(memberId)) return null - if (context.isOnVacation(memberId)) return null + if (context.isWorkSuppressed(memberId)) return null if (!context.hasFcmToken(memberId)) return null val override = context.getOverride(memberId) @@ -122,8 +122,9 @@ class NotificationBatchService( fun isNotificationEnabled(memberId: Long): Boolean = settingsMap[memberId]?.workNotificationEnabled != false - fun isOnVacation(memberId: Long): Boolean = - overridesMap[memberId]?.type == DailyWorkScheduleType.VACATION + fun isWorkSuppressed(memberId: Long): Boolean = + overridesMap[memberId]?.type == DailyWorkScheduleType.VACATION || + overridesMap[memberId]?.type == DailyWorkScheduleType.NONE fun hasFcmToken(memberId: Long): Boolean = !tokensMap[memberId].isNullOrEmpty() From 3329e510bbfc8cb3bf8646425fe12007b3027481 Mon Sep 17 00:00:00 2001 From: jeyong Date: Sat, 21 Mar 2026 16:20:16 +0900 Subject: [PATCH 3/4] Update NotificationLogRepository and NotificationSyncService to support multiple scheduled dates and ensure upcoming work notifications --- .../repository/NotificationLogRepository.kt | 10 ++++-- .../notification/NotificationSyncService.kt | 36 +++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt index cf36564..c1facec 100644 --- a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt +++ b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt @@ -14,12 +14,18 @@ interface NotificationLogRepository : JpaRepository { status: NotificationStatus, ): List - fun findAllByMemberIdAndScheduledDateAndStatus( + fun findAllByMemberIdAndScheduledDateInAndStatus( memberId: Long, - scheduledDate: LocalDate, + scheduledDates: Collection, status: NotificationStatus, ): List + fun findAllByMemberIdAndScheduledDateInAndNotificationTypeIn( + memberId: Long, + scheduledDates: Collection, + notificationTypes: Collection, + ): List + fun existsByMemberIdAndScheduledDateAndStatus( memberId: Long, scheduledDate: LocalDate, diff --git a/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt b/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt index 942b93e..88ab091 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt @@ -18,6 +18,7 @@ class NotificationSyncService( private val notificationLogRepository: NotificationLogRepository, ) { private val log = LoggerFactory.getLogger(javaClass) + private val workNotificationTypes = listOf(NotificationType.CLOCK_IN, NotificationType.CLOCK_OUT) @Transactional fun syncNotifications( @@ -27,12 +28,13 @@ class NotificationSyncService( clockInTime: LocalTime?, clockOutTime: LocalTime?, ) { + val relatedDates = listOf(date, date.plusDays(1)) val pendingLogs = notificationLogRepository - .findAllByMemberIdAndScheduledDateAndStatus(memberId, date, NotificationStatus.PENDING) + .findAllByMemberIdAndScheduledDateInAndStatus(memberId, relatedDates, NotificationStatus.PENDING) .filter { it.notificationType != NotificationType.PAYDAY } if (pendingLogs.isEmpty()) { - restoreIfCancelled(memberId, date, type, clockInTime, clockOutTime) + ensureUpcomingWorkNotifications(memberId, date, type, clockInTime, clockOutTime) return } @@ -49,6 +51,7 @@ class NotificationSyncService( when (pendingLog.notificationType) { NotificationType.CLOCK_IN -> { clockInTime?.let { pendingLog.scheduledTime = truncateToMinute(it) } + pendingLog.scheduledDate = date } NotificationType.CLOCK_OUT -> { @@ -70,7 +73,7 @@ class NotificationSyncService( log.info("Synced pending notifications for member {} on {}", memberId, date) } - private fun restoreIfCancelled( + private fun ensureUpcomingWorkNotifications( memberId: Long, date: LocalDate, type: DailyWorkScheduleType, @@ -80,29 +83,42 @@ class NotificationSyncService( if (type != DailyWorkScheduleType.WORK) return if (clockInTime == null || clockOutTime == null) return - val hasCancelled = notificationLogRepository - .existsByMemberIdAndScheduledDateAndStatus(memberId, date, NotificationStatus.CANCELLED) - if (!hasCancelled) return - val now = LocalDateTime.now(ZoneId.of("Asia/Seoul")) val logs = mutableListOf() val truncatedIn = truncateToMinute(clockInTime) val truncatedOut = truncateToMinute(clockOutTime) val isMidnightCrossing = truncatedOut < truncatedIn + val relatedDates = listOf(date, date.plusDays(1)) + val existingLogs = notificationLogRepository + .findAllByMemberIdAndScheduledDateInAndNotificationTypeIn( + memberId, + relatedDates, + workNotificationTypes, + ) - if (date.atTime(truncatedIn).isAfter(now)) { + if (date.atTime(truncatedIn).isAfter(now) && existingLogs.none { + it.notificationType == NotificationType.CLOCK_IN && + it.scheduledDate == date && + it.status != NotificationStatus.CANCELLED + } + ) { logs.add(NotificationLog(memberId, NotificationType.CLOCK_IN, date, truncatedIn)) } val clockOutDate = if (isMidnightCrossing) date.plusDays(1) else date - if (clockOutDate.atTime(truncatedOut).isAfter(now)) { + if (clockOutDate.atTime(truncatedOut).isAfter(now) && existingLogs.none { + it.notificationType == NotificationType.CLOCK_OUT && + it.scheduledDate == clockOutDate && + it.status != NotificationStatus.CANCELLED + } + ) { logs.add(NotificationLog(memberId, NotificationType.CLOCK_OUT, clockOutDate, truncatedOut)) } if (logs.isNotEmpty()) { notificationLogRepository.saveAll(logs) log.info( - "Restored {} notifications for member {} on {} (vacation -> work)", + "Ensured {} upcoming work notifications for member {} on {}", logs.size, memberId, date, ) } From f3046fcd00d2bb72aa6667913dd4c1edd64b96ea Mon Sep 17 00:00:00 2001 From: jeyong Date: Sat, 21 Mar 2026 17:22:02 +0900 Subject: [PATCH 4/4] Update NotificationLogRepository and NotificationSyncService to improve pending work notification retrieval logic --- .../repository/NotificationLogRepository.kt | 7 +++++++ .../notification/NotificationSyncService.kt | 19 +++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt index c1facec..05d32b9 100644 --- a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt +++ b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt @@ -20,6 +20,13 @@ interface NotificationLogRepository : JpaRepository { status: NotificationStatus, ): List + fun findAllByMemberIdAndScheduledDateAndNotificationTypeAndStatus( + memberId: Long, + scheduledDate: LocalDate, + notificationType: NotificationType, + status: NotificationStatus, + ): List + fun findAllByMemberIdAndScheduledDateInAndNotificationTypeIn( memberId: Long, scheduledDates: Collection, diff --git a/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt b/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt index 88ab091..3b19472 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationSyncService.kt @@ -28,10 +28,7 @@ class NotificationSyncService( clockInTime: LocalTime?, clockOutTime: LocalTime?, ) { - val relatedDates = listOf(date, date.plusDays(1)) - val pendingLogs = notificationLogRepository - .findAllByMemberIdAndScheduledDateInAndStatus(memberId, relatedDates, NotificationStatus.PENDING) - .filter { it.notificationType != NotificationType.PAYDAY } + val pendingLogs = findPendingWorkNotifications(memberId, date) if (pendingLogs.isEmpty()) { ensureUpcomingWorkNotifications(memberId, date, type, clockInTime, clockOutTime) @@ -73,6 +70,20 @@ class NotificationSyncService( log.info("Synced pending notifications for member {} on {}", memberId, date) } + private fun findPendingWorkNotifications(memberId: Long, date: LocalDate): List { + val sameDayLogs = notificationLogRepository + .findAllByMemberIdAndScheduledDateInAndStatus(memberId, listOf(date), NotificationStatus.PENDING) + .filter { it.notificationType != NotificationType.PAYDAY } + val nextDayClockOutLogs = notificationLogRepository + .findAllByMemberIdAndScheduledDateAndNotificationTypeAndStatus( + memberId, + date.plusDays(1), + NotificationType.CLOCK_OUT, + NotificationStatus.PENDING, + ) + return sameDayLogs + nextDayClockOutLogs + } + private fun ensureUpcomingWorkNotifications( memberId: Long, date: LocalDate,