Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ class WorkScheduleTime private constructor(

companion object {
fun of(clockIn: LocalTime, clockOut: LocalTime) = WorkScheduleTime(
clockInTime = LocalTime.of(clockIn.hour, clockIn.minute),
clockOutTime = LocalTime.of(clockOut.hour, clockOut.minute),
clockInTime = normalize(clockIn),
clockOutTime = normalize(clockOut),
)

fun normalize(time: LocalTime): LocalTime = LocalTime.of(time.hour, time.minute)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.moa.service.notification

import org.springframework.stereotype.Component
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId

@Component
class NotificationScheduleClock {
fun now(): LocalDateTime = LocalDateTime.now(ZoneId.of("Asia/Seoul"))

fun isFutureSchedule(date: LocalDate, time: LocalTime, now: LocalDateTime = now()): Boolean =
date.atTime(time).isAfter(now)
Comment on lines +10 to +14
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId

@Service
class NotificationSyncService(
private val notificationLogRepository: NotificationLogRepository,
private val clock: NotificationScheduleClock,
) {
private val log = LoggerFactory.getLogger(javaClass)
private val workNotificationTypes = listOf(NotificationType.CLOCK_IN, NotificationType.CLOCK_OUT)
Expand Down Expand Up @@ -49,7 +48,7 @@ class NotificationSyncService(
when (pendingLog.notificationType) {
NotificationType.CLOCK_IN -> {
clockInTime?.let {
pendingLog.scheduledTime = LocalTime.of(it.hour, it.minute)
pendingLog.scheduledTime = WorkScheduleTime.normalize(it)
}
pendingLog.scheduledDate = date
}
Expand All @@ -60,7 +59,7 @@ class NotificationSyncService(
pendingLog.scheduledDate = schedule.clockOutDate(date)
pendingLog.scheduledTime = schedule.clockOutTime
} else if (clockOutTime != null) {
pendingLog.scheduledTime = LocalTime.of(clockOutTime.hour, clockOutTime.minute)
pendingLog.scheduledTime = WorkScheduleTime.normalize(clockOutTime)
pendingLog.scheduledDate = date
}
}
Expand All @@ -75,7 +74,7 @@ class NotificationSyncService(
private fun findPendingWorkNotifications(memberId: Long, date: LocalDate): List<NotificationLog> {
val sameDayLogs = notificationLogRepository
.findAllByMemberIdAndScheduledDateInAndStatus(memberId, listOf(date), NotificationStatus.PENDING)
.filter { it.notificationType != NotificationType.PAYDAY && it.notificationType != NotificationType.PUBLIC_HOLIDAY }
.filter { it.notificationType in workNotificationTypes }
val nextDayClockOutLogs = notificationLogRepository
.findAllByMemberIdAndScheduledDateAndNotificationTypeAndStatus(
memberId,
Expand All @@ -98,7 +97,7 @@ class NotificationSyncService(
if (type != DailyWorkScheduleType.WORK) return
if (clockInTime == null || clockOutTime == null) return

val now = LocalDateTime.now(ZoneId.of("Asia/Seoul"))
val now = clock.now()
val schedule = WorkScheduleTime.of(clockInTime, clockOutTime)
val clockOutDate = schedule.clockOutDate(date)
val relatedDates = listOf(date, date.plusDays(1))
Expand All @@ -111,7 +110,7 @@ class NotificationSyncService(

val logs = mutableListOf<NotificationLog>()

if (date.atTime(schedule.clockInTime).isAfter(now) && existingLogs.none {
if (clock.isFutureSchedule(date, schedule.clockInTime, now) && existingLogs.none {
it.notificationType == NotificationType.CLOCK_IN &&
it.scheduledDate == date &&
it.status != NotificationStatus.CANCELLED
Expand All @@ -120,7 +119,7 @@ class NotificationSyncService(
logs.add(NotificationLog(memberId, NotificationType.CLOCK_IN, date, schedule.clockInTime))
}

if (clockOutDate.atTime(schedule.clockOutTime).isAfter(now) && existingLogs.none {
if (clock.isFutureSchedule(clockOutDate, schedule.clockOutTime, now) && existingLogs.none {
it.notificationType == NotificationType.CLOCK_OUT &&
it.scheduledDate == clockOutDate &&
it.status != NotificationStatus.CANCELLED
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.moa.service.notification

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId

class NotificationScheduleClockTest {

private val sut = NotificationScheduleClock()

@Test
fun `now 는 Asia_Seoul timezone 기준 현재 시각을 반환한다`() {
val before = LocalDateTime.now(ZoneId.of("Asia/Seoul"))
val now = sut.now()
val after = LocalDateTime.now(ZoneId.of("Asia/Seoul"))

assertThat(now).isBetween(before, after)
}

@Test
fun `명시한 now 보다 1분 미래인 스케줄은 future 로 판정한다`() {
val now = LocalDateTime.of(2026, 5, 22, 13, 30)
val date = LocalDate.of(2026, 5, 22)
val time = LocalTime.of(13, 31)

assertThat(sut.isFutureSchedule(date, time, now)).isTrue()
}

@Test
fun `명시한 now 보다 1분 과거인 스케줄은 future 가 아니다`() {
val now = LocalDateTime.of(2026, 5, 22, 13, 30)
val date = LocalDate.of(2026, 5, 22)
val time = LocalTime.of(13, 29)

assertThat(sut.isFutureSchedule(date, time, now)).isFalse()
}

@Test
fun `명시한 now 와 정확히 같은 시각은 future 가 아니다`() {
// isAfter 는 strict 비교라 동일 시각은 false 가 맞다.
val now = LocalDateTime.of(2026, 5, 22, 13, 30)
val date = LocalDate.of(2026, 5, 22)
val time = LocalTime.of(13, 30)

assertThat(sut.isFutureSchedule(date, time, now)).isFalse()
}

@Test
fun `자정 직전 now 에서 다음날 00시 00분 스케줄은 future 로 판정한다`() {
val now = LocalDateTime.of(2026, 5, 22, 23, 59)
val nextDay = LocalDate.of(2026, 5, 23)
val midnight = LocalTime.of(0, 0)

assertThat(sut.isFutureSchedule(nextDay, midnight, now)).isTrue()
}
}
Loading