diff --git a/docs/20260309_푸시딥링크검증.md b/docs/20260309_푸시딥링크검증.md new file mode 100644 index 00000000..b58d99a5 --- /dev/null +++ b/docs/20260309_푸시딥링크검증.md @@ -0,0 +1,16 @@ +- [x] deep_link 파라미터 추가 여부를 푸시 발송 코드 기준으로 확인한다. +- [x] deep_link 값이 `voiceon://community/345` 형태인지 생성 규칙을 확인한다. +- [x] 검증 결과를 문서 하단에 기록한다. + +## 검증 기록 + +### 1차 확인 +- 무엇을: 푸시 발송 시 FCM payload에 `deep_link` 파라미터가 실제로 추가되는지와 커뮤니티 알림 형식이 `voiceon://community/{id}`인지 확인했다. +- 왜: 서버 구현이 문서 설명과 일치하는지, 그리고 앱이 기대하는 딥링크 문자열을 실제로 내려주는지 검증하기 위해서다. +- 어떻게: + - `src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt` 확인: `createDeepLink(deepLinkValue, deepLinkId)` 결과가 null이 아니면 `multicastMessage.putData("deep_link", deepLink)`로 payload에 추가됨. + - `src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt` 확인: 생성 규칙은 `server.env == voiceon`일 때 `voiceon://{deepLinkValue.value}/{deepLinkId}`, 그 외 환경은 `voiceon-test://{deepLinkValue.value}/{deepLinkId}`임. + - `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt` 확인: 커뮤니티 새 글 알림은 `deepLinkValue = FcmDeepLinkValue.COMMUNITY`, `deepLinkId = member.id!!`를 전달하므로 운영 환경 기준 최종 값은 `voiceon://community/{creatorId}` 형식임. + - `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityController.kt` 확인: 커뮤니티 목록 조회 API가 `creatorId`를 받으므로 커뮤니티 딥링크의 식별자도 크리에이터 ID 기준과 일치함. + - `./gradlew build` 실행(성공) + - 코드 수정은 하지 않음(확인 작업만 수행). diff --git a/docs/20260309_푸시딥링크파라미터추가.md b/docs/20260309_푸시딥링크파라미터추가.md new file mode 100644 index 00000000..338ec01f --- /dev/null +++ b/docs/20260309_푸시딥링크파라미터추가.md @@ -0,0 +1,29 @@ +- [x] FCM 푸시 생성 경로에서 딥링크 파라미터 추가 위치 확정 +- [x] `server.env` 기반 URI scheme(`voiceon://`, `voiceon-test://`) 분기 로직 구현 +- [x] `deep_link_value` 매핑 규칙(`live`, `channel`, `content`, `series`, `audition`, `community`) 반영 +- [x] FCM payload에 최종 딥링크 문자열(`{URISCHEME}://{deep_link_value}/{ID}`) 주입 +- [x] 관련 테스트/검증 수행 후 결과 기록 + +## 검증 기록 + +### 1차 구현 +- 무엇을: FCM 이벤트에 딥링크 메타(`deepLinkValue`, `deepLinkId`)를 추가하고, `FcmService`에서 `deep_link` payload(`{URISCHEME}://{deep_link_value}/{ID}`)를 생성하도록 구현했다. +- 왜: 푸시 수신 시 앱이 직접 딥링크로 진입하도록 서버에서 일관된 규칙으로 URL을 포함하기 위해서다. +- 어떻게: + - `./gradlew test` 실행(성공) + - `./gradlew build` 실행(초기 실패: import 정렬 ktlint 위반) + - `./gradlew ktlintFormat` 실행(성공) + - `./gradlew test && ./gradlew build` 재실행(성공) + - LSP 진단은 Kotlin LSP 미구성 환경으로 실행 불가(Gradle 컴파일/테스트/ktlint로 대체 검증) + +### 2차 수정 +- 무엇을: 오디션 푸시의 `deepLinkId`를 `-1` 대체값이 아닌 실제 `audition.id` nullable 값으로 조정했다. +- 왜: ID가 null일 때 비정상 딥링크(`/audition/-1`)가 생성되는 가능성을 제거하기 위해서다. +- 어떻게: + - `./gradlew test && ./gradlew build` 실행(성공) + +### 3차 수정 +- 무엇을: `server.env` 값 해석 기준을 `voiceon`(프로덕션), `voiceon_test` 및 그 외(개발/기타)로 조정했다. +- 왜: 실제 운영 환경 변수 규칙과 딥링크 URI scheme 선택 조건을 일치시키기 위해서다. +- 어떻게: + - `./gradlew test && ./gradlew build` 실행(성공) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt index a3b6c07d..be698587 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/audition/AdminAuditionService.kt @@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.admin.audition.role.AdminAuditionRoleRepository import kr.co.vividnext.sodalive.audition.AuditionStatus import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.utils.generateFileName @@ -95,7 +96,9 @@ class AdminAuditionService( messageKey = "admin.audition.fcm.message.new", args = listOf(audition.title), isAuth = audition.isAdult, - auditionId = audition.id ?: -1 + auditionId = audition.id ?: -1, + deepLinkValue = FcmDeepLinkValue.AUDITION, + deepLinkId = audition.id ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt index 35cb6232..8fb97c9c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt @@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.i18n.LangContext @@ -331,7 +332,10 @@ class AdminLiveService( title = room.member!!.nickname, messageKey = "live.room.fcm.message.canceled", args = listOf(room.title), - pushTokens = pushTokens + pushTokens = pushTokens, + roomId = room.id, + deepLinkValue = FcmDeepLinkValue.LIVE, + deepLinkId = room.id ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index 033bb73b..26c5b9e0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -28,6 +28,7 @@ import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository import kr.co.vividnext.sodalive.content.translation.TranslatedContent import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.extensions.convertLocalDateTime +import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.i18n.LangContext @@ -463,7 +464,9 @@ class AudioContentService( message = audioContent.title, recipients = listOf(audioContent.member!!.id!!), isAuth = null, - contentId = contentId + contentId = contentId, + deepLinkValue = FcmDeepLinkValue.CONTENT, + deepLinkId = contentId ) ) @@ -478,7 +481,9 @@ class AudioContentService( args = listOf(audioContent.title), isAuth = audioContent.isAdult, contentId = contentId, - creatorId = audioContent.member!!.id + creatorId = audioContent.member!!.id, + deepLinkValue = FcmDeepLinkValue.CONTENT, + deepLinkId = contentId ) ) } @@ -500,7 +505,9 @@ class AudioContentService( args = listOf(audioContent.title), isAuth = audioContent.isAdult, contentId = audioContent.id!!, - creatorId = audioContent.member!!.id + creatorId = audioContent.member!!.id, + deepLinkValue = FcmDeepLinkValue.CONTENT, + deepLinkId = audioContent.id!! ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt index 5413e497..63e90011 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt @@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.content.AudioContentRepository import kr.co.vividnext.sodalive.content.LanguageDetectEvent import kr.co.vividnext.sodalive.content.LanguageDetectTargetType import kr.co.vividnext.sodalive.content.order.OrderRepository +import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.i18n.LangContext @@ -91,7 +92,9 @@ class AudioContentCommentService( args = listOf(audioContent.title), contentId = audioContentId, commentParentId = parentId, - myMemberId = member.id + myMemberId = member.id, + deepLinkValue = FcmDeepLinkValue.CONTENT, + deepLinkId = audioContentId ) ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt index f5d39792..c326a210 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -19,6 +19,7 @@ import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService +import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.i18n.Lang @@ -667,7 +668,9 @@ class ExplorerService( type = FcmEventType.CHANGE_NOTICE, title = member.nickname, messageKey = "explorer.notice.fcm.message", - creatorId = member.id!! + creatorId = member.id!!, + deepLinkValue = FcmDeepLinkValue.CHANNEL, + deepLinkId = member.id!! ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt index 8e827e69..71491071 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunityService.kt @@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCo import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.PostCommunityPostLikeRequest import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.PostCommunityPostLikeResponse import kr.co.vividnext.sodalive.extensions.getTimeAgoString +import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.i18n.LangContext @@ -124,7 +125,9 @@ class CreatorCommunityService( type = FcmEventType.CHANGE_NOTICE, title = member.nickname, messageKey = "creator.community.fcm.new_post", - creatorId = member.id!! + creatorId = member.id!!, + deepLinkValue = FcmDeepLinkValue.COMMUNITY, + deepLinkId = member.id!! ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt index bbf0b7f2..74eeca2b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -16,6 +16,15 @@ enum class FcmEventType { CREATE_CONTENT_COMMENT, IN_PROGRESS_AUDITION } +enum class FcmDeepLinkValue(val value: String) { + LIVE("live"), + CHANNEL("channel"), + CONTENT("content"), + SERIES("series"), + AUDITION("audition"), + COMMUNITY("community") +} + class FcmEvent( val type: FcmEventType, val title: String = "", @@ -32,6 +41,8 @@ class FcmEvent( val messageId: Long? = null, val creatorId: Long? = null, val auditionId: Long? = null, + val deepLinkValue: FcmDeepLinkValue? = null, + val deepLinkId: Long? = null, val commentParentId: Long? = null, val myMemberId: Long? = null, val isAvailableJoinCreator: Boolean? = null, @@ -166,7 +177,9 @@ class FcmSendListener( contentId = contentId ?: fcmEvent.contentId, messageId = messageId ?: fcmEvent.messageId, creatorId = creatorId ?: fcmEvent.creatorId, - auditionId = auditionId ?: fcmEvent.auditionId + auditionId = auditionId ?: fcmEvent.auditionId, + deepLinkValue = fcmEvent.deepLinkValue, + deepLinkId = fcmEvent.deepLinkId ) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt index 68e05701..02f726e1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt @@ -8,11 +8,16 @@ import com.google.firebase.messaging.MessagingErrorCode import com.google.firebase.messaging.MulticastMessage import com.google.firebase.messaging.Notification import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Service @Service -class FcmService(private val pushTokenService: PushTokenService) { +class FcmService( + private val pushTokenService: PushTokenService, + @Value("\${server.env}") + private val serverEnv: String +) { private val logger = LoggerFactory.getLogger(this::class.java) @Async @@ -25,7 +30,9 @@ class FcmService(private val pushTokenService: PushTokenService) { messageId: Long? = null, contentId: Long? = null, creatorId: Long? = null, - auditionId: Long? = null + auditionId: Long? = null, + deepLinkValue: FcmDeepLinkValue? = null, + deepLinkId: Long? = null ) { if (tokens.isEmpty()) return logger.info("os: $container") @@ -82,6 +89,11 @@ class FcmService(private val pushTokenService: PushTokenService) { multicastMessage.putData("audition_id", auditionId.toString()) } + val deepLink = createDeepLink(deepLinkValue, deepLinkId) + if (deepLink != null) { + multicastMessage.putData("deep_link", deepLink) + } + val response = FirebaseMessaging.getInstance().sendEachForMulticast(multicastMessage.build()) val failedTokens = mutableListOf() @@ -115,6 +127,20 @@ class FcmService(private val pushTokenService: PushTokenService) { } } + private fun createDeepLink(deepLinkValue: FcmDeepLinkValue?, deepLinkId: Long?): String? { + if (deepLinkValue == null || deepLinkId == null) { + return null + } + + val uriScheme = if (serverEnv.equals("voiceon", ignoreCase = true)) { + "voiceon" + } else { + "voiceon-test" + } + + return "$uriScheme://${deepLinkValue.value}/$deepLinkId" + } + fun sendPointGranted(tokens: List, point: Int) { if (tokens.isEmpty()) return val data = mapOf( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index d5d0e574..ef4f5a6a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -19,6 +19,7 @@ import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.extensions.convertLocalDateTime +import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.fcm.PushTokenRepository @@ -489,6 +490,8 @@ class LiveRoomService( isAvailableJoinCreator = createdRoom.isAvailableJoinCreator, roomId = createdRoom.id, creatorId = createdRoom.member!!.id, + deepLinkValue = FcmDeepLinkValue.LIVE, + deepLinkId = createdRoom.id, genderRestriction = createdRoom.genderRestriction ) ) @@ -662,6 +665,8 @@ class LiveRoomService( isAvailableJoinCreator = room.isAvailableJoinCreator, roomId = room.id, creatorId = room.member!!.id, + deepLinkValue = FcmDeepLinkValue.LIVE, + deepLinkId = room.id, genderRestriction = room.genderRestriction ) ) @@ -729,7 +734,10 @@ class LiveRoomService( title = room.member!!.nickname, messageKey = "live.room.fcm.message.canceled", args = listOf(room.title), - pushTokens = pushTokens + pushTokens = pushTokens, + roomId = room.id, + deepLinkValue = FcmDeepLinkValue.LIVE, + deepLinkId = room.id ) ) }