From 0f371ffd0ec55f4672983ee1dbab4c13ca9f06bb Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 13 Mar 2026 11:34:16 +0900 Subject: [PATCH] =?UTF-8?q?fix(deeplink):=20=ED=91=B8=EC=8B=9C=20=EB=94=A5?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=9A=B0=EC=84=A0=20=EB=B6=84=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=ED=98=BC=ED=95=A9=20=EB=9D=BC=EC=9A=B0=ED=8C=85?= =?UTF-8?q?=EC=9D=84=20=EB=B0=A9=EC=A7=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fcm/SodaFirebaseMessagingService.kt | 22 ++- .../sodalive/main/DeepLinkActivity.kt | 181 ++++++++++++++++++ .../vividnext/sodalive/main/MainActivity.kt | 105 ++++++++++ docs/20260313_푸시메시지터치딥링크우선처리.md | 44 +++++ 4 files changed, 344 insertions(+), 8 deletions(-) create mode 100644 docs/20260313_푸시메시지터치딥링크우선처리.md diff --git a/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt b/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt index 27762a86..1e0e9b37 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt @@ -75,14 +75,20 @@ class SodaFirebaseMessagingService : FirebaseMessagingService() { } } - val deepLinkExtras = android.os.Bundle().apply { - messageData["room_id"]?.let { putString("room_id", it) } - messageData["message_id"]?.let { putString("message_id", it) } - messageData["content_id"]?.let { putString("content_id", it) } - messageData["channel_id"]?.let { putString("channel_id", it) } - messageData["audition_id"]?.let { putString("audition_id", it) } - messageData["deep_link_value"]?.let { putString("deep_link_value", it) } - messageData["deep_link_sub5"]?.let { putString("deep_link_sub5", it) } + val deepLinkExtras = if (!deepLinkUrl.isNullOrBlank()) { + android.os.Bundle().apply { + putString("deep_link", deepLinkUrl) + } + } else { + android.os.Bundle().apply { + messageData["room_id"]?.let { putString("room_id", it) } + messageData["message_id"]?.let { putString("message_id", it) } + messageData["content_id"]?.let { putString("content_id", it) } + messageData["channel_id"]?.let { putString("channel_id", it) } + messageData["audition_id"]?.let { putString("audition_id", it) } + messageData["deep_link_value"]?.let { putString("deep_link_value", it) } + messageData["deep_link_sub5"]?.let { putString("deep_link_sub5", it) } + } } if (!deepLinkExtras.isEmpty) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt index 168f0c2b..f180ed54 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt @@ -5,9 +5,15 @@ import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.localbroadcastmanager.content.LocalBroadcastManager +import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity +import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity import kr.co.vividnext.sodalive.app.SodaLiveApp +import kr.co.vividnext.sodalive.audition.AuditionActivity import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity +import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityAllActivity import kr.co.vividnext.sodalive.live.room.LiveRoomActivity +import kr.co.vividnext.sodalive.message.MessageActivity import kr.co.vividnext.sodalive.mypage.can.payment.CanPaymentActivity import kr.co.vividnext.sodalive.splash.SplashActivity import java.util.Locale @@ -18,6 +24,7 @@ class DeepLinkActivity : AppCompatActivity() { val data: Uri? = intent?.data val deepLinkExtras = buildDeepLinkExtras(intent) + val deepLinkUrl = deepLinkExtras?.getString("deep_link") if (data != null && data.scheme != null) { val host = data.host @@ -46,6 +53,15 @@ class DeepLinkActivity : AppCompatActivity() { } if (SodaLiveApp.isAppInForeground) { + if (!deepLinkUrl.isNullOrBlank()) { + deepLinkExtras?.let { + if (routeForegroundDeepLink(it)) { + finish() + return + } + } + } + startActivity( Intent(applicationContext, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) @@ -70,6 +86,10 @@ class DeepLinkActivity : AppCompatActivity() { val data = intent.data + data?.toString()?.takeIf { it.isNotBlank() }?.let { + extras.putString("deep_link", it) + } + fun putIfAbsent(key: String, value: String?) { if (!value.isNullOrBlank() && !extras.containsKey(key)) { extras.putString(key, value) @@ -106,6 +126,7 @@ class DeepLinkActivity : AppCompatActivity() { copyString("message_id") copyString("audition_id") copyString("content_id") + copyString("deep_link") copyString("deep_link_value") copyString("deep_link_sub5") @@ -142,6 +163,14 @@ class DeepLinkActivity : AppCompatActivity() { extras.putString("content_id", it.toString()) } + intent.getStringExtra("deep_link")?.takeIf { it.isNotBlank() }?.let { + extras.putString("deep_link", it) + } + + intent.getStringExtra("deepLink")?.takeIf { it.isNotBlank() }?.let { + extras.putString("deep_link", it) + } + if (data != null) { applyPathDeepLink(data = data, putIfAbsent = ::putIfAbsent) } @@ -186,6 +215,158 @@ class DeepLinkActivity : AppCompatActivity() { } } + private fun routeForegroundDeepLink(bundle: Bundle): Boolean { + val roomId = bundle.getString("room_id")?.toLongOrNull() + ?: bundle.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 } + val channelId = bundle.getString("channel_id")?.toLongOrNull() + ?: bundle.getLong(Constants.EXTRA_USER_ID).takeIf { it > 0 } + val messageId = bundle.getString("message_id")?.toLongOrNull() + ?: bundle.getLong(Constants.EXTRA_MESSAGE_ID).takeIf { it > 0 } + val contentId = bundle.getString("content_id")?.toLongOrNull() + ?: bundle.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 } + val auditionId = bundle.getString("audition_id")?.toLongOrNull() + ?: bundle.getLong(Constants.EXTRA_AUDITION_ID).takeIf { it > 0 } + val communityCreatorId = bundle.getString(Constants.EXTRA_COMMUNITY_CREATOR_ID)?.toLongOrNull() + ?: bundle.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID).takeIf { it > 0 } + + when { + roomId != null && roomId > 0 -> { + startActivity( + Intent(applicationContext, LiveRoomActivity::class.java).apply { + putExtra(Constants.EXTRA_ROOM_ID, roomId) + } + ) + return true + } + + channelId != null && channelId > 0 -> { + startActivity( + Intent(applicationContext, UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, channelId) + } + ) + return true + } + + contentId != null && contentId > 0 -> { + startActivity( + Intent(applicationContext, AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, contentId) + } + ) + return true + } + + messageId != null && messageId > 0 -> { + startActivity(Intent(applicationContext, MessageActivity::class.java)) + return true + } + + communityCreatorId != null && communityCreatorId > 0 -> { + startActivity( + Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply { + putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, communityCreatorId) + } + ) + return true + } + + auditionId != null && auditionId > 0 -> { + startActivity(Intent(applicationContext, AuditionActivity::class.java)) + return true + } + } + + val deepLinkValue = bundle.getString("deep_link_value") + val deepLinkValueId = bundle.getString("deep_link_sub5")?.toLongOrNull() + return routeByDeepLinkValue(deepLinkValue = deepLinkValue, deepLinkValueId = deepLinkValueId) + } + + private fun routeByDeepLinkValue(deepLinkValue: String?, deepLinkValueId: Long?): Boolean { + if (deepLinkValue.isNullOrBlank()) { + return false + } + + return when (deepLinkValue.lowercase(Locale.ROOT)) { + "series" -> { + if (deepLinkValueId == null || deepLinkValueId <= 0) { + return false + } + + startActivity( + Intent(applicationContext, SeriesDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_SERIES_ID, deepLinkValueId) + } + ) + true + } + + "content" -> { + if (deepLinkValueId == null || deepLinkValueId <= 0) { + return false + } + + startActivity( + Intent(applicationContext, AudioContentDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, deepLinkValueId) + } + ) + true + } + + "channel" -> { + if (deepLinkValueId == null || deepLinkValueId <= 0) { + return false + } + + startActivity( + Intent(applicationContext, UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, deepLinkValueId) + } + ) + true + } + + "live" -> { + if (deepLinkValueId == null || deepLinkValueId <= 0) { + return false + } + + startActivity( + Intent(applicationContext, LiveRoomActivity::class.java).apply { + putExtra(Constants.EXTRA_ROOM_ID, deepLinkValueId) + } + ) + true + } + + "community" -> { + if (deepLinkValueId == null || deepLinkValueId <= 0) { + return false + } + + startActivity( + Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply { + putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId) + } + ) + true + } + + "message" -> { + startActivity(Intent(applicationContext, MessageActivity::class.java)) + true + } + + "audition" -> { + startActivity(Intent(applicationContext, AuditionActivity::class.java)) + true + } + + else -> false + } + } + private fun applyPathDeepLink( data: Uri, putIfAbsent: (key: String, value: String?) -> Unit diff --git a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt index c011df08..03fe01ca 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.res.ColorStateList +import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler @@ -60,6 +61,7 @@ import java.util.Locale import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +import androidx.core.net.toUri @UnstableApi class MainActivity : BaseActivity(ActivityMainBinding::inflate) { @@ -312,6 +314,20 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl } private fun executeBundleDeeplink(bundle: Bundle): Boolean { + val deepLinkUrl = bundle.getString("deep_link") + if (!deepLinkUrl.isNullOrBlank()) { + val deepLinkBundle = buildBundleFromDeepLinkUrl(deepLinkUrl) + if (deepLinkBundle != null) { + return executeBundleRoute(deepLinkBundle) + } + + return false + } + + return executeBundleRoute(bundle) + } + + private fun executeBundleRoute(bundle: Bundle): Boolean { val roomId = bundle.getString("room_id")?.toLongOrNull() ?: bundle.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 } val channelId = bundle.getString("channel_id")?.toLongOrNull() @@ -385,6 +401,95 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl return false } + private fun buildBundleFromDeepLinkUrl(deepLinkUrl: String): Bundle? { + val data = runCatching { deepLinkUrl.toUri() }.getOrNull() ?: return null + val extras = Bundle().apply { + putString("deep_link", deepLinkUrl) + } + + fun putQuery(key: String) { + val value = data.getQueryParameter(key) + if (!value.isNullOrBlank()) { + extras.putString(key, value) + } + } + + putQuery("room_id") + putQuery("channel_id") + putQuery("message_id") + putQuery("audition_id") + putQuery("content_id") + putQuery("deep_link_value") + putQuery("deep_link_sub5") + putQuery(Constants.EXTRA_COMMUNITY_CREATOR_ID) + + applyPathDeepLink(data = data) { key, value -> + if (!value.isNullOrBlank() && !extras.containsKey(key)) { + extras.putString(key, value) + } + } + + return extras + } + + private fun applyPathDeepLink( + data: Uri, + putIfAbsent: (key: String, value: String?) -> Unit + ) { + val host = data.host?.lowercase(Locale.ROOT).orEmpty() + val pathSegments = data.pathSegments.filter { it.isNotBlank() } + + val pathType: String + val pathId: String? + + if (host.isNotBlank() && host != "payverse") { + pathType = host + pathId = pathSegments.firstOrNull() + } else if (pathSegments.isNotEmpty()) { + pathType = pathSegments[0].lowercase(Locale.ROOT) + pathId = pathSegments.getOrNull(1) + } else { + return + } + + when (pathType) { + "live" -> { + putIfAbsent("room_id", pathId) + putIfAbsent("deep_link_value", "live") + putIfAbsent("deep_link_sub5", pathId) + } + + "content" -> { + putIfAbsent("content_id", pathId) + putIfAbsent("deep_link_value", "content") + putIfAbsent("deep_link_sub5", pathId) + } + + "series" -> { + putIfAbsent("deep_link_value", "series") + putIfAbsent("deep_link_sub5", pathId) + } + + "community" -> { + putIfAbsent("deep_link_value", "community") + putIfAbsent(Constants.EXTRA_COMMUNITY_CREATOR_ID, pathId) + putIfAbsent("deep_link_sub5", pathId) + } + + "message" -> { + putIfAbsent("deep_link_value", "message") + putIfAbsent("message_id", pathId) + putIfAbsent("deep_link_sub5", pathId) + } + + "audition" -> { + putIfAbsent("deep_link_value", "audition") + putIfAbsent("audition_id", pathId) + putIfAbsent("deep_link_sub5", pathId) + } + } + } + private fun executeOneLink() { val deepLinkValue = SharedPreferenceManager.marketingLinkValue val deepLinkValueId = SharedPreferenceManager.marketingLinkValueId diff --git a/docs/20260313_푸시메시지터치딥링크우선처리.md b/docs/20260313_푸시메시지터치딥링크우선처리.md new file mode 100644 index 00000000..8320fa62 --- /dev/null +++ b/docs/20260313_푸시메시지터치딥링크우선처리.md @@ -0,0 +1,44 @@ +# 2026-03-13 푸시 메시지 터치 딥링크 우선 처리 + +## 체크리스트 +- [x] 기존 푸시 터치/딥링크 라우팅 경로 분석 +- [x] 푸시 터치 시 `deep_link` 비어있지 않으면 딥링크 우선 실행, 비어 있으면 기존 로직 유지 +- [x] 앱 실행 중 딥링크 실행 시 메인 페이지 호출 없이 현재 페이지에서 목적지로 이동하도록 수정 +- [x] 앱 미실행 후 `MainActivity` 진입 로직에도 동일한 `deep_link` 우선 분기 반영 +- [x] `deep_link` 존재 시 번들에 `deep_link`만 넣고, 미존재 시 fallback(`room_id` 등)만 넣도록 상호배타 분기 적용 +- [x] `MainActivity` 딥링크/푸시 처리에서도 `deep_link` 존재 시 fallback과 병합하지 않도록 상호배타 분기 적용 +- [x] 정적 진단/빌드/테스트 및 결과 기록 + +## 검증 기록 +- 2026-03-13 + - 무엇/왜/어떻게: 작업 착수 전 요구사항에 맞는 푸시 터치/딥링크 처리 지점을 찾기 위해 코드베이스 탐색을 시작했다. + - 실행 명령: `grep(pattern="deep_link|deeplink|deepLink", path=".", include="*.{kt,kts,xml,java}")`, `grep(pattern="FirebaseMessagingService|notification|push|PendingIntent|MainActivity", path=".", include="*.{kt,java,xml}")` + - 결과: `SodaFirebaseMessagingService`, `MainActivity`, `DeepLinkActivity`를 핵심 수정 후보로 확인했다. +- 2026-03-13 + - 무엇/왜/어떻게: 푸시 탭 시 raw `deep_link` 유무로 분기하기 위해 FCM payload 전달/포그라운드 라우팅/MainActivity 파싱 로직을 함께 수정했다. + - 실행 명령: `git diff -- app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt` + - 결과: `deep_link` 전달(`SodaFirebaseMessagingService`) + 포그라운드 직접 이동 분기(`DeepLinkActivity`) + cold start 시 `MainActivity`의 `deep_link` 우선 파싱 분기가 반영됨을 확인했다. +- 2026-03-13 + - 무엇/왜/어떻게: 수정 파일 정적 진단 가능 여부를 확인했다. + - 실행 명령: `lsp_diagnostics(filePath="app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt")`, `lsp_diagnostics(filePath="app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt")`, `lsp_diagnostics(filePath="app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt")` + - 결과: 현재 실행 환경에서 Kotlin(`.kt`) LSP 서버가 미구성되어 진단을 수행할 수 없음을 확인했다. +- 2026-03-13 + - 무엇/왜/어떻게: 변경으로 인한 컴파일/단위테스트 회귀 여부를 확인하기 위해 Debug 빌드와 단위 테스트를 실행했다. + - 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 결과: `BUILD SUCCESSFUL`. +- 2026-03-13 + - 무엇/왜/어떻게: `deep_link`와 fallback 파라미터가 섞이지 않도록 FCM 번들 생성을 상호배타 분기로 변경했다. + - 실행 명령: `grep(pattern="val deepLinkExtras = if \(!deepLinkUrl.isNullOrBlank\(\)\)|putString\(\"deep_link\"|messageData\[\"room_id\"\]", path="app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt", output_mode="content")` + - 결과: `deep_link` 존재 시 `putString("deep_link", deepLinkUrl)`만 수행하고, fallback 필드(`room_id` 등)는 else 블록에서만 채워지도록 분리가 확인되었다. +- 2026-03-13 + - 무엇/왜/어떻게: 상호배타 분기 변경 이후 컴파일/테스트 회귀 여부를 재검증했다. + - 실행 명령: `lsp_diagnostics(filePath="app/src/main/java/kr/co/vividnext/sodalive/fcm/SodaFirebaseMessagingService.kt")`, `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 결과: Kotlin LSP 서버 미구성으로 `.kt` 진단은 불가, Gradle 검증은 `BUILD SUCCESSFUL`. +- 2026-03-13 + - 무엇/왜/어떻게: cold start 구간에서도 섞임이 없도록 `MainActivity.executeBundleDeeplink`의 `deep_link` 처리에서 기존 fallback 번들과 병합 로직을 제거했다. + - 실행 명령: `grep(pattern="mergedBundle|putAll\\(deepLinkBundle\\)|return executeBundleRoute\\(deepLinkBundle\\)|return executeBundleRoute\\(bundle\\)", path="app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt", output_mode="content")` + - 결과: `putAll(deepLinkBundle)`/병합 흔적 없이 `deep_link` 경로(`return executeBundleRoute(deepLinkBundle)`)와 fallback 경로(`return executeBundleRoute(bundle)`)가 상호배타로 분리됨을 확인했다. +- 2026-03-13 + - 무엇/왜/어떻게: `MainActivity` 분기 수정 후 컴파일/단위 테스트 회귀를 재확인했다. + - 실행 명령: `lsp_diagnostics(filePath="app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt")`, `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 결과: Kotlin LSP 서버 미구성으로 `.kt` 진단은 불가, Gradle 검증은 `BUILD SUCCESSFUL`.