-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 완결 및 휴재 알림 구현 #950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 완결 및 휴재 알림 구현 #950
Changes from all commits
68decd6
b2d30cd
7aa3541
99f3461
623fc0d
c272d24
c60affa
98604d2
44c3b04
69f886b
68ce374
a6267b3
b1b2869
7f9d005
6b4ba97
9bcf2c9
be6f601
b7a649c
82a58a8
18be37c
407551b
0358c8f
79bc869
d4e930e
704739c
a5bfe89
1fc483e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.into.websoso.core.common.util.message | ||
|
|
||
| data class PushMessage( | ||
| val title: String?, | ||
| val body: String?, | ||
| val feedId: Long?, | ||
| val novelId: Long?, | ||
| val notificationId: Long, | ||
| ) { | ||
| val destination: PushDestination | ||
| get() = when { | ||
| feedId != null -> PushDestination.FEED | ||
| novelId != null -> PushDestination.NOVEL | ||
| else -> PushDestination.NOTIFICATION_DETAIL | ||
| } | ||
|
|
||
| companion object { | ||
| fun from(data: Map<String, String>): PushMessage? { | ||
| val notificationId = data["notificationId"]?.toLongOrNull() ?: return null | ||
|
|
||
| return PushMessage( | ||
| title = data["title"], | ||
| body = data["body"], | ||
| feedId = data["feedId"]?.toLongOrNull(), | ||
| novelId = data["novelId"]?.toLongOrNull(), | ||
| notificationId = notificationId, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| enum class PushDestination { | ||
| FEED, | ||
| NOVEL, | ||
| NOTIFICATION_DETAIL, | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,10 +9,14 @@ import androidx.core.app.NotificationCompat | |
| import com.google.firebase.messaging.FirebaseMessagingService | ||
| import com.google.firebase.messaging.RemoteMessage | ||
| import com.into.websoso.core.resource.R.mipmap.ic_wss_logo | ||
| import com.into.websoso.core.resource.R.string.app_name | ||
| import com.into.websoso.core.resource.R.string.push_notification_channel_description | ||
| import com.into.websoso.core.resource.R.string.push_notification_default_body | ||
| import com.into.websoso.data.repository.PushMessageRepository | ||
| import com.into.websoso.ui.feedDetail.FeedDetailActivity | ||
| import com.into.websoso.ui.main.MainActivity | ||
| import com.into.websoso.ui.notificationDetail.NotificationDetailActivity | ||
| import com.into.websoso.ui.novelDetail.NovelDetailActivity | ||
| import dagger.hilt.android.AndroidEntryPoint | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Dispatchers | ||
|
|
@@ -27,44 +31,51 @@ class WSSFirebaseMessagingService : FirebaseMessagingService() { | |
| override fun onMessageReceived(message: RemoteMessage) { | ||
| super.onMessageReceived(message) | ||
|
|
||
| val receivedData = message.data | ||
| if (receivedData.isEmpty()) return | ||
|
|
||
| val title = receivedData["title"] ?: DEFAULT_TITLE | ||
| val body = receivedData["body"] ?: DEFAULT_BODY | ||
| val feedId = receivedData["feedId"]?.toLongOrNull() | ||
| val notificationId = receivedData["notificationId"]?.toLong() ?: return | ||
| val pushMessage = PushMessage.from(message.data) ?: return | ||
|
|
||
| setupNotificationChannel() | ||
| val pendingIntent = createPendingIntent( | ||
| feedId, | ||
| notificationId, | ||
| val pendingIntent = createPendingIntent(pushMessage) | ||
| showNotification( | ||
| title = pushMessage.title ?: getString(app_name), | ||
| body = pushMessage.body ?: getString(push_notification_default_body), | ||
| pendingIntent = pendingIntent, | ||
| ) | ||
| showNotification(title, body, pendingIntent) | ||
| } | ||
|
|
||
| private fun setupNotificationChannel() { | ||
| val notificationManager = | ||
| getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager | ||
| val channel = NotificationChannel( | ||
| CHANNEL_ID, | ||
| CHANNEL_NAME, | ||
| getString(app_name), | ||
| NotificationManager.IMPORTANCE_DEFAULT, | ||
| ).apply { | ||
| description = CHANNEL_DESCRIPTION | ||
| description = getString(push_notification_channel_description) | ||
| } | ||
| notificationManager.createNotificationChannel(channel) | ||
| } | ||
|
|
||
| private fun createPendingIntent( | ||
| feedId: Long?, | ||
| notificationId: Long, | ||
| ): PendingIntent { | ||
| private fun createPendingIntent(pushMessage: PushMessage): PendingIntent { | ||
| val mainIntent = MainActivity.getIntent(this) | ||
| val notificationId = pushMessage.notificationId | ||
|
|
||
| val detailIntent = when (pushMessage.destination) { | ||
| PushDestination.FEED -> FeedDetailActivity.getIntent( | ||
| this, | ||
| requireNotNull(pushMessage.feedId), | ||
| notificationId, | ||
| ) | ||
|
|
||
| val detailIntent = when { | ||
| feedId != null -> FeedDetailActivity.getIntent(this, feedId, notificationId) | ||
| else -> NotificationDetailActivity.getIntent(this, notificationId) | ||
| PushDestination.NOVEL -> NovelDetailActivity.getIntent( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 작품 푸시 알림을 눌렀을 때도 읽음 처리가 되는지 확인이 필요해 보입니다. 피드 푸시는 FeedDetailActivity에 notificationId를 전달해 읽음 처리하고 있지만, 작품 푸시는 NovelDetailActivity에 novelId만 전달하고 있습니다. 이 경우 푸시를 눌러 작품 상세로 이동해도 해당 알림은 읽지 않은 상태로 남을 수 있을 것 같아요!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확인했습니다! 푸시뿐만이 아니라 다른 경로에서도 읽음 처리가 되지 않아 같이 수정했습니다 |
||
| this, | ||
| requireNotNull(pushMessage.novelId), | ||
| notificationId, | ||
| ) | ||
|
|
||
| PushDestination.NOTIFICATION_DETAIL -> NotificationDetailActivity.getIntent( | ||
| this, | ||
| notificationId, | ||
| ) | ||
| } | ||
|
|
||
| return TaskStackBuilder.create(this).run { | ||
|
|
@@ -108,10 +119,6 @@ class WSSFirebaseMessagingService : FirebaseMessagingService() { | |
| } | ||
|
|
||
| companion object { | ||
| private const val DEFAULT_TITLE = "웹소소" | ||
| private const val DEFAULT_BODY = "푸시 알림 메시지입니다" | ||
| private const val CHANNEL_ID = "websoso" | ||
| private const val CHANNEL_NAME = "웹소소" | ||
| private const val CHANNEL_DESCRIPTION = "웹소소 알림입니다." | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package com.into.websoso.data.mapper | ||
|
|
||
| import com.into.websoso.data.model.NovelNotificationSettingEntity | ||
| import com.into.websoso.data.model.NovelNotificationSubscriptionEntity | ||
| import com.into.websoso.data.model.NovelNotificationSubscriptionsEntity | ||
| import com.into.websoso.data.remote.response.NovelNotificationSettingResponseDto | ||
| import com.into.websoso.data.remote.response.NovelNotificationSubscriptionsResponseDto | ||
|
|
||
| fun NovelNotificationSettingResponseDto.toData(): NovelNotificationSettingEntity = | ||
| NovelNotificationSettingEntity( | ||
| isCompletionNotificationEnabled = isCompletionNotificationEnabled, | ||
| isHiatusReturnNotificationEnabled = isHiatusReturnNotificationEnabled, | ||
| ) | ||
|
|
||
| fun NovelNotificationSubscriptionsResponseDto.toData(): NovelNotificationSubscriptionsEntity = | ||
| NovelNotificationSubscriptionsEntity( | ||
| isLoadable = isLoadable, | ||
| nextSubscriptionId = nextSubscriptionId, | ||
| subscriptions = subscriptions.map { | ||
| NovelNotificationSubscriptionEntity( | ||
| subscriptionId = it.subscriptionId, | ||
| novelId = it.novelId, | ||
| novelTitle = it.novelTitle, | ||
| novelAuthor = it.novelAuthor, | ||
| novelImage = it.novelImage, | ||
| registeredDate = it.registeredDate, | ||
| ) | ||
| }, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.into.websoso.data.model | ||
|
|
||
| data class NovelNotificationSettingEntity( | ||
| val isCompletionNotificationEnabled: Boolean, | ||
| val isHiatusReturnNotificationEnabled: Boolean, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.into.websoso.data.model | ||
|
|
||
| data class NovelNotificationSubscriptionEntity( | ||
| val subscriptionId: Long, | ||
| val novelId: Long, | ||
| val novelTitle: String, | ||
| val novelAuthor: String, | ||
| val novelImage: String, | ||
| val registeredDate: String, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.into.websoso.data.model | ||
|
|
||
| data class NovelNotificationSubscriptionsEntity( | ||
| val isLoadable: Boolean, | ||
| val nextSubscriptionId: Long?, | ||
| val subscriptions: List<NovelNotificationSubscriptionEntity>, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.into.websoso.data.remote.api | ||
|
|
||
| import com.into.websoso.data.remote.request.NovelNotificationSettingRequestDto | ||
| import com.into.websoso.data.remote.request.NovelNotificationSubscriptionsDeleteRequestDto | ||
| import com.into.websoso.data.remote.response.NovelNotificationSettingResponseDto | ||
| import com.into.websoso.data.remote.response.NovelNotificationSubscriptionsResponseDto | ||
| import retrofit2.http.Body | ||
| import retrofit2.http.GET | ||
| import retrofit2.http.HTTP | ||
| import retrofit2.http.PUT | ||
| import retrofit2.http.Path | ||
| import retrofit2.http.Query | ||
|
|
||
| interface NovelNotificationApi { | ||
| @GET("novels/{novelId}/notification") | ||
| suspend fun getNovelNotificationSetting( | ||
| @Path("novelId") novelId: Long, | ||
| ): NovelNotificationSettingResponseDto | ||
|
|
||
| @PUT("novels/{novelId}/notification") | ||
| suspend fun putNovelNotificationSetting( | ||
| @Path("novelId") novelId: Long, | ||
| @Body novelNotificationSettingRequestDto: NovelNotificationSettingRequestDto, | ||
| ) | ||
|
|
||
| @GET("users/me/notification/novels") | ||
| suspend fun getNovelNotificationSubscriptions( | ||
| @Query("notificationType") notificationType: String, | ||
| @Query("lastSubscriptionId") lastSubscriptionId: Long, | ||
| @Query("size") size: Int, | ||
| ): NovelNotificationSubscriptionsResponseDto | ||
|
|
||
| @HTTP(method = "DELETE", path = "users/me/notification/novels", hasBody = true) | ||
| suspend fun deleteNovelNotificationSubscriptions( | ||
| @Body novelNotificationSubscriptionsDeleteRequestDto: NovelNotificationSubscriptionsDeleteRequestDto, | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.into.websoso.data.remote.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class NovelNotificationSettingRequestDto( | ||
| @SerialName("isCompletionNotificationEnabled") | ||
| val isCompletionNotificationEnabled: Boolean, | ||
| @SerialName("isHiatusReturnNotificationEnabled") | ||
| val isHiatusReturnNotificationEnabled: Boolean, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.into.websoso.data.remote.request | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class NovelNotificationSubscriptionsDeleteRequestDto( | ||
| @SerialName("notificationType") | ||
| val notificationType: String, | ||
| @SerialName("novelIds") | ||
| val novelIds: List<Long>, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.into.websoso.data.remote.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class NovelNotificationSettingResponseDto( | ||
| @SerialName("isCompletionNotificationEnabled") | ||
| val isCompletionNotificationEnabled: Boolean, | ||
| @SerialName("isHiatusReturnNotificationEnabled") | ||
| val isHiatusReturnNotificationEnabled: Boolean, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.into.websoso.data.remote.response | ||
|
|
||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| data class NovelNotificationSubscriptionsResponseDto( | ||
| @SerialName("isLoadable") | ||
| val isLoadable: Boolean, | ||
| @SerialName("nextSubscriptionId") | ||
| val nextSubscriptionId: Long? = null, | ||
| @SerialName("subscriptions") | ||
| val subscriptions: List<NovelNotificationSubscriptionResponseDto>, | ||
| ) { | ||
| @Serializable | ||
| data class NovelNotificationSubscriptionResponseDto( | ||
| @SerialName("subscriptionId") | ||
| val subscriptionId: Long, | ||
| @SerialName("novelId") | ||
| val novelId: Long, | ||
| @SerialName("novelTitle") | ||
| val novelTitle: String, | ||
| @SerialName("novelAuthor") | ||
| val novelAuthor: String, | ||
| @SerialName("novelImage") | ||
| val novelImage: String, | ||
| @SerialName("registeredDate") | ||
| val registeredDate: String, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PushDestination을 PushMessage와 같은 파일에 둔 이유가 있을까요?
현재는 PushMessage에 종속적인 타입이라 함께 두신 것 같기도 한데, 이후 목적지가 추가되거나 다른 곳에서도 사용될 가능성을 고려하면 별도 파일로 분리하는 방식도 괜찮을 것 같아 의견 여쭤봅니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
종속적인 타입이라 이렇게 두었고 푸시가 다른 곳에서도 다른 로직으로 처리된다면 이런 설계가 아닌 다른 방식으로 설계를 해야 할 것 같아 현재는 이렇게 두었습니다. 지금은 pushmessage, pushdestination이 확장에 열려있는 설계는 아니지만 추후 어떤 범위로 확장이 될지 몰라 그때 재설계하는게 좋다 생각했습니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 확장성을 고려해 분리를 선호하는 스타일이라 궁금했어요! 👍🏻