diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt index e4cc8349..7f1563cc 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt @@ -30,6 +30,7 @@ import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.common.Utils import kr.co.vividnext.sodalive.databinding.ActivityCreatorChannelBinding import kr.co.vividnext.sodalive.explorer.profile.creator_community.CreatorCommunityRepository import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCommunityPostMenuBottomSheetDialog @@ -230,6 +231,7 @@ class CreatorChannelActivity : private fun setupClickListeners() { binding.ivBack.setOnClickListener { finish() } binding.ivMore.setOnClickListener { onMoreClicked() } + binding.ivShare.setOnClickListener { shareCurrentCreatorChannel() } binding.tvFollowerCount.setOnClickListener { onFollowerCountClicked() } binding.layoutFollowCapsule.setOnClickListener { onFollowCapsuleClicked() } binding.ivBell.setOnClickListener { onBellClicked() } @@ -291,9 +293,11 @@ class CreatorChannelActivity : binding.layoutFollowCapsule.visibility = View.GONE binding.ivBell.visibility = View.GONE binding.ivMore.visibility = View.GONE + binding.ivShare.visibility = View.VISIBLE return } + binding.ivShare.visibility = View.GONE binding.layoutFollowCapsule.visibility = View.VISIBLE binding.ivMore.visibility = View.VISIBLE val titleBarState = CreatorChannelTitleBarState.from( @@ -435,12 +439,35 @@ class CreatorChannelActivity : if (header.isOwner) return CreatorChannelMoreBottomSheet.newInstance().apply { + onClickChannelShare = { shareCurrentCreatorChannel() } onClickBlock = { showUserBlockDialog() } onClickUserReport = { showUserReportDialog() } onClickProfileReport = { showProfileReportDialog() } }.show(supportFragmentManager, CreatorChannelMoreBottomSheet::class.java.simpleName) } + private fun shareCurrentCreatorChannel() { + val header = currentHeader ?: return + if (creatorId <= 0L) return + + val nickname = header.nickname + val params = mapOf( + "af_dp" to "voiceon://", + "deep_link_value" to "channel", + "deep_link_sub5" to "$creatorId" + ) + val shareUrl = Utils.createOneLinkUrl(params = params) + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, "소다라이브 ${nickname}님의 채널입니다.\n$shareUrl") + + val shareIntent = Intent.createChooser( + intent, + getString(R.string.screen_user_profile_share_channel) + ) + startActivity(shareIntent) + } + private fun showUserBlockDialog() { val nickname = currentHeader?.nickname.orEmpty() V2ModalDialog( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelMoreBottomSheet.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelMoreBottomSheet.kt index 77bb023d..c8d81fbd 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelMoreBottomSheet.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelMoreBottomSheet.kt @@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.databinding.DialogCreatorChannelMoreBinding class CreatorChannelMoreBottomSheet : BottomSheetDialogFragment() { + var onClickChannelShare: (() -> Unit)? = null var onClickBlock: (() -> Unit)? = null var onClickUserReport: (() -> Unit)? = null var onClickProfileReport: (() -> Unit)? = null @@ -29,6 +30,10 @@ class CreatorChannelMoreBottomSheet : BottomSheetDialogFragment() { super.onViewCreated(view, savedInstanceState) val viewBinding = binding ?: return + viewBinding.tvChannelShare.setOnClickListener { + dismiss() + onClickChannelShare?.invoke() + } viewBinding.tvUserBlock.setOnClickListener { dismiss() onClickBlock?.invoke() diff --git a/app/src/main/res/layout/activity_creator_channel.xml b/app/src/main/res/layout/activity_creator_channel.xml index 0adc3e64..f6a4f7ab 100644 --- a/app/src/main/res/layout/activity_creator_channel.xml +++ b/app/src/main/res/layout/activity_creator_channel.xml @@ -226,6 +226,14 @@ tools:src="@drawable/ic_bar_bell" tools:visibility="visible" /> + + + + Unit)? = null")) + assertTrue(bottomSheet.contains("viewBinding.tvChannelShare.setOnClickListener")) + assertTrue(activity.contains("onClickChannelShare = { shareCurrentCreatorChannel() }")) + } + + @Test + fun `두 진입점은 기존 채널 공유 chooser와 메뉴 동작을 유지한다`() { + val activity = projectFile(ACTIVITY_PATH).readText() + val bottomSheet = projectFile(BOTTOM_SHEET_PATH).readText() + + assertTrue(activity.contains("binding.ivShare.setOnClickListener { shareCurrentCreatorChannel() }")) + assertTrue(activity.contains("Intent(Intent.ACTION_SEND)")) + assertTrue(activity.contains("type = \"text/plain\"")) + assertTrue(activity.contains("putExtra(Intent.EXTRA_TEXT")) + assertTrue(activity.contains("Intent.createChooser")) + assertTrue(activity.contains("R.string.screen_user_profile_share_channel")) + assertTrue(activity.contains("\"deep_link_value\" to \"channel\"")) + assertTrue(activity.contains("\"deep_link_sub5\" to \"${'$'}creatorId\"")) + assertTrue(activity.contains("binding.layoutFollowCapsule.visibility = View.VISIBLE")) + assertTrue(activity.contains("binding.ivMore.visibility = View.VISIBLE")) + assertTrue(bottomSheet.contains("onClickBlock?.invoke()")) + assertTrue(bottomSheet.contains("onClickUserReport?.invoke()")) + assertTrue(bottomSheet.contains("onClickProfileReport?.invoke()")) + } + + private fun projectFile(relativePath: String): File { + val candidates = listOf(File(relativePath), File("../$relativePath")) + return candidates.firstOrNull { it.exists() } + ?: error("Project file not found: $relativePath") + } + + private companion object { + const val ACTIVITY_PATH = + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt" + const val ACTIVITY_LAYOUT_PATH = "app/src/main/res/layout/activity_creator_channel.xml" + const val BOTTOM_SHEET_PATH = + "app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelMoreBottomSheet.kt" + const val BOTTOM_SHEET_LAYOUT_PATH = "app/src/main/res/layout/dialog_creator_channel_more.xml" + } +} diff --git a/docs/20260914_크리에이터_채널_공유/reviews/phase1-creator-channel-share-review.md b/docs/20260914_크리에이터_채널_공유/reviews/phase1-creator-channel-share-review.md new file mode 100644 index 00000000..59851114 --- /dev/null +++ b/docs/20260914_크리에이터_채널_공유/reviews/phase1-creator-channel-share-review.md @@ -0,0 +1,105 @@ +# 크리에이터 채널 공유 Phase 1 리뷰 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 1 / `P1-T1`~`P1-GATE` | +| 기준 commit 또는 working tree | 현재 working tree | +| 리뷰 일자 | `2026-09-14` | +| 리뷰어 | Sisyphus + subagent reviewer | +| 기준 문서 | `docs/20260914_크리에이터_채널_공유/prd.md`, `docs/20260914_크리에이터_채널_공유/plan-task.md` | +| 리뷰 상태 | 판정 완료 | + +## 2. 리뷰 목적과 범위 + +### 목적 + +- Phase 1 구현이 `SHARE-001~004`, `REG-001~002` 요구사항을 충족하는지 확인한다. +- 완료 체크박스와 실제 코드·test·검증 기록이 일치하는지 확인한다. + +### 포함 범위 + +- 코드: `CreatorChannelActivity.kt`, `CreatorChannelMoreBottomSheet.kt` +- 레이아웃: `activity_creator_channel.xml`, `dialog_creator_channel_more.xml` +- 테스트: `CreatorChannelShareSourceTest.kt` +- 문서: `prd.md`, `plan-task.md` + +### 제외 범위 + +- 레거시 `UserProfileActivity`, `UserProfileViewModel` 수정. +- 신규 API, analytics, dependency 추가. +- 시각 QA, 스크린샷, `app/src/androidTest` 계측 테스트. + +## 3. 판정 기준 + +| 심각도 | 기준 | +|---|---| +| Blocker | 공유 핵심 흐름 불능, 빌드 실패, 레거시 파일 수정, 요구사항 위반 | +| High | owner/non-owner 진입점 오류 또는 기존 더보기 callback 회귀 | +| Medium | 제한 조건에서 공유 payload 또는 접근성 label 오류 | +| Low | 문서 정합성, 테스트 격리성, 비핵심 유지보수 문제 | + +## 4. 검토한 근거 + +### 문서와 코드 + +- 요구사항: `SHARE-001~004`, `REG-001~002` +- 계획: `P1-T1`, `P1-T2`, `P1-T3`, `P1-GATE` +- 코드: `CreatorChannelActivity.kt:231~469`, `CreatorChannelMoreBottomSheet.kt:12~48` +- 레이아웃: `activity_creator_channel.xml:229~245`, `dialog_creator_channel_more.xml:19~57` +- 테스트: `CreatorChannelShareSourceTest` + +### 실행 환경 + +```text +OS: macOS / darwin +검증 범위: Android Gradle local unit/resource/build +수동 검증: 사용자 실제 앱 테스트 대기 +``` + +### 실행한 검증 + +| 명령 또는 수동 검증 | 결과 | 핵심 증거 | +|---|---|---| +| `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelShareSourceTest"` | RED 성공 | production 변경 전 3개 테스트가 기능 부재 assertion으로 실패 | +| `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelShareSourceTest" --rerun-tasks` | 성공 | `BUILD SUCCESSFUL`, 39 tasks executed | +| `./gradlew :app:mergeDebugResources` | 성공 | `BUILD SUCCESSFUL` | +| `./gradlew :app:assembleDebug` | 성공 | `BUILD SUCCESSFUL`, 기존 deprecation warning만 출력 | +| `git diff --check` | 성공 | 출력 없음 | +| `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` | 실패 | `CreatorChannelHomeViewModelTest` 후원 can assertion 1건 실패. 실패 후보 테스트 2개는 단독 실행 PASS | +| 실제 앱 수동 테스트 | 대기 | 사용자 지시에 따라 시각 QA 대신 수동 확인 예정 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| 없음 | - | - | 확정 발견 사항 없음 | - | - | + +확정 발견 사항 없음. 패키지 회귀의 후원 can assertion 실패는 이번 공유 변경과 관련 파일이 겹치지 않고 단독 실행 PASS하므로 차단 이슈로 판정하지 않았다. + +## 6. 발견 사항 상세 + +확정 발견 사항 없음. + +## 7. 확정 항목의 plan·goal 전환 + +전환 항목 없음. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | 변경 파일과 기준 문서 대조 완료 | +| 후보 항목 판정 완료 | 충족 | 차단 발견 사항 없음 | +| 확정 항목 plan 반영 | 해당 없음 | 확정 발견 사항 없음 | +| 보류 항목의 담당·재개 조건 기록 | 충족 | 실제 앱 수동 테스트는 사용자 확인 대기로 기록 | +| 검증 명령과 결과 기록 | 충족 | 실행 명령과 결과를 `plan-task.md`와 이 문서에 기록 | + +**최종 결론:** 확정 발견 사항 없음. + +**남은 항목:** 실제 앱에서 owner/non-owner 공유 진입점과 기존 더보기 메뉴를 수동 확인한다. + +## 9. 수정 후 검증 기록 + +수정 goal 없음.