fix(deeplink): 커뮤니티 댓글 딥링크 postId 라우팅을 정렬한다

This commit is contained in:
2026-03-13 21:39:20 +09:00
parent 598a04d084
commit 60677e262c
4 changed files with 212 additions and 9 deletions

View File

@@ -45,6 +45,8 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
private lateinit var imm: InputMethodManager
private var creatorId: Long = 0
private var deepLinkTargetPostId: Long = 0
private var isDeepLinkCommentHandled = false
private var isListMode = false
private var listAnchorPosition = 0
private var gridAnchorPosition = 0
@@ -114,6 +116,10 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
}
creatorId = intent.getLongExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, 0)
deepLinkTargetPostId = intent.getLongExtra(Constants.EXTRA_COMMUNITY_POST_ID, 0)
if (deepLinkTargetPostId <= 0) {
deepLinkTargetPostId = intent.getStringExtra(Constants.EXTRA_COMMUNITY_POST_ID)?.toLongOrNull() ?: 0
}
if (creatorId <= 0) {
Toast.makeText(
applicationContext,
@@ -121,6 +127,7 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
Toast.LENGTH_LONG
).show()
finish()
return
}
bindData()
@@ -178,15 +185,7 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
)
},
showCommentBottomSheetDialog = { postId, existOrdered ->
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<ActivityCreatorCommunityAllBind
loadingDialog.show(screenWidth, "")
} else {
loadingDialog.dismiss()
openDeepLinkTargetCommentIfNeeded()
}
}
@@ -275,9 +275,50 @@ class CreatorCommunityAllActivity : BaseActivity<ActivityCreatorCommunityAllBind
gridAdapter.items.addAll(it)
listAdapter.notifyDataSetChanged()
gridAdapter.notifyDataSetChanged()
openDeepLinkTargetCommentIfNeeded()
}
}
private fun openDeepLinkTargetCommentIfNeeded() {
if (isDeepLinkCommentHandled || deepLinkTargetPostId <= 0) {
return
}
val targetPost = listAdapter.items.firstOrNull { it.postId == deepLinkTargetPostId }
if (targetPost != null) {
isDeepLinkCommentHandled = true
switchToListMode(listAdapter.items.indexOfFirst { it.postId == targetPost.postId }, fromGridItemClick = false)
showCommentBottomSheet(postId = targetPost.postId, existOrdered = targetPost.existOrdered)
return
}
if (viewModel.isLast) {
isDeepLinkCommentHandled = true
Toast.makeText(
applicationContext,
getString(R.string.screen_creator_community_all_error_invalid_request),
Toast.LENGTH_LONG
).show()
return
}
if (viewModel.isLoading.value != true) {
viewModel.getCommunityPostList()
}
}
private fun showCommentBottomSheet(postId: Long, existOrdered: Boolean) {
val dialog = CreatorCommunityCommentFragment(
creatorId = creatorId,
postId = postId,
existOrdered = existOrdered
)
dialog.show(
supportFragmentManager,
dialog.tag
)
}
private fun setupRecyclerViews() {
val listRecyclerView = binding.rvCreatorCommunity
listRecyclerView.layoutManager = LinearLayoutManager(

View File

@@ -111,6 +111,15 @@ class DeepLinkActivity : AppCompatActivity() {
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)
}
}
intent.getBundleExtra(Constants.EXTRA_DATA)?.let { source ->
@@ -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

View File

@@ -336,6 +336,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(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>(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>(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>(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
}

View File

@@ -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 `<data>` 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` 인텐트로 전달됨을 재확인했다.