Skip to content
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

카테고리별 푸시 on/off #341

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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
47 changes: 47 additions & 0 deletions api/src/main/kotlin/handler/PushPreferenceHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.wafflestudio.snutt.handler

import com.wafflestudio.snutt.middleware.SnuttRestApiDefaultMiddleware
import com.wafflestudio.snutt.notification.data.PushCategory
import com.wafflestudio.snutt.notification.dto.PushPreferenceResponse
import com.wafflestudio.snutt.notification.service.PushPreferenceService
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest

@Component
class PushPreferenceHandler(
private val pushPreferenceService: PushPreferenceService,
snuttRestApiDefaultMiddleware: SnuttRestApiDefaultMiddleware,
) : ServiceHandler(
handlerMiddleware = snuttRestApiDefaultMiddleware,
) {
suspend fun getPushPreferences(req: ServerRequest) =
handle(req) {
val user = req.getContext().user!!
val pushPreferences = pushPreferenceService.getPushPreferences(user)
pushPreferences.map { PushPreferenceResponse(it) }
}

suspend fun enableLectureUpdate(req: ServerRequest) =
handle(req) {
val user = req.getContext().user!!
pushPreferenceService.enablePush(user, PushCategory.LECTURE_UPDATE)
}

suspend fun disableLectureUpdate(req: ServerRequest) =
handle(req) {
val user = req.getContext().user!!
pushPreferenceService.disablePush(user, PushCategory.LECTURE_UPDATE)
}

suspend fun enableVacancyNotification(req: ServerRequest) =
handle(req) {
val user = req.getContext().user!!
pushPreferenceService.enablePush(user, PushCategory.VACANCY_NOTIFICATION)
}

suspend fun disableVacancyNotification(req: ServerRequest) =
handle(req) {
val user = req.getContext().user!!
pushPreferenceService.disablePush(user, PushCategory.VACANCY_NOTIFICATION)
}
}
14 changes: 14 additions & 0 deletions api/src/main/kotlin/router/MainRouter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.wafflestudio.snutt.handler.FriendTableHandler
import com.wafflestudio.snutt.handler.LectureSearchHandler
import com.wafflestudio.snutt.handler.NotificationHandler
import com.wafflestudio.snutt.handler.PopupHandler
import com.wafflestudio.snutt.handler.PushPreferenceHandler
import com.wafflestudio.snutt.handler.StaticPageHandler
import com.wafflestudio.snutt.handler.TagHandler
import com.wafflestudio.snutt.handler.TimetableHandler
Expand Down Expand Up @@ -70,6 +71,7 @@ class MainRouter(
private val feedbackHandler: FeedbackHandler,
private val staticPageHandler: StaticPageHandler,
private val evServiceHandler: EvServiceHandler,
private val pushPreferenceHandler: PushPreferenceHandler,
) {
@Bean
fun healthCheck() =
Expand Down Expand Up @@ -343,4 +345,16 @@ class MainRouter(
GET("/privacy_policy").invoke { staticPageHandler.privacyPolicy() }
GET("/terms_of_service").invoke { staticPageHandler.termsOfService() }
}

@Bean
fun pushPreferenceRouter() =
v1CoRouter {
"/push/preferences".nest {
GET("", pushPreferenceHandler::getPushPreferences)
POST("lecture-update", pushPreferenceHandler::enableLectureUpdate)
DELETE("lecture-update", pushPreferenceHandler::disableLectureUpdate)
POST("vacancy-notification", pushPreferenceHandler::enableVacancyNotification)
DELETE("vacancy-notification", pushPreferenceHandler::disableVacancyNotification)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.wafflestudio.snutt.common.push.dto.PushMessage
import com.wafflestudio.snutt.coursebook.data.Coursebook
import com.wafflestudio.snutt.notification.data.Notification
import com.wafflestudio.snutt.notification.data.NotificationType
import com.wafflestudio.snutt.notification.data.PushCategory
import com.wafflestudio.snutt.notification.service.NotificationService
import com.wafflestudio.snutt.notification.service.PushService
import com.wafflestudio.snutt.notification.service.PushWithNotificationService
Expand Down Expand Up @@ -74,7 +75,7 @@ class SugangSnuNotificationServiceImpl(
urlScheme = DeeplinkType.NOTIFICATIONS,
)
}
pushService.sendTargetPushes(userIdToMessage)
pushService.sendCategoricalTargetPushes(userIdToMessage, PushCategory.LECTURE_UPDATE)
}

override suspend fun notifyCoursebookUpdate(coursebook: Coursebook) {
Expand Down
41 changes: 41 additions & 0 deletions core/src/main/kotlin/notification/data/PushCategory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.wafflestudio.snutt.notification.data

import com.fasterxml.jackson.annotation.JsonValue
import org.springframework.core.convert.converter.Converter
import org.springframework.data.convert.ReadingConverter
import org.springframework.data.convert.WritingConverter
import org.springframework.stereotype.Component

enum class PushCategory(
@JsonValue val value: Int,
) {
Comment on lines +9 to +11
Copy link
Member

@Hank-Choi Hank-Choi Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 PushCategory라는 이름이 헷갈리네요
NotificationType이랑 거의 비슷해서 헷갈려요

유저가 설정할 수 있는 값이라는 PushPreference같은 느낌이 이름에 같이 들어갔으면 좋겠습니다.

NORMAL(0),
LECTURE_UPDATE(1),
VACANCY_NOTIFICATION(2),
;

companion object {
private val valueMap = PushCategory.entries.associateBy { e -> e.value }

fun getOfValue(value: Int): PushCategory? = valueMap[value]
}
}

@ReadingConverter
@Component
class PushCategoryReadConverter : Converter<Int, PushCategory> {
override fun convert(source: Int): PushCategory = PushCategory.getOfValue(source)!!
}

@WritingConverter
@Component
class PushCategoryWriteConverter : Converter<PushCategory, Int> {
override fun convert(source: PushCategory): Int = source.value
}

fun PushCategory(notificationType: NotificationType) =
when (notificationType) {
NotificationType.LECTURE_UPDATE -> PushCategory.LECTURE_UPDATE
NotificationType.LECTURE_VACANCY -> PushCategory.VACANCY_NOTIFICATION
else -> PushCategory.NORMAL
}
21 changes: 21 additions & 0 deletions core/src/main/kotlin/notification/data/PushOptOut.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.wafflestudio.snutt.notification.data

import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.index.CompoundIndex
import org.springframework.data.mongodb.core.index.Indexed
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.core.mapping.Field
import org.springframework.data.mongodb.core.mapping.FieldType

@Document(collection = "push_opt_out")
@CompoundIndex(def = "{ 'user_id': 1, 'push_category': 1 }", unique = true)
data class PushOptOut(
@Id
val id: String? = null,
@Indexed
@Field("user_id", targetType = FieldType.OBJECT_ID)
val userId: String,
@Field("push_category")
@Indexed
val pushCategory: PushCategory,
)
Comment on lines +12 to +21
Copy link
Member

@Hank-Choi Hank-Choi Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

row 하나로 유저의 모든 설정값을 보면 좋을 것 같은데 현재는 좀 RDB스러운 접근이라고 생각합니다.
개인적으론 해당 컬렉션을 조회할 때 바로 어떤 유저가 어떤 푸시를 키고 껐는지 알 수 있었으면 좋겠습니다.
아래처럼 되면 좋을 것 같아요
ex)

{
    "userId": "abcdefg123123",
    "pushSetting": [
        {
            "category": "LECTURE_UPDATE",
            "enable": true
        },
        {
            "category": "VACANCY_NOTIFICATION",
            "enable": false
        },
        ...
    ]
}

8 changes: 8 additions & 0 deletions core/src/main/kotlin/notification/dto/PushPreference.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.wafflestudio.snutt.notification.dto

import com.wafflestudio.snutt.notification.data.PushCategory

data class PushPreference(
val pushCategory: PushCategory,
val enabled: Boolean,
)
12 changes: 12 additions & 0 deletions core/src/main/kotlin/notification/dto/PushPreferenceResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.wafflestudio.snutt.notification.dto

data class PushPreferenceResponse(
val pushCategoryName: String,
val enabled: Boolean,
)

fun PushPreferenceResponse(pushPreference: PushPreference) =
PushPreferenceResponse(
pushCategoryName = pushPreference.pushCategory.name,
enabled = pushPreference.enabled,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.wafflestudio.snutt.notification.repository

import com.wafflestudio.snutt.notification.data.PushCategory
import com.wafflestudio.snutt.notification.data.PushOptOut
import org.springframework.data.repository.kotlin.CoroutineCrudRepository

interface PushOptOutRepository : CoroutineCrudRepository<PushOptOut, String> {
suspend fun existsByUserIdAndPushCategory(
userId: String,
pushCategory: PushCategory,
): Boolean

suspend fun deleteByUserIdAndPushCategory(
userId: String,
pushCategory: PushCategory,
): Long

suspend fun findByUserIdInAndPushCategory(
userIds: List<String>,
pushCategory: PushCategory,
): List<PushOptOut>

suspend fun findByUserId(userId: String): List<PushOptOut>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.wafflestudio.snutt.notification.service

import com.wafflestudio.snutt.notification.data.PushCategory
import com.wafflestudio.snutt.notification.data.PushOptOut
import com.wafflestudio.snutt.notification.dto.PushPreference
import com.wafflestudio.snutt.notification.repository.PushOptOutRepository
import com.wafflestudio.snutt.users.data.User
import org.springframework.stereotype.Service

interface PushPreferenceService {
suspend fun enablePush(
user: User,
pushCategory: PushCategory,
)

suspend fun disablePush(
user: User,
pushCategory: PushCategory,
)

suspend fun getPushPreferences(user: User): List<PushPreference>
}

@Service
class PushPreferenceServiceImpl(
private val pushOptOutRepository: PushOptOutRepository,
) : PushPreferenceService {
override suspend fun enablePush(
user: User,
pushCategory: PushCategory,
) {
pushOptOutRepository.save(
PushOptOut(
userId = user.id!!,
pushCategory = pushCategory,
),
)
}

override suspend fun disablePush(
user: User,
pushCategory: PushCategory,
) {
pushOptOutRepository.deleteByUserIdAndPushCategory(
userId = user.id!!,
pushCategory = pushCategory,
)
}

override suspend fun getPushPreferences(user: User): List<PushPreference> {
val allPushCategories = PushCategory.entries.filterNot { it == PushCategory.NORMAL }
val disabledPushCategories = pushOptOutRepository.findByUserId(user.id!!).map { it.pushCategory }.toSet()
return allPushCategories.map {
if (it in disabledPushCategories) {
return@map PushPreference(
pushCategory = it,
enabled = false,
)
} else {
return@map PushPreference(
pushCategory = it,
enabled = true,
)
}
}
}
}
73 changes: 73 additions & 0 deletions core/src/main/kotlin/notification/service/PushService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.wafflestudio.snutt.notification.service
import com.wafflestudio.snutt.common.push.PushClient
import com.wafflestudio.snutt.common.push.dto.PushMessage
import com.wafflestudio.snutt.common.push.dto.TargetedPushMessageWithToken
import com.wafflestudio.snutt.notification.data.PushCategory
import com.wafflestudio.snutt.notification.repository.PushOptOutRepository
import org.springframework.stereotype.Service

/**
Expand All @@ -22,12 +24,30 @@ interface PushService {
suspend fun sendGlobalPush(pushMessage: PushMessage)

suspend fun sendTargetPushes(userToPushMessage: Map<String, PushMessage>)

suspend fun sendCategoricalPush(
pushMessage: PushMessage,
userId: String,
pushCategory: PushCategory,
)

suspend fun sendCategoricalPushes(
pushMessage: PushMessage,
userIds: List<String>,
pushCategory: PushCategory,
)

suspend fun sendCategoricalTargetPushes(
userToPushMessage: Map<String, PushMessage>,
pushCategory: PushCategory,
)
}

@Service
class PushServiceImpl internal constructor(
private val deviceService: DeviceService,
private val pushClient: PushClient,
private val pushOptOutRepository: PushOptOutRepository,
) : PushService {
override suspend fun sendPush(
pushMessage: PushMessage,
Expand Down Expand Up @@ -60,4 +80,57 @@ class PushServiceImpl internal constructor(
deviceService.getUserDevices(userId).map { it.fcmRegistrationId to pushMessage }
}.map { (fcmRegistrationId, message) -> TargetedPushMessageWithToken(fcmRegistrationId, message) }
.let { pushClient.sendMessages(it) }

override suspend fun sendCategoricalPush(
pushMessage: PushMessage,
userId: String,
pushCategory: PushCategory,
) {
if (pushCategory == PushCategory.NORMAL || !pushOptOutRepository.existsByUserIdAndPushCategory(userId, pushCategory)) {
sendPush(pushMessage, userId)
}
}
Comment on lines +83 to +92
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PushService가 PushCategory에 대해 알 필요가 있을까요?
PushPreferenceService가 있는 것 같은데 유저 필터링은 거기서 해결해도 될 것 같아요


override suspend fun sendCategoricalPushes(
pushMessage: PushMessage,
userIds: List<String>,
pushCategory: PushCategory,
) {
if (pushCategory == PushCategory.NORMAL) {
sendPushes(pushMessage, userIds)
}

val filteredUserIds =
pushOptOutRepository
.findByUserIdInAndPushCategory(userIds, pushCategory)
.map { it.userId }
.toSet()
.let { optOutUserIds -> userIds.filterNot { it in optOutUserIds } }

if (filteredUserIds.isNotEmpty()) {
sendPushes(pushMessage, filteredUserIds)
}
}

override suspend fun sendCategoricalTargetPushes(
userToPushMessage: Map<String, PushMessage>,
pushCategory: PushCategory,
) {
if (pushCategory == PushCategory.NORMAL) {
sendTargetPushes(userToPushMessage)
}

val userIds = userToPushMessage.keys.toList()

val filteredUserToPushMessage =
pushOptOutRepository
.findByUserIdInAndPushCategory(userIds, pushCategory)
.map { it.userId }
.toSet()
.let { optOutUserIds -> userToPushMessage.filterKeys { it !in optOutUserIds } }

if (filteredUserToPushMessage.isNotEmpty()) {
sendTargetPushes(filteredUserToPushMessage)
}
}
}
Loading