Skip to content

Commit a49c47b

Browse files
authored
푸시 개발 (초대장, 모임 정기 푸시) (#26)
* feat: 푸시 API 개발 (모임 푸시, 초대장 푸시) * feat: 푸시 API 개발 (모임 푸시, 초대장 푸시) * fix: test * refactor: 불필요 코드 제거
1 parent 6ec35d2 commit a49c47b

File tree

22 files changed

+219
-150
lines changed

22 files changed

+219
-150
lines changed

tuk-api/src/main/kotlin/nexters/tuk/application/device/DeviceService.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ package nexters.tuk.application.device
22

33
import nexters.tuk.application.device.dto.request.DeviceCommand
44
import nexters.tuk.application.device.dto.response.DeviceResponse
5-
import nexters.tuk.contract.BaseException
6-
import nexters.tuk.contract.ErrorType
75
import nexters.tuk.domain.device.Device
86
import nexters.tuk.domain.device.DeviceRepository
9-
import org.slf4j.LoggerFactory
107
import org.springframework.stereotype.Service
118
import org.springframework.transaction.annotation.Transactional
129

@@ -49,4 +46,19 @@ class DeviceService(
4946
updatedAt = device.updatedAt
5047
)
5148
}
49+
50+
@Transactional(readOnly = true)
51+
fun getDeviceTokens(memberIds: List<Long>): List<DeviceResponse.MemberDeviceToken> {
52+
if (memberIds.isEmpty()) return emptyList()
53+
54+
val devices = deviceRepository.findByMemberIdIn(memberIds)
55+
if (devices.isEmpty()) return emptyList()
56+
57+
return devices.mapNotNull { device ->
58+
DeviceResponse.MemberDeviceToken(
59+
memberId = device.memberId,
60+
deviceToken = device.deviceToken ?: return@mapNotNull null
61+
)
62+
}
63+
}
5264
}

tuk-api/src/main/kotlin/nexters/tuk/application/device/dto/response/DeviceResponse.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@ class DeviceResponse {
99
val deviceToken: String,
1010
val updatedAt: LocalDateTime,
1111
)
12+
13+
data class MemberDeviceToken(
14+
val memberId: Long,
15+
val deviceToken: String,
16+
)
1217
}

tuk-api/src/main/kotlin/nexters/tuk/application/gathering/GatheringMemberService.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import org.springframework.transaction.annotation.Transactional
1010
@Service
1111
class GatheringMemberService(
1212
private val gatheringRepository: GatheringRepository,
13-
private val gatheringMemberRepository: GatheringMemberRepository
13+
private val gatheringMemberRepository: GatheringMemberRepository,
1414
) {
1515
@Transactional
1616
fun joinGathering(gatheringId: Long, memberId: Long): GatheringMemberResponse.JoinGathering {
@@ -20,7 +20,7 @@ class GatheringMemberService(
2020
}
2121

2222
val gatheringMember = GatheringMember.registerMember(gathering, memberId)
23-
.let { gatheringMemberRepository.save(it) }
23+
.let { gatheringMemberRepository.save(it) }
2424

2525
return GatheringMemberResponse.JoinGathering(gatheringMember.id)
2626
}
@@ -34,11 +34,11 @@ class GatheringMemberService(
3434
val gatheringMembers = gatheringMemberRepository.findAllByMemberId(memberId)
3535

3636
return gatheringMembers
37-
.map { it.gathering }
38-
.map {
37+
.map { memberGathering ->
3938
GatheringMemberResponse.MemberGatherings(
40-
it.id,
41-
it.name,
39+
memberGathering.gathering.id,
40+
memberGathering.gathering.name,
41+
memberGathering.gathering.intervalDays.toInt(),
4242
)
4343
}.sortedBy { it.name }
4444
}

tuk-api/src/main/kotlin/nexters/tuk/application/gathering/GatheringQueryService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class GatheringQueryService(
2424
GatheringResponse.GatheringOverviews.GatheringOverview(
2525
gatheringId = it.id,
2626
gatheringName = it.name,
27-
lastNotificationRelativeTime = RelativeTime.fromDays(0)
27+
lastNotificationRelativeTime = RelativeTime.fromDays(it.pushIntervalDays)
2828
)
2929
}
3030

tuk-api/src/main/kotlin/nexters/tuk/application/gathering/dto/response/GatheringMemberResponse.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import io.swagger.v3.oas.annotations.media.Schema
55
class GatheringMemberResponse {
66
@Schema(name = "JoinGatheringResponse")
77
data class JoinGathering(
8-
val id: Long
8+
val id: Long,
99
)
1010

1111
data class MemberGatherings(
1212
val id: Long,
1313
val name: String,
14+
val pushIntervalDays: Int,
1415
)
1516
}

tuk-api/src/main/kotlin/nexters/tuk/application/gathering/dto/response/GatheringResponse.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class GatheringResponse {
77
@Schema(name = "GenerateResponse")
88
data class Generate(
99
@Schema(description = "생성된 모임 id")
10-
val gatheringId: Long
10+
val gatheringId: Long,
1111
)
1212

1313
@Schema(name = "GatheringOverviewsResponse")
@@ -23,7 +23,7 @@ class GatheringResponse {
2323
@Schema(description = "모임명")
2424
val gatheringName: String,
2525
@Schema(description = "상대 시간 타입 - \"오늘\", \"n일 전\", \"n주 전\", \"n개월 전\", \"n년 전\" ")
26-
val lastNotificationRelativeTime: RelativeTime
26+
val lastNotificationRelativeTime: RelativeTime,
2727
)
2828
}
2929

@@ -40,7 +40,7 @@ class GatheringResponse {
4040
@Schema(description = "받은 제안 수")
4141
val receivedProposalCount: Int,
4242
@Schema(description = "모임원")
43-
val members: List<MemberOverview>
43+
val members: List<MemberOverview>,
4444
) {
4545
data class MemberOverview(
4646
@Schema(description = "사용자 id")
@@ -49,4 +49,9 @@ class GatheringResponse {
4949
val memberName: String,
5050
)
5151
}
52+
53+
data class GatheringMembers(
54+
val gatheringId: Long,
55+
val memberIds: List<Long>,
56+
)
5257
}

tuk-api/src/main/kotlin/nexters/tuk/application/member/MemberService.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,19 @@ class MemberService(
6666
name = member.name,
6767
)
6868
}
69+
70+
@Transactional(readOnly = true)
71+
fun getMembers(memberIds: List<Long>): List<MemberResponse.Overview> {
72+
if (memberIds.isEmpty()) return emptyList()
73+
74+
val members = memberRepository.findAllById(memberIds).toList()
75+
if (members.isEmpty()) return emptyList()
76+
77+
return members.mapNotNull { member ->
78+
MemberResponse.Overview(
79+
memberId = member.id,
80+
memberName = member.name ?: return@mapNotNull null,
81+
)
82+
}
83+
}
6984
}

