Skip to content

Commit eb0e428

Browse files
committed
푸시 알림 추가 (#206)
#205 ### Proposed Changes (PR 목적과 변경사항을 적어주세요.) ### Code Review Point (리뷰어가 중점적으로 보면 좋을 부분을 적어주세요.) (cherry picked from commit c13ac7e)
1 parent ea3c7d7 commit eb0e428

File tree

7 files changed

+150
-61
lines changed

7 files changed

+150
-61
lines changed

src/main/kotlin/com/ssak3/timeattack/notifications/domain/FcmNotificationConstants.kt

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
package com.ssak3.timeattack.notifications.domain
22

3-
import kotlin.random.Random
4-
53
object FcmNotificationConstants {
6-
fun getMessage(order: Int): String {
7-
val messages = messageTemplate[order] ?: throw IllegalStateException("message not exist for this number")
8-
val index = Random.nextInt(messages.size)
9-
return messages[index]
4+
fun getMessage(index: Int): String {
5+
val messages =
6+
taskBeforeMessageTemplate[index] ?: throw IllegalStateException("message not exist for this number")
7+
return messages
8+
}
9+
10+
fun getRemindMessage(index: Int): String {
11+
val message = remindMessageTemplate[index] ?: throw IllegalStateException("message not exist for this number")
12+
return message
1013
}
1114

1215
fun getSupportMessage(
1316
personaId: Int,
17+
personaName: String,
18+
nickname: String,
1419
index: Int,
1520
): String {
16-
val messages =
21+
val supportMessage =
1722
supportMessageTemplate[personaId] ?: throw IllegalStateException("message not exist for this number")
18-
return messages[index]
23+
val message =
24+
"""
25+
$personaName ${nickname}
26+
${supportMessage[index]}
27+
""".trimIndent()
28+
return message
1929
}
2030

2131
fun getRoute(order: Int): String {
@@ -31,36 +41,57 @@ object FcmNotificationConstants {
3141

3242
private const val REMINDER_LIMIT = 3
3343

34-
private val messageTemplate =
44+
private val taskBeforeMessageTemplate =
3545
mapOf(
3646
0 to
37-
listOf(
38-
"""
39-
작업 시간이 다 되었어요!
40-
작은 행동부터 시작해볼까요?
41-
""".trimIndent(),
42-
),
47+
"""
48+
작업 시간이 다 되었어요!
49+
작은 행동부터 시작해볼까요?
50+
""".trimIndent(),
4351
1 to
44-
listOf(
45-
"""
46-
이제 두 번의 기회만 남았어요!
47-
미루기 전에 얼른 시작해볼까요?
48-
""".trimIndent(),
49-
),
52+
"""
53+
아직 할 일을 시작하지 않으셨네요.
54+
그리 어렵지 않아요. 1분만 투자해볼까요?
55+
""".trimIndent(),
5056
2 to
51-
listOf(
52-
"""
53-
한번만 더 알림오고 끝이에요!
54-
작업을 미루기 전에 얼른 시작해보세요!
55-
""".trimIndent(),
56-
),
57+
"""
58+
혹시 까먹으셨나요? 아직 시작하지 않으셨어요!
59+
시작 타이밍, 지금 딱 좋아요. 기회는 계속 안 옵니다!
60+
""".trimIndent(),
5761
3 to
58-
listOf(
59-
"""
60-
이게 마지막 기회에요!
61-
더 미루면 알림도 포기할거에요. 당장 시작하세요!
62-
""".trimIndent(),
63-
),
62+
"""
63+
이제 3번 중 2번 남았습니다. 더 미루면 놓쳐요!
64+
다음 알림까진 2분… 진짜 시작해볼까요?
65+
""".trimIndent(),
66+
4 to
67+
"""
68+
마지막 기회 하나 남았습니다. 진짜 이번엔 시작해야 해요.
69+
이제 끝입니다. 알림은 여기까지만 참을게요!
70+
""".trimIndent(),
71+
5 to
72+
"""
73+
더 이상 알림은 없습니다. 지금이 진짜 마지막 찬스 🔥
74+
지금 안 하면 오늘도 미룸 예약!
75+
""".trimIndent(),
76+
)
77+
78+
private val remindMessageTemplate =
79+
mapOf(
80+
0 to
81+
"""
82+
이제 두 번의 기회만 남았어요!
83+
미루기 전에 얼른 시작해볼까요?
84+
""".trimIndent(),
85+
1 to
86+
"""
87+
한번만 더 알림오고 끝이에요!
88+
작업을 미루기 전에 얼른 시작해보세요!
89+
""".trimIndent(),
90+
2 to
91+
"""
92+
이게 마지막 기회에요!
93+
더 미루면 알림도 포기할거에요. 당장 시작하세요!
94+
""".trimIndent(),
6495
)
6596

6697
/**

src/main/kotlin/com/ssak3/timeattack/notifications/service/PushNotificationListener.kt

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.ssak3.timeattack.notifications.service
33
import com.ssak3.timeattack.common.utils.Logger
44
import com.ssak3.timeattack.member.service.MemberService
55
import com.ssak3.timeattack.notifications.domain.FcmNotificationConstants.getMessage
6+
import com.ssak3.timeattack.notifications.domain.FcmNotificationConstants.getRemindMessage
67
import com.ssak3.timeattack.notifications.domain.FcmNotificationConstants.getSupportMessage
78
import com.ssak3.timeattack.notifications.domain.PushNotification
89
import com.ssak3.timeattack.task.service.TaskService
@@ -33,16 +34,18 @@ class PushNotificationListener(
3334
val member = memberService.getMemberById(event.memberId)
3435
val task = taskService.getTaskById(event.taskId)
3536

36-
val pushNotification =
37-
PushNotification(
38-
member = member,
39-
task = task,
40-
scheduledAt = event.alarmTime.withSecond(0),
41-
order = 0,
42-
message = getMessage(0),
43-
)
37+
val pushNotifications =
38+
event.alarmTimes.mapIndexed { i, alarmTime ->
39+
PushNotification(
40+
member = member,
41+
task = task,
42+
scheduledAt = alarmTime,
43+
order = 0,
44+
message = getMessage(i),
45+
)
46+
}
4447

45-
pushNotificationService.save(pushNotification)
48+
pushNotificationService.saveAll(pushNotifications)
4649
}
4750

4851
@EventListener
@@ -51,14 +54,18 @@ class PushNotificationListener(
5154
val member = memberService.getMemberById(event.memberId)
5255
val task = taskService.getTaskById(event.taskId)
5356

57+
// 기존 알림 제거
58+
pushNotificationService.delete(task)
59+
60+
// 리마인드 알림 등록
5461
val pushNotifications: List<PushNotification> =
55-
event.alarmTimes.map {
62+
event.alarmTimes.mapIndexed { i, alarmTime ->
5663
PushNotification(
5764
member = member,
5865
task = task,
59-
scheduledAt = it.alarmTime.withSecond(0),
60-
order = it.order,
61-
message = getMessage(it.order),
66+
scheduledAt = alarmTime,
67+
order = i + 1,
68+
message = getRemindMessage(i),
6269
)
6370
}
6471

@@ -104,14 +111,23 @@ class PushNotificationListener(
104111
val member = memberService.getMemberById(event.memberId)
105112
val task = taskService.getTaskById(event.taskId)
106113

114+
// 기존 알림 제거
115+
pushNotificationService.delete(task)
116+
107117
val pushNotifications: List<PushNotification> =
108118
event.alarmTimes.map {
109119
PushNotification(
110120
member = member,
111121
task = task,
112122
scheduledAt = it.alarmTime.withSecond(0),
113123
order = -1,
114-
message = getSupportMessage(personaId = task.persona.id.toInt(), index = it.index),
124+
message =
125+
getSupportMessage(
126+
personaId = task.persona.id.toInt(),
127+
personaName = task.persona.name,
128+
nickname = member.nickname,
129+
index = it.index,
130+
),
115131
)
116132
}
117133

src/main/kotlin/com/ssak3/timeattack/task/scheduler/OverdueTaskStatusUpdateScheduler.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@ import com.ssak3.timeattack.common.exception.ApplicationException
44
import com.ssak3.timeattack.common.exception.ApplicationExceptionType
55
import com.ssak3.timeattack.common.utils.Logger
66
import com.ssak3.timeattack.common.utils.checkNotNull
7+
import com.ssak3.timeattack.external.firebase.domain.DevicePlatform
8+
import com.ssak3.timeattack.notifications.domain.FcmMessage
9+
import com.ssak3.timeattack.notifications.service.FcmDeviceService
10+
import com.ssak3.timeattack.notifications.service.FcmPushNotificationService
11+
import com.ssak3.timeattack.retrospection.repository.RetrospectionRepository
712
import com.ssak3.timeattack.task.domain.Task
813
import com.ssak3.timeattack.task.domain.TaskStatus
914
import com.ssak3.timeattack.task.domain.TaskStatus.Companion.statusesToFail
1015
import com.ssak3.timeattack.task.domain.TaskStatus.FAIL
1116
import com.ssak3.timeattack.task.domain.TaskStatus.FOCUSED
1217
import com.ssak3.timeattack.task.repository.TaskRepository
1318
import org.springframework.context.event.EventListener
19+
import org.springframework.data.repository.findByIdOrNull
1420
import org.springframework.scheduling.TaskScheduler
1521
import org.springframework.stereotype.Service
1622
import org.springframework.transaction.support.TransactionTemplate
@@ -19,6 +25,9 @@ import java.time.ZoneId
1925
@Service
2026
class OverdueTaskStatusUpdateScheduler(
2127
private val taskRepository: TaskRepository,
28+
private val retrospectionRepository: RetrospectionRepository,
29+
private val fcmPushNotificationService: FcmPushNotificationService,
30+
private val fcmDeviceService: FcmDeviceService,
2231
private val taskScheduler: TaskScheduler,
2332
private val transactionTemplate: TransactionTemplate,
2433
) : Logger {
@@ -34,6 +43,41 @@ class OverdueTaskStatusUpdateScheduler(
3443
)
3544

3645
logger.info("Task(${task.id}) 상태 체크 스케줄러 등록 완료: 예정 실행 시간 = $scheduledTime")
46+
47+
val scheduledTimeForPushNotification = task.dueDatetime.plusMinutes(30)
48+
taskScheduler.schedule(
49+
{ checkRetrospectionAndSendPushNotification(task.id) },
50+
scheduledTimeForPushNotification.atZone(ZoneId.systemDefault()).toInstant(),
51+
)
52+
53+
logger.info("Task(${task.id}) 회고 푸시 알림 등록 완료: 예정 실행 시간 = $scheduledTime")
54+
}
55+
56+
private fun checkRetrospectionAndSendPushNotification(taskId: Long) {
57+
val isExist = retrospectionRepository.findByTaskId(taskId) != null
58+
val task =
59+
taskRepository.findByIdOrNull(taskId)
60+
?: throw ApplicationException(ApplicationExceptionType.TASK_NOT_FOUND_BY_ID, taskId)
61+
val memberId = checkNotNull(task.member.id, "memberId")
62+
63+
if (!isExist) {
64+
fcmDeviceService.getDevicesByMember(memberId).forEach { device ->
65+
val message =
66+
FcmMessage(
67+
token = device.fcmRegistrationToken,
68+
platform = DevicePlatform.valueOf(device.devicePlatform.toString()),
69+
taskId = checkNotNull(task.id, "task id"),
70+
body =
71+
"""
72+
PPT 만들고 대본 작성 마감일이 끝났어요!
73+
회고를 작성하며 과정을 돌아보세요.
74+
""".trimIndent(),
75+
route = "/retrospection",
76+
)
77+
78+
fcmPushNotificationService.sendNotification(message)
79+
}
80+
}
3781
}
3882

3983
/**

src/main/kotlin/com/ssak3/timeattack/task/service/TaskService.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import com.ssak3.timeattack.task.repository.TaskModeRepository
2121
import com.ssak3.timeattack.task.repository.TaskRepository
2222
import com.ssak3.timeattack.task.repository.TaskTypeRepository
2323
import com.ssak3.timeattack.task.service.events.DeleteTaskNotificationEvent
24-
import com.ssak3.timeattack.task.service.events.ReminderAlarm
2524
import com.ssak3.timeattack.task.service.events.ReminderSaveEvent
2625
import com.ssak3.timeattack.task.service.events.SupportAlarm
2726
import com.ssak3.timeattack.task.service.events.SupportNotificationSaveEvent
@@ -102,11 +101,12 @@ class TaskService(
102101
val savedTaskEntity = taskRepository.save(task.toEntity())
103102

104103
// 4. Task 이벤트 발행
104+
val alarmTimes = getAlarmTimes(scheduledTaskRequest.triggerActionAlarmTime)
105105
val triggerActionNotificationSaveEvent =
106106
TriggerActionNotificationSaveEvent(
107107
checkNotNull(member.id),
108108
checkNotNull(savedTaskEntity.id),
109-
scheduledTaskRequest.triggerActionAlarmTime,
109+
alarmTimes,
110110
)
111111
eventPublisher.publishEvent(triggerActionNotificationSaveEvent)
112112

@@ -119,6 +119,10 @@ class TaskService(
119119
return savedTask
120120
}
121121

122+
// 2분단위로 첫번째 알림 + 5번 추가 알림 = 6번
123+
private fun getAlarmTimes(startTime: LocalDateTime): List<LocalDateTime> =
124+
generateSequence(startTime) { it.plusMinutes(2) }.take(6).map { it.withSecond(0).withNano(0) }.toList()
125+
122126
private fun findPersonaByTaskTypeAndTaskMode(
123127
taskType: String,
124128
taskMode: String,
@@ -271,7 +275,7 @@ class TaskService(
271275
taskHoldOffRequest.remindInterval * order.toLong(),
272276
)
273277
task.validateReminderAlarmTime(nextReminderAlarmTime)
274-
ReminderAlarm(order, nextReminderAlarmTime)
278+
nextReminderAlarmTime
275279
}
276280

277281
// 3. 리마인더 알림 저장 이벤트 발행

src/main/kotlin/com/ssak3/timeattack/task/service/events/ReminderSaveEvent.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,5 @@ import java.time.LocalDateTime
55
data class ReminderSaveEvent(
66
val memberId: Long,
77
val taskId: Long,
8-
val alarmTimes: List<ReminderAlarm>,
9-
)
10-
11-
data class ReminderAlarm(
12-
val order: Int,
13-
val alarmTime: LocalDateTime,
8+
val alarmTimes: List<LocalDateTime>,
149
)

src/main/kotlin/com/ssak3/timeattack/task/service/events/TriggerActionNotificationSaveEvent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ import java.time.LocalDateTime
55
class TriggerActionNotificationSaveEvent(
66
val memberId: Long,
77
val taskId: Long,
8-
val alarmTime: LocalDateTime,
8+
val alarmTimes: List<LocalDateTime>,
99
)

src/test/kotlin/com/ssak3/timeattack/task/service/TaskServiceEventTest.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import com.ssak3.timeattack.task.repository.TaskRepository
1515
import com.ssak3.timeattack.task.repository.TaskTypeRepository
1616
import com.ssak3.timeattack.task.repository.entity.TaskModeEntity
1717
import com.ssak3.timeattack.task.repository.entity.TaskTypeEntity
18-
import com.ssak3.timeattack.task.service.events.ReminderAlarm
1918
import com.ssak3.timeattack.task.service.events.ReminderSaveEvent
2019
import io.mockk.every
2120
import org.assertj.core.api.Assertions.assertThat
@@ -138,9 +137,9 @@ class TaskServiceEventTest(
138137

139138
val expectedReminderAlarms =
140139
listOf(
141-
ReminderAlarm(1, LocalDateTime.of(2025, 1, 1, 0, 15, 0)),
142-
ReminderAlarm(2, LocalDateTime.of(2025, 1, 1, 0, 30, 0)),
143-
ReminderAlarm(3, LocalDateTime.of(2025, 1, 1, 0, 45, 0)),
140+
LocalDateTime.of(2025, 1, 1, 0, 15, 0),
141+
LocalDateTime.of(2025, 1, 1, 0, 30, 0),
142+
LocalDateTime.of(2025, 1, 1, 0, 45, 0),
144143
)
145144
assertThat(reminderSaveEvent.alarmTimes).containsExactlyElementsOf(expectedReminderAlarms)
146145
}

0 commit comments

Comments
 (0)