Skip to content

Commit 9a2304d

Browse files
authored
Merge pull request #257 from depromeet/feature/talk-calendar-reminder
[Feat] 톡캘린더 일정에 시작 5분 전 알림 추가
2 parents c7c30a4 + 496065f commit 9a2304d

6 files changed

Lines changed: 70 additions & 1 deletion

File tree

docs/superpowers/specs/2026-08-18-kakao-talk-calendar-design.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ JWT 서명 키를 재사용하지 않는 이유는 수명주기가 묶이기 때
337337
| 종료 | `startedAt` + 30분 |
338338
| 타임존 | `Asia/Seoul`. 카카오에 넘기는 시각은 UTC RFC5545 형식 |
339339
| 설명 | `초대 링크: {URL}` 한 줄. URL 은 `PartyInviteService.findLatestUsableInviteToken` 의 토큰과 `app.web-base-url` 을 조합. 사용 가능한 초대가 없으면 빈 문자열 |
340+
| 미리 알림 | 시작 5분 전 하나. 카카오는 `0(시작 시각) < 값 ≤ 43200` 만 받으므로 시작 시각 알림은 지정할 수 없다 |
340341

341342
파티 엔티티의 `endedAt()`은 롤링페이퍼 열람 기한인 7일 뒤라 캘린더 일정 종료 시각으로 쓰지 않는다.
342343

src/main/kotlin/com/team2/server/calendar/domain/policy/PartyCalendarEventPolicy.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ object PartyCalendarEventPolicy {
1010
/** 카카오 톡캘린더 일정 제목 제한. */
1111
const val MAX_TITLE_LENGTH = 50
1212

13+
/**
14+
* 미리 알림 시점(분).
15+
*
16+
* 카카오는 `0 < 값 ≤ 43200` 만 받으므로 시작 시각 알림(0)은 지정할 수 없다.
17+
* 파티가 [DURATION_MINUTES] 분짜리라 한 번이면 충분하다.
18+
*/
19+
const val REMINDER_MINUTES_BEFORE = 5
20+
1321
fun compose(
1422
kind: CelebrationKind,
1523
celebrantName: String?,
@@ -21,6 +29,7 @@ object PartyCalendarEventPolicy {
2129
startAt = startedAt,
2230
endAt = startedAt.plusMinutes(DURATION_MINUTES),
2331
description = inviteUrl?.let { "초대 링크: $it" } ?: "",
32+
reminderMinutes = listOf(REMINDER_MINUTES_BEFORE),
2433
)
2534

2635
private fun composeTitle(

src/main/kotlin/com/team2/server/calendar/domain/vo/CalendarEvent.kt

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,32 @@ package com.team2.server.calendar.domain.vo
22

33
import java.time.LocalDateTime
44

5+
private const val REMINDER_MIN_MINUTES = 5
6+
private const val REMINDER_MAX_MINUTES = 43_200
7+
private const val REMINDER_INTERVAL_MINUTES = 5
8+
private const val REMINDER_MAX_COUNT = 2
9+
510
data class CalendarEvent(
611
val title: String,
712
val startAt: LocalDateTime,
813
val endAt: LocalDateTime,
914
val description: String,
10-
)
15+
/** 미리 알림 시점(분). 카카오는 5분 간격으로 최대 2개까지 받는다. */
16+
val reminderMinutes: List<Int>,
17+
) {
18+
/**
19+
* 카카오 제약을 여기서 막는다. 통과시키면 일정 생성 요청이 400 으로 거부되는데,
20+
* 어댑터가 그것을 `KAKAO_CALENDAR_UNAVAILABLE` 로 바꿔 카카오 장애처럼 보이게 된다.
21+
*/
22+
init {
23+
require(reminderMinutes.size <= REMINDER_MAX_COUNT) {
24+
"미리 알림은 최대 ${REMINDER_MAX_COUNT}개다 (현재 ${reminderMinutes.size}개)"
25+
}
26+
require(reminderMinutes.all { it in REMINDER_MIN_MINUTES..REMINDER_MAX_MINUTES }) {
27+
"미리 알림은 $REMINDER_MIN_MINUTES~$REMINDER_MAX_MINUTES 분이어야 한다 (현재 $reminderMinutes)"
28+
}
29+
require(reminderMinutes.all { it % REMINDER_INTERVAL_MINUTES == 0 }) {
30+
"미리 알림은 ${REMINDER_INTERVAL_MINUTES}분 간격이어야 한다 (현재 $reminderMinutes)"
31+
}
32+
}
33+
}

src/main/kotlin/com/team2/server/calendar/infrastructure/kakao/KakaoTalkCalendarAdapter.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class KakaoTalkCalendarAdapter(
118118
"all_day" to false,
119119
),
120120
"description" to event.description,
121+
"reminders" to event.reminderMinutes,
121122
),
122123
)
123124

