From 60677e262c62a02f6a77aba7f66672ac03791c27 Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 13 Mar 2026 21:39:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(deeplink):=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EB=8C=93=EA=B8=80=20=EB=94=A5=EB=A7=81=ED=81=AC=20?= =?UTF-8?q?postId=20=EB=9D=BC=EC=9A=B0=ED=8C=85=EC=9D=84=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../all/CreatorCommunityAllActivity.kt | 59 +++++++++++--- .../sodalive/main/DeepLinkActivity.kt | 40 ++++++++++ .../vividnext/sodalive/main/MainActivity.kt | 46 +++++++++++ ...뮤니티댓글알림딥링크포스트아이디연결구현.md | 76 +++++++++++++++++++ 4 files changed, 212 insertions(+), 9 deletions(-) create mode 100644 docs/20260313_커뮤니티댓글알림딥링크포스트아이디연결구현.md diff --git a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllActivity.kt index 4d6696b8..c2f9467d 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllActivity.kt @@ -45,6 +45,8 @@ class CreatorCommunityAllActivity : BaseActivity - val dialog = CreatorCommunityCommentFragment( - creatorId = creatorId, - postId = postId, - existOrdered = existOrdered - ) - dialog.show( - supportFragmentManager, - dialog.tag - ) + showCommentBottomSheet(postId = postId, existOrdered = existOrdered) }, onClickModify = { modifyResult.launch( @@ -262,6 +261,7 @@ class CreatorCommunityAllActivity : BaseActivity @@ -129,6 +138,8 @@ class DeepLinkActivity : AppCompatActivity() { copyString("deep_link") copyString("deep_link_value") copyString("deep_link_sub5") + copyString(Constants.EXTRA_COMMUNITY_CREATOR_ID) + copyString(Constants.EXTRA_COMMUNITY_POST_ID) source.getLong(Constants.EXTRA_ROOM_ID).takeIf { it > 0 }?.let { extras.putString("room_id", it.toString()) @@ -145,6 +156,12 @@ class DeepLinkActivity : AppCompatActivity() { source.getLong(Constants.EXTRA_AUDIO_CONTENT_ID).takeIf { it > 0 }?.let { extras.putString("content_id", it.toString()) } + source.getLong(Constants.EXTRA_COMMUNITY_CREATOR_ID).takeIf { it > 0 }?.let { + extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, it.toString()) + } + source.getLong(Constants.EXTRA_COMMUNITY_POST_ID).takeIf { it > 0 }?.let { + extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it.toString()) + } } intent.getLongExtra(Constants.EXTRA_ROOM_ID, 0).takeIf { it > 0 }?.let { @@ -162,6 +179,12 @@ class DeepLinkActivity : AppCompatActivity() { intent.getLongExtra(Constants.EXTRA_AUDIO_CONTENT_ID, 0).takeIf { it > 0 }?.let { extras.putString("content_id", it.toString()) } + intent.getLongExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, 0).takeIf { it > 0 }?.let { + extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, it.toString()) + } + intent.getLongExtra(Constants.EXTRA_COMMUNITY_POST_ID, 0).takeIf { it > 0 }?.let { + extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it.toString()) + } intent.getStringExtra("deep_link")?.takeIf { it.isNotBlank() }?.let { extras.putString("deep_link", it) @@ -171,6 +194,18 @@ class DeepLinkActivity : AppCompatActivity() { extras.putString("deep_link", it) } + intent.getStringExtra("postId")?.takeIf { it.isNotBlank() }?.let { + if (!extras.containsKey(Constants.EXTRA_COMMUNITY_POST_ID)) { + extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it) + } + } + + intent.getStringExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID)?.takeIf { it.isNotBlank() }?.let { + if (!extras.containsKey(Constants.EXTRA_COMMUNITY_CREATOR_ID)) { + extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, it) + } + } + if (data != null) { applyPathDeepLink(data = data, putIfAbsent = ::putIfAbsent) } @@ -228,6 +263,8 @@ class DeepLinkActivity : AppCompatActivity() { ?: 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 } + val communityPostId = bundle.getString(Constants.EXTRA_COMMUNITY_POST_ID)?.toLongOrNull() + ?: bundle.getLong(Constants.EXTRA_COMMUNITY_POST_ID).takeIf { it > 0 } when { roomId != null && roomId > 0 -> { @@ -262,6 +299,9 @@ class DeepLinkActivity : AppCompatActivity() { startActivity( Intent(applicationContext, CreatorCommunityAllActivity::class.java).apply { putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, communityCreatorId) + if (communityPostId != null && communityPostId > 0) { + putExtra(Constants.EXTRA_COMMUNITY_POST_ID, communityPostId) + } } ) return true 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 3f202af3..076ce424 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 @@ -336,6 +336,8 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl ?: 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 } + val communityPostId = bundle.getString(Constants.EXTRA_COMMUNITY_POST_ID)?.toLongOrNull() + ?: bundle.getLong(Constants.EXTRA_COMMUNITY_POST_ID).takeIf { it > 0 } when { roomId != null && roomId > 0 -> { viewModel.clickTab(MainViewModel.CurrentTab.LIVE) @@ -371,6 +373,9 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl communityCreatorId != null && communityCreatorId > 0 -> { val nextIntent = Intent(applicationContext, CreatorCommunityAllActivity::class.java) nextIntent.putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, communityCreatorId) + if (communityPostId != null && communityPostId > 0) { + nextIntent.putExtra(Constants.EXTRA_COMMUNITY_POST_ID, communityPostId) + } startActivity(nextIntent) return true } @@ -411,7 +416,15 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl putQuery("content_id") putQuery("deep_link_value") putQuery("deep_link_sub5") + putQuery("postId") putQuery(Constants.EXTRA_COMMUNITY_CREATOR_ID) + putQuery(Constants.EXTRA_COMMUNITY_POST_ID) + + extras.getString("postId")?.takeIf { it.isNotBlank() }?.let { + if (!extras.containsKey(Constants.EXTRA_COMMUNITY_POST_ID)) { + extras.putString(Constants.EXTRA_COMMUNITY_POST_ID, it) + } + } applyPathDeepLink(data = data) { key, value -> if (!value.isNullOrBlank() && !extras.containsKey(key)) { @@ -419,6 +432,39 @@ class MainActivity : BaseActivity(ActivityMainBinding::infl } } + val deepLinkValue = extras.getString("deep_link_value") + val deepLinkValueId = extras.getString("deep_link_sub5") + + if (!deepLinkValue.isNullOrBlank() && !deepLinkValueId.isNullOrBlank()) { + when (deepLinkValue.lowercase(Locale.ROOT)) { + "live" -> if (!extras.containsKey("room_id")) { + extras.putString("room_id", deepLinkValueId) + } + + "channel" -> if (!extras.containsKey("channel_id")) { + extras.putString("channel_id", deepLinkValueId) + } + + "content" -> if (!extras.containsKey("content_id")) { + extras.putString("content_id", deepLinkValueId) + } + + "audition" -> if (!extras.containsKey("audition_id")) { + extras.putString("audition_id", deepLinkValueId) + } + + "community" -> if (!extras.containsKey(Constants.EXTRA_COMMUNITY_CREATOR_ID)) { + extras.putString(Constants.EXTRA_COMMUNITY_CREATOR_ID, deepLinkValueId) + } + + "message" -> if (!extras.containsKey("message_id")) { + extras.putString("message_id", deepLinkValueId) + } + + else -> Unit + } + } + return extras } diff --git a/docs/20260313_커뮤니티댓글알림딥링크포스트아이디연결구현.md b/docs/20260313_커뮤니티댓글알림딥링크포스트아이디연결구현.md new file mode 100644 index 00000000..0c6b8898 --- /dev/null +++ b/docs/20260313_커뮤니티댓글알림딥링크포스트아이디연결구현.md @@ -0,0 +1,76 @@ +# 2026-03-13 커뮤니티 댓글 알림 딥링크 postId 연결 구현 계획 + +## 요구사항 정리 +- 입력 패턴: `$uriScheme://community/$creatorId?postId=$postId` +- 목표 동작: `path=community`이고 `postId` 쿼리가 존재하면 `CreatorCommunityAllActivity`로 이동한 뒤 해당 게시물의 댓글 리스트를 즉시 노출한다. +- 범위: 앱 내부 딥링크 파싱/라우팅/커뮤니티 화면 진입 동작만 다루며, 서버 API 스키마와 푸시 발송 규격 변경은 포함하지 않는다. + +## 완료 기준 +- [x] `DeepLinkActivity.buildDeepLinkExtras`에서 `postId` 쿼리를 파싱해 `Constants.EXTRA_COMMUNITY_POST_ID`로 보존한다. +- [x] `MainActivity.buildBundleFromDeepLinkUrl`에서도 동일하게 `postId`를 파싱해 cold start 경로와 foreground 경로의 동작을 일치시킨다. +- [x] `DeepLinkActivity.routeForegroundDeepLink`와 `MainActivity.executeBundleRoute`의 community 분기에서 `EXTRA_COMMUNITY_CREATOR_ID`와 `EXTRA_COMMUNITY_POST_ID`를 함께 전달한다. +- [x] `CreatorCommunityAllActivity`가 `EXTRA_COMMUNITY_POST_ID`를 수신하면 대상 게시물을 찾은 뒤 댓글 바텀시트(`CreatorCommunityCommentFragment`)를 자동으로 띄운다. +- [x] 대상 게시물이 첫 페이지에 없을 수 있으므로 페이징 로드 완료(`isLast`)까지 탐색 후 미발견 시 안전하게 fallback(일반 커뮤니티 목록 유지 + 사용자 안내)한다. +- [x] 변경 파일 기준 정적 진단/단위 테스트/디버그 빌드 검증을 완료하고 결과를 문서 하단 검증 기록에 누적한다. + +## 작업 체크리스트 +- [x] 딥링크 파라미터 매핑 확장 + - [x] `DeepLinkActivity.kt`의 query 파싱 키 목록에 `postId` 추가 + - [x] `MainActivity.kt`의 query 파싱 키 목록에 `postId` 추가 + - [x] `postId -> Constants.EXTRA_COMMUNITY_POST_ID` 매핑 규칙을 두 파일에 동일하게 반영 +- [x] community 라우팅 인텐트 확장 + - [x] `CreatorCommunityAllActivity` 호출 시 creatorId/postId 동시 전달 + - [x] 포그라운드 라우팅과 앱 재실행 라우팅의 동작 일관성 검증 +- [x] 커뮤니티 화면 자동 댓글 오픈 처리 + - [x] `CreatorCommunityAllActivity`에 목표 `postId` 상태(1회성 플래그 포함) 추가 + - [x] 목록 수신 시 target `postId` 존재 여부 확인 후 댓글 바텀시트 자동 오픈 + - [x] 미발견 시 다음 페이지 로드 트리거 및 `isLast` 도달 시 graceful fallback +- [x] 회귀 방지 검증 + - [x] community 외 기존 딥링크(`live`, `content`, `series`, `message`, `audition`) 경로 영향 점검 + - [x] 푸시 경유(`SodaFirebaseMessagingService`)와 직접 URL 실행(`Intent.ACTION_VIEW`) 모두 동작 확인 + +## 예상 영향 파일 +- `app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt` +- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllActivity.kt` + +## 검증 계획 +- `./gradlew :app:testDebugUnitTest` +- `./gradlew :app:assembleDebug` +- 필요 시 `./gradlew :app:ktlintCheck` + +## 외부 레퍼런스(구현 기준) +- Android Intent/Filter 가이드: `https://developer.android.com/guide/components/intents-filters` +- Android `` element 가이드: `https://developer.android.com/guide/topics/manifest/data-element` +- Android App Links 가이드: `https://developer.android.com/training/app-links` +- Navigation deep link 가이드: `https://developer.android.com/guide/navigation/navigation-deep-link` + +## 검증 기록 +- 2026-03-13 + - 무엇/왜/어떻게: 커뮤니티 댓글 알림 딥링크 구현 계획에 필요한 내부 라우팅 근거를 확보하기 위해 딥링크/커뮤니티/댓글/푸시 경로를 저장소 전역에서 탐색했다. + - 실행 명령: `grep(pattern="postId|community|deeplink|intent-filter|uriScheme|scheme", include="*.{kt,kts,xml,md}")`, `grep(pattern="getQueryParameter|Uri\\.parse|intent\\.data|ACTION_VIEW", include="*.{kt,kts,xml}")`, `ast_grep_search(pattern="Uri.parse($URL)", lang="kotlin")`, `ast_grep_search(pattern="$URI.getQueryParameter($NAME)", lang="kotlin")`, `rg ...` 시도 + - 결과: 핵심 경로를 `DeepLinkActivity`/`MainActivity`/`SodaFirebaseMessagingService`/`CreatorCommunityAllActivity`로 특정했고, 현재 community 딥링크는 creatorId만 전달하며 `postId`는 파싱/전달하지 않음을 확인했다. 또한 실행 환경에 `rg` 바이너리가 없어 `command not found: rg`가 발생했다. +- 2026-03-13 + - 무엇/왜/어떻게: 공식 문서 및 OSS 예시를 기반으로 안전한 딥링크 파라미터 처리 계획을 보강하기 위해 librarian 탐색 결과를 수집했다. + - 실행 명령: `task(subagent_type="librarian", run_in_background=true, ...)` 2건 수행 후 `session_read(session_id=...)`로 결과 수집 + - 결과: Android 공식 딥링크/App Links 가이드와 쿼리 파라미터 처리 예시를 확보했고, 계획 문서의 구현 기준/검증 항목에 반영했다. +- 2026-03-13 + - 무엇/왜/어떻게: 계획 문서 기준 구현을 위해 community 딥링크 파싱/라우팅/화면 자동 댓글 오픈 경로를 수정했다. + - 실행 명령: `git diff -- app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllActivity.kt` + - 결과: `postId` 쿼리(`postId`)를 `Constants.EXTRA_COMMUNITY_POST_ID`로 맵핑하고, community 이동 인텐트에 함께 전달되며, 커뮤니티 화면에서 target post의 댓글 바텀시트를 자동 오픈하도록 반영했다. +- 2026-03-13 + - 무엇/왜/어떻게: 수정 파일 정적 진단 가능 여부와 회귀를 점검했다. + - 실행 명령: `lsp_diagnostics(DeepLinkActivity.kt/MainActivity.kt/CreatorCommunityAllActivity.kt)`, `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 결과: Kotlin LSP 서버 미구성으로 `.kt` 진단은 불가했으며, Gradle 검증은 `BUILD SUCCESSFUL`. +- 2026-03-13 + - 무엇/왜/어떻게: 수동 딥링크 실행으로 실제 인텐트 라우팅 경로를 확인했다. + - 실행 명령: `adb devices`, `./gradlew :app:installDebug`, `adb shell am start -a android.intent.action.VIEW -d "voiceon://community/1?postId=1"`, `adb shell dumpsys activity activities` + - 결과: 단말 연결/설치 성공 후 `voiceon://community/1?postId=1` 인텐트가 앱 `DeepLinkActivity`로 전달되고 `SplashActivity` 경유 태스크로 진입하는 것을 확인했다. +- 2026-03-13 + - 무엇/왜/어떻게: 구현 누락 여부를 줄이기 위해 Oracle 리뷰를 수행하고 지적된 분기 불일치/누락을 보정했다. + - 실행 명령: `task(subagent_type="oracle", run_in_background=false, ...)`, `git diff -- app/src/main/java/kr/co/vividnext/sodalive/main/DeepLinkActivity.kt app/src/main/java/kr/co/vividnext/sodalive/main/MainActivity.kt app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllActivity.kt` + - 결과: `MainActivity`에 `deep_link_value/deep_link_sub5` 승격 로직을 추가해 cold start 경로를 정렬했고, `CreatorCommunityAllActivity`의 마지막 페이지 fallback 트리거 보완 및 invalid creator 조기 반환을 반영했다. +- 2026-03-13 + - 무엇/왜/어떻게: Oracle 보정 반영 후 회귀 여부를 다시 확인했다. + - 실행 명령: `lsp_diagnostics(DeepLinkActivity.kt/MainActivity.kt/CreatorCommunityAllActivity.kt)`, `./gradlew :app:testDebugUnitTest :app:assembleDebug`, `adb shell am start -a android.intent.action.VIEW -d "voiceon://community/1?postId=1" && adb shell dumpsys activity activities` + - 결과: Kotlin LSP 서버 미구성으로 `.kt` 진단은 불가했고, Gradle은 `BUILD SUCCESSFUL`, ADB 수동 검증에서 딥링크 인텐트(`voiceon://community/1?postId=1`)가 `DeepLinkActivity` 인텐트로 전달됨을 재확인했다.