tuk-api/src/main/kotlin/nexters/tuk/application/push/PushSender.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package nexters.tuk.application.push
33
import nexters.tuk.application.push.dto.request.PushCommand
44

55
@JvmInline
6-
value class DeviceToken(val token: String)
6+
value class DeviceToken(val token: String) {
7+
init {
8+
require(token.isNotBlank()) { "Device token must not be blank." }
9+
}
10+
}
711

812
interface PushSender {
913
fun send(
Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,56 @@
11
package nexters.tuk.application.push
22

3+
import nexters.tuk.application.device.DeviceService
4+
import nexters.tuk.application.gathering.GatheringMemberService
5+
import nexters.tuk.application.member.MemberService
36
import nexters.tuk.application.push.dto.request.PushCommand
4-
import nexters.tuk.domain.device.DeviceRepository
57
import org.slf4j.LoggerFactory
68
import org.springframework.stereotype.Service
79
import org.springframework.transaction.annotation.Transactional
810

911
@Service
1012
class PushService(
1113
private val pushSender: PushSender,
12-
private val deviceRepository: DeviceRepository,
14+
private val deviceService: DeviceService,
15+
private val gatheringMemberService: GatheringMemberService,
16+
private val memberService: MemberService,
1317
) {
1418
private val logger = LoggerFactory.getLogger(PushService::class.java)
1519

1620
@Transactional
1721
fun sendPush(command: PushCommand.Push) {
18-
logger.info("Sending bulk push notification. Recipients: ${command.recipients.size}")
19-
val memberIds = command.recipients.map { it.memberId }
20-
val deviceTokens = deviceRepository.findByMemberIdIn(memberIds)
21-
.mapNotNull { device -> device.deviceToken?.let { DeviceToken(it) } }
22+
val pushMessage = PushMessage.random()
23+
when (command) {
24+
is PushCommand.Push.GatheringNotification -> {
25+
val memberIds = command.recipients.map { it.memberId }
26+
pushAll(memberIds = memberIds, pushMessage = pushMessage)
27+
logger.info("Sent gathering notification push. Recipients: ${command.recipients.size}, PushType: ${command.pushType}")
28+
}
2229

23-
pushSender.send(
24-
deviceTokens = deviceTokens,
25-
message = command.message
26-
)
27-
logger.info("Sent push notification. Recipients: ${command.recipients.size}")
30+
is PushCommand.Push.Proposal -> {
31+
val memberIds = gatheringMemberService.getGatheringMemberIds(gatheringId = command.gatheringId)
32+
pushAll(memberIds = memberIds, pushMessage = pushMessage)
33+
logger.info("Sent proposal push. GatheringId: ${command.gatheringId}, Recipients: ${memberIds.size}, PushType: ${command.pushType}")
34+
}
35+
}
36+
}
37+
38+
private fun pushAll(
39+
memberIds: List<Long>,
40+
pushMessage: PushMessage,
41+
) {
42+
val memberNameMap = memberService.getMembers(memberIds).associate { it.memberId to it.memberName }
43+
deviceService.getDeviceTokens(memberIds).forEach { token ->
44+
pushSender.send(
45+
deviceTokens = listOf(DeviceToken(token.deviceToken)),
46+
message = PushCommand.MessagePayload(
47+
title = pushMessage.getTitle(
48+
memberNameMap[token.memberId]
49+
?: throw IllegalArgumentException("Member name not found for ID: ${token.memberId}")
50+
),
51+
body = pushMessage.body
52+
)
53+
)
54+
}
2855
}
2956
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package nexters.tuk.application.push
2+
3+
4+
enum class PushMessage(
5+
private val title: String,
6+
val body: String,
7+
) {
8+
PUSH_VERSION_A("%s님! 방금 '툭'— 누군가 만남을 제안했어요.", "슬슬 그리워질 타이밍... 아닐까요?"),
9+
PUSH_VERSION_B("누군가 %s님을 떠올리며 툭— 건넸어요.", "이번엔 그냥 지나치지 마세요 :)"),
10+
PUSH_VERSION_C("%s님, 툭— 누가 당신을 부르고 있어요.", "이번엔 누굴까? 살짝 들여다볼래요?"),
11+
PUSH_VERSION_D("%s님, 누가 몰래 '툭' 했대요.", "그냥 넘어가긴... 좀 아쉽죠?"),
12+
;
13+
14+
fun getTitle(memberName: String): String {
15+
return title.format(memberName)
16+
}
17+
18+
companion object {
19+
fun random(): PushMessage {
20+
return entries.toTypedArray().random()
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)