src/test/kotlin/com/team2/server/calendar/domain/policy/PartyCalendarEventPolicyTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.team2.server.calendar.domain.policy
22

3+
import com.team2.server.calendar.domain.vo.CalendarEvent
34
import com.team2.server.calendar.domain.vo.CelebrationKind
45
import org.junit.jupiter.api.Test
56
import java.time.LocalDateTime
67
import kotlin.test.assertEquals
8+
import kotlin.test.assertFailsWith
79
import kotlin.test.assertFalse
810
import kotlin.test.assertTrue
911

@@ -118,4 +120,35 @@ class PartyCalendarEventPolicyTest {
118120

119121
assertEquals("", event.description)
120122
}
123+
124+
@Test
125+
fun `미리 알림은 시작 5분 전 하나다`() {
126+
// 카카오는 0(시작 시각) 을 허용하지 않으므로 시작 시각 알림은 지정할 수 없다.
127+
val event =
128+
PartyCalendarEventPolicy.compose(
129+
kind = CelebrationKind.BIRTHDAY,
130+
celebrantName = "지민",
131+
startedAt = LocalDateTime.of(2026, 8, 20, 19, 0),
132+
inviteUrl = null,
133+
)
134+
135+
assertEquals(listOf(5), event.reminderMinutes)
136+
}
137+
138+
@Test
139+
fun `카카오 제약을 벗어난 미리 알림은 일정을 만들 수 없다`() {
140+
val rejected = listOf(listOf(0), listOf(3), listOf(43205), listOf(-5), listOf(5, 10, 15))
141+
142+
for (reminders in rejected) {
143+
assertFailsWith<IllegalArgumentException>(message = "$reminders") {
144+
CalendarEvent(
145+
title = "제목",
146+
startAt = startedAt,
147+
endAt = startedAt.plusMinutes(30),
148+
description = "",
149+
reminderMinutes = reminders,
150+
)
151+
}
152+
}
153+
}
121154
}

src/test/kotlin/com/team2/server/calendar/infrastructure/kakao/KakaoTalkCalendarAdapterTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class KakaoTalkCalendarAdapterTest {
4242
startAt = LocalDateTime.of(2026, 8, 20, 19, 0),
4343
endAt = LocalDateTime.of(2026, 8, 20, 19, 30),
4444
description = "초대 링크: https://example.com/invite/abc",
45+
reminderMinutes = listOf(5),
4546
)
4647

4748
@Test
@@ -54,6 +55,7 @@ class KakaoTalkCalendarAdapterTest {
5455
.andExpect(content().string(org.hamcrest.Matchers.containsString("calendar_id=primary")))
5556
.andExpect(content().string(org.hamcrest.Matchers.containsString("2026-08-20T10%3A00%3A00Z")))
5657
.andExpect(content().string(org.hamcrest.Matchers.containsString("2026-08-20T10%3A30%3A00Z")))
58+
.andExpect(content().string(org.hamcrest.Matchers.containsString("%22reminders%22%3A%5B5%5D")))
5759
.andRespond(withSuccess("""{"event_id":"event-1"}""", MediaType.APPLICATION_JSON))
5860

5961
val eventId = adapter.createEvent("kakao-token", event)

0 commit comments

Comments
 (0)