Files
sodalive-android/docs/20260622_크리에이터_채널_후원_탭/plan-task.md

536 lines
61 KiB
Markdown

# 크리에이터 채널 후원 탭 구현 계획/TASK
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans` to implement this plan task-by-task. 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.
**Goal:** `GET /api/v2/creator-channels/{creatorId}/donations` 응답을 기반으로 크리에이터 채널 `후원` 탭에 후원 랭킹, 전체 후원 수, 후원 내역 목록, empty 상태, 후원하기 액션, pagination을 표시한다.
**Architecture:** 기존 `CreatorChannelActivity``ViewPager2`/`CreatorChannelPagerAdapter` 구조를 유지하고, `CreatorChannelTab.Donation`의 placeholder를 신규 `CreatorChannelDonationFragment`로 교체한다. 후원 탭 전용 Fragment/ViewModel/DTO/mapper/adapter는 `kr.co.vividnext.sodalive.v2.creator.channel.donation` 하위에 두되, API/Repository는 기존 채널 공통 `CreatorChannelApi`/`CreatorChannelRepository`에 endpoint만 최소 추가한다. 후원하기 dialog는 홈 탭과 동일한 `LiveRoomDonationDialog` 표시 경로를 Activity에 공통 helper로 분리해 재사용하고, 후원 탭 ViewModel은 후원 성공 후 첫 페이지를 재조회한다.
**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test.
---
## 전제와 성공 기준
- PRD: `docs/20260622_크리에이터_채널_후원_탭/prd.md`
- 기존 채널 컨테이너:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
- `app/src/main/res/layout/activity_creator_channel.xml`
- 기존 채널 API/Repository:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
- 기존 홈 탭 후원 UI/후원 액션 참조:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
- `app/src/main/res/layout/item_creator_channel_home_donation.xml`
- `app/src/main/res/layout/item_creator_channel_home_donation_row.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationDialog.kt`
- 기존 후원 전체보기 참조:
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAllViewActivity.kt`
- `Constants.EXTRA_USER_ID`
- Figma:
- 전체 후원 탭: `290:9093`
- 후원 랭킹 섹션: `290:9097`
- 후원 empty 상태: `290:9008`
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/donations`이다.
- 첫 페이지 `page``0`, 기본 `size``20`이다.
- query parameter는 `page`, `size`만 전달한다.
- 정렬 query parameter와 sort popup은 후원 탭에서 사용하지 않는다.
- `rankings`는 서버가 항상 최대 8명까지만 내려준다.
- 후원 랭킹 `전체보기` 버튼은 기존 `UserProfileDonationAllViewActivity`로 이동한다.
- 현재 API는 비밀 후원 여부를 별도 필드로 내려주지 않으므로 비밀 후원 전용 UI/표시 분기는 구현하지 않는다.
- 본인 채널에서는 content/empty 상태 모두 floating 후원하기 버튼을 숨긴다.
- 본인 채널 empty 상태에서는 중앙 `후원하기` button도 숨긴다.
- 후원 성공 후에는 후원 탭의 `donationCount`, `rankings`, `donations`가 최신화되도록 첫 페이지를 재조회한다.
- 구현 완료 후 최소 다음 명령을 실행한다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Donation*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:compileDebugKotlin`
- `./gradlew :app:ktlintCheck`
- `git diff --check`
---
## Figma 참조 필요 Phase
- Phase 1: 제한 참조
- 기존 placeholder, 후원 dialog, 전체보기 Activity, owner 판정 경계 확인이 중심이며 Figma는 PRD 기준만 확인한다.
- Phase 2: Figma 참조 불필요
- API/DTO/Repository/ViewModel 계약은 서버 응답과 기존 FanTalk/Community 탭 패턴을 따른다.
- Phase 3: 제한 참조
- mapper와 UI model은 PRD와 기존 홈 후원 카드 정책을 함께 확인한다.
- Phase 4: 필수 참조
- 랭킹 카드, Sort-bar without sort, 후원 내역 item은 Figma `290:9093`, `290:9097` 기준으로 구현한다.
- Phase 5: 필수 참조
- empty 상태와 empty `후원하기` button은 Figma `290:9008` 기준으로 구현한다.
- Phase 6: 제한 참조
- 탭 연결, pagination, 후원 dialog 재사용, 전체보기 이동은 기존 코드 패턴 중심으로 검증한다.
- Phase 7: 필수 참조
- 최종 수동 화면 검증은 PRD의 Figma 노드와 실제 화면을 대조한다.
---
## 파일 구조
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
- `CreatorChannelTab.Donation`을 신규 `CreatorChannelDonationFragment`로 연결한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- `CreatorChannelDonationFragment.Host` 구현, 후원 탭 선택 시 최초 로드, pagination trigger, ViewPager 높이 갱신, 전체보기 이동, 공통 후원 dialog 표시 helper, 후원 성공 후 홈 탭 refresh hook을 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt`
- 후원 탭 endpoint를 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
- 후원 목록 조회 method를 추가한다. 기존 `postChannelDonation()`은 재사용한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/data/CreatorChannelDonationTabResponse.kt`
- `CreatorChannelDonationTabResponse`, `MemberDonationRankingResponse`, `CreatorChannelDonationResponse`를 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationViewModel.kt`
- 최초 조회, retry, refresh, pagination, 후원 성공 후 재조회, loading/error/empty/content 상태를 관리한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/model/CreatorChannelDonationUiModels.kt`
- 랭킹 item, 후원 item, 화면 상태 UI model을 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/model/CreatorChannelDonationMappers.kt`
- DTO를 UI model로 변환하고 순위 번호, 날짜 포맷, message fallback, header 색상을 결정한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragment.kt`
- 후원 탭 UI, adapter, retry, pagination error toast, 후원하기 callback, 전체보기 callback, host callback 연결을 담당한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/ui/CreatorChannelDonationAdapter.kt`
- 랭킹 섹션, count bar, 후원 내역 목록을 하나의 RecyclerView 또는 섹션 adapter로 표시한다.
- 생성 가능: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/ui/CreatorChannelDonationRankingAdapter.kt`
- 랭킹 grid가 nested RecyclerView로 구현될 때만 추가한다.
- 생성: `app/src/main/res/layout/fragment_creator_channel_donation.xml`
- RecyclerView, empty, error/retry, floating 후원하기 버튼 영역을 포함한다.
- 생성: `app/src/main/res/layout/item_creator_channel_donation_ranking.xml`
- `후원 랭킹` 카드, 4열 x 2행 랭킹 grid, `전체보기` button을 구현한다.
- 생성: `app/src/main/res/layout/item_creator_channel_donation.xml`
- 후원 내역 item header, 캔 badge, 메시지를 구현한다.
- 생성 가능: `app/src/main/res/layout/item_creator_channel_donation_ranking_member.xml`
- 랭킹 member cell을 별도 adapter로 구현할 때만 추가한다.
- 생성 가능: `app/src/main/res/drawable/bg_creator_channel_donation_empty_button.xml`
- empty `후원하기` capsule 배경이 기존 drawable로 대응되지 않을 때만 추가한다.
- 수정: `app/src/main/res/values/strings.xml`
- 후원 탭 empty/error/retry/action/count/fallback 문구를 추가하거나 기존 문자열을 재사용한다.
- 수정: `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml`
- 신규 문자열의 다국어 값을 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
- `CreatorChannelDonationViewModel` binding을 추가한다.
- 테스트 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationMapperTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationViewModelTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationPaginationTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragmentLayoutTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationActionTest.kt`
- 테스트 수정:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
---
### Phase 1: 기존 구조 확인과 작업 경계 고정
- [x] **Task 1.1: Donation 탭 placeholder 연결 지점 확인**
- 확인 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt`
- 작업:
- `CreatorChannelTab.Donation`이 현재 `CreatorChannelPlaceholderFragment`로 연결되는지 확인한다.
- 신규 `CreatorChannelDonationFragment.newInstance(creatorId)`로 교체할 위치를 고정한다.
- 검증:
- 실행: `rg -n "CreatorChannelTab\\.Donation|CreatorChannelPlaceholderFragment|createFragment" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel`
- 기대 결과: Donation placeholder와 관련 테스트 갱신 지점이 확인된다.
- 검증 기록:
- 2026-06-22: `rg -n "CreatorChannelTab\\.Donation|CreatorChannelPlaceholderFragment|createFragment" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel` 실행. `CreatorChannelPagerAdapter.createFragment()`에서 `Home/Live/Audio/Series/Community/FanTalk`만 실제 Fragment로 연결되고 `Donation``else -> CreatorChannelPlaceholderFragment.newInstance(tab)` 경로를 타는 것을 확인했다. `CreatorChannelPagerAdapterTest`도 현재 실제 Fragment 탭 외 나머지는 placeholder로 유지된다는 기대를 가진다. 향후 교체 위치는 `CreatorChannelPagerAdapter.kt``when (tab)` 분기다.
- [x] **Task 1.2: 기존 후원 dialog와 홈 후원 성공 처리 확인**
- 확인 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeViewModel.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
- 작업:
- `CreatorChannelActivity.onCreatorChannelDonationClicked()`에서 `LiveRoomDonationDialog`를 여는 옵션을 확인한다.
- 홈 탭의 `CreatorChannelHomeViewModel.postChannelDonation()``SharedPreferenceManager.can` 차감 후 홈을 재조회하는지 확인한다.
- 후원 탭에서도 같은 dialog 옵션과 `CreatorChannelRepository.postChannelDonation()`을 재사용하되, 성공 후 후원 탭 첫 페이지를 재조회하도록 설계한다.
- 검증:
- 실행: `rg -n "onCreatorChannelDonationClicked|LiveRoomDonationDialog|postChannelDonation|SharedPreferenceManager.can" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel`
- 기대 결과: 홈 후원 dialog 옵션, repository 위임, can 차감, 홈 재조회 흐름이 확인된다.
- 검증 기록:
- 2026-06-22: `rg -n "onCreatorChannelDonationClicked|LiveRoomDonationDialog|postChannelDonation|SharedPreferenceManager.can" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel` 실행. `CreatorChannelActivity.onCreatorChannelDonationClicked()`가 owner가 아닐 때 `LiveRoomDonationDialog``isLiveDonation = true`, `messageMaxLength = 100`, `secretToggleLabelResId = R.string.screen_user_profile_channel_donation_secret`, `applySecretMissionMessageHint = false` 옵션으로 열고, submit 시 `homeActionDelegate?.postChannelDonation(...)`로 위임하는 것을 확인했다. `CreatorChannelHomeViewModel.postChannelDonation()``CreatorChannelRepository.postChannelDonation()` 호출 성공 시 `SharedPreferenceManager.can = (SharedPreferenceManager.can - can).coerceAtLeast(0)`로 차감하고 `loadHome(content.header.creatorId)`로 홈을 재조회한다.
- [x] **Task 1.3: 기존 후원 전체보기 Activity 호출 계약 확인**
- 확인 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAllViewActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/UserProfileActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelHomeUiModels.kt`
- 작업:
- 기존 `UserProfileDonationAllViewActivity``Constants.EXTRA_USER_ID`를 요구하는지 확인한다.
- 크리에이터 채널에서 보유한 `creatorId`가 기존 Activity 호출에 그대로 사용 가능한지 API/기존 호출 경로를 대조한다.
- 식별자 불일치가 확인되면 구현 전에 PRD/계획 문서를 갱신하고 사용자에게 확인한다.
- 검증:
- 실행: `rg -n "UserProfileDonationAllViewActivity|EXTRA_USER_ID|creatorId|userId" app/src/main/java/kr/co/vividnext/sodalive/explorer/profile app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt`
- 기대 결과: 전체보기 Activity 인자 계약과 크리에이터 채널 보유 식별자가 확인된다.
- 검증 기록:
- 2026-06-22: `rg -n "UserProfileDonationAllViewActivity|EXTRA_USER_ID|creatorId|userId" app/src/main/java/kr/co/vividnext/sodalive/explorer/profile app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel app/src/main/java/kr/co/vividnext/sodalive/common/Constants.kt` 실행. `UserProfileDonationAllViewActivity``intent.getLongExtra(Constants.EXTRA_USER_ID, 0)`를 읽고, 기존 `UserProfileActivity``Intent(..., UserProfileDonationAllViewActivity::class.java)``Constants.EXTRA_USER_ID``userId`를 전달한다. 크리에이터 채널의 `creatorId`도 채널 owner/member 식별자로 사용되고 있어 Phase 6에서 `putExtra(Constants.EXTRA_USER_ID, creatorId)`로 연결 가능한 전제를 확인했다. 추가로 `UserProfileChannelDonationAllViewActivity``Constants.EXTRA_USER_ID`를 받는 채널 후원 목록 전체보기 화면으로 존재함을 확인했다. 현재 계획의 버튼은 `후원 랭킹` 섹션의 `전체보기`이므로 `UserProfileDonationAllViewActivity` 전제는 유지하되, 향후 요구가 후원 내역 전체보기로 바뀌면 `UserProfileChannelDonationAllViewActivity`로 계획을 먼저 갱신해야 한다.
- [x] **Task 1.4: 기존 목록 탭 pagination/viewport 패턴 확인**
- 확인 파일:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModel.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityViewModel.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- 작업:
- 후원 ViewModel도 `FIRST_PAGE = 0`, `DEFAULT_PAGE_SIZE = 20`, `requestGeneration`, `paginationErrorMessage`, `consumePaginationErrorMessage()` 패턴을 따른다.
- 후원 Fragment도 `onCreatorChannelDonationTabSelected()`, `onCreatorChannelDonationScrolledToBottom()`, `onCreatorChannelDonationRefreshRequested()`, `onCreatorChannelDonationViewportHeightChanged()` entry를 제공한다.
- 검증:
- 실행: `rg -n "requestGeneration|paginationErrorMessage|consumePaginationErrorMessage|ScrolledToBottom|RefreshRequested|ViewportHeightChanged" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel`
- 기대 결과: FanTalk/Community의 pagination, refresh, viewport 패턴이 확인된다.
- 검증 기록:
- 2026-06-22: `rg -n "requestGeneration|paginationErrorMessage|consumePaginationErrorMessage|ScrolledToBottom|RefreshRequested|ViewportHeightChanged" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel` 실행. `CreatorChannelFanTalkViewModel``CreatorChannelCommunityViewModel``requestGeneration`, `paginationErrorMessage`, `consumePaginationErrorMessage()` 패턴을 사용하고, `FIRST_PAGE = 0`, `DEFAULT_PAGE_SIZE = 20` 기준으로 첫 페이지와 pagination을 구분하는 것을 확인했다. Fragment entry는 `onCreatorChannelFanTalkScrolledToBottom()`, `onCreatorChannelFanTalkRefreshRequested()`, `onCreatorChannelFanTalkViewportHeightChanged(minHeight)` 및 Community의 scrolled/refresh 패턴을 확인했다. Activity는 현재 Live/Audio/Series/Community/FanTalk에 대해 하단 스크롤 dispatcher와 load-more 탭 판정을 가지고 있으며, Donation 추가 지점은 `notifyCurrentCreatorChannelTabScrolledToBottom()`, `isCreatorChannelLoadMoreTab()`, `updateCreatorChannelTabViewportHeight()`다.
### Phase 2: API/DTO/Repository/ViewModel 계약 추가
- [x] **Task 2.1: 후원 탭 DTO 추가**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/data/CreatorChannelDonationTabResponse.kt`
- 작업:
- `@Keep`, `@SerializedName` 기반으로 `CreatorChannelDonationTabResponse`, `MemberDonationRankingResponse`, `CreatorChannelDonationResponse`를 추가한다.
- PRD의 서버 필드명과 동일하게 `donationCount`, `rankings`, `donations`, `page`, `size`, `hasNext`, `userId`, `nickname`, `profileImage`, `donationCan`, `profileImageUrl`, `can`, `message`, `createdAtUtc`를 정의한다.
- PRD의 `@JsonProperty("hasNext")`는 서버 계약 설명이므로 구현은 프로젝트 기존 Gson 패턴인 `@SerializedName("hasNext")`를 사용한다.
- 검증:
- 실행: `./gradlew :app:compileDebugKotlin`
- 기대 결과: 신규 DTO 추가 후 컴파일이 PASS한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelDonationTabResponse`, `MemberDonationRankingResponse`, `CreatorChannelDonationResponse``@Keep`/`@SerializedName`으로 추가했다. focused GREEN 실행 중 `:app:compileDebugKotlin`이 PASS해 DTO 컴파일을 확인했다.
- [x] **Task 2.2: 후원 탭 endpoint와 Repository method 추가**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
- 작업:
- `@GET("/api/v2/creator-channels/{creatorId}/donations")` endpoint를 추가한다.
- query parameter `page`, `size`만 전달한다.
- Repository method는 `getDonations(creatorId, page, size, token)` 형태로 둔다.
- 기존 `postChannelDonation(creatorId, can, isSecret, message, token)`은 후원 탭 ViewModel에서 재사용한다.
- 검증:
- 실행: `./gradlew :app:compileDebugKotlin`
- 기대 결과: API/Repository 추가 후 기존 Koin graph와 충돌 없이 컴파일된다.
- 검증 기록:
- 2026-06-22: `CreatorChannelApi.getDonations(creatorId, page, size, Authorization)``CreatorChannelRepository.getDonations(creatorId, page, size, token)`를 추가했다. focused GREEN 실행 중 `:app:compileDebugKotlin`이 PASS해 endpoint/repository 컴파일을 확인했다.
- [x] **Task 2.3: 후원 ViewModel RED 테스트 작성**
- 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationViewModelTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationPaginationTest.kt`
- 작업:
- 최초 로드가 `page=0`, `size=20`으로 repository를 호출하는 테스트를 작성한다.
- 성공 응답이 있으면 `Content(donationCount, rankings, donations, page, size, hasNext)` 상태가 되는 테스트를 작성한다.
- `donationCount == 0` 또는 표시 가능한 `donations`가 비어 있으면 `Empty` 상태가 되는 테스트를 작성한다.
- `hasNext == true`에서 `loadMore()``page + 1`을 호출하고 기존 `donations` 뒤에 append하는 테스트를 작성한다.
- loading 중 중복 `loadMore()`를 막는 테스트를 작성한다.
- 후원 성공 시 `SharedPreferenceManager.can`을 차감하고 첫 페이지를 재조회하는 테스트를 작성한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationPaginationTest"`
- 기대 결과: 구현 전에는 신규 타입/메서드 부재로 FAIL한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelDonationViewModelTest`, `CreatorChannelDonationPaginationTest`를 작성한 뒤 focused 테스트를 실행했다. 구현 전 `CreatorChannelDonationViewModel`, `CreatorChannelDonationTabResponse`, `CreatorChannelRepository.getDonations` 등 신규 타입/메서드 부재로 `:app:compileDebugUnitTestKotlin`이 FAIL하여 RED를 확인했다.
- [x] **Task 2.4: 후원 ViewModel 구현**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationViewModel.kt`
- 작업:
- `CreatorChannelDonationUiState.Loading`, `Empty`, `Error`, `Content`를 정의한다.
- `loadDonations(creatorId, isOwner)`, `retryDonations()`, `refreshDonations()`, `loadMore()`, `postChannelDonation(can, isSecret, message)`, `consumePaginationErrorMessage()`, `consumeActionToastMessage()`, `consumeDonationSuccessEvent()`를 제공한다.
- 첫 페이지 성공 후 `donations.isEmpty() || donationCount == 0`이면 `Empty` 상태로 전환한다.
- 후원 성공 시 `(SharedPreferenceManager.can - can).coerceAtLeast(0)`로 로컬 can을 차감하고 첫 페이지를 재조회한다.
- 후원 성공 event는 Fragment가 홈 탭 refresh를 요청할 수 있도록 1회성 상태로 노출한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationPaginationTest"`
- 기대 결과: RED 테스트가 PASS한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelDonationViewModel`에 Loading/Empty/Error/Content, 첫 페이지 조회, retry/refresh, loadMore append, pagination error consume, 후원 성공 후 can 차감/첫 페이지 재조회/성공 event를 구현했다. focused GREEN 명령에서 ViewModel/Pagination 테스트가 PASS했다.
- [x] **Task 2.5: Koin binding 추가**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
- 작업:
- `CreatorChannelDonationViewModel` import와 `viewModel { CreatorChannelDonationViewModel(get(), get()) }` binding을 추가한다.
- 두 번째 dependency는 `UtcRelativeTimeTextFormatter`를 사용한다.
- 검증:
- 실행: `./gradlew :app:compileDebugKotlin`
- 기대 결과: Koin binding 추가 후 컴파일이 PASS한다.
- 검증 기록:
- 2026-06-22: `AppDI``CreatorChannelDonationViewModel(get(), get())` binding을 추가했다. focused GREEN 실행 중 `:app:compileDebugKotlin`이 PASS해 Koin binding 컴파일을 확인했다.
### Phase 3: UI model/mapper/공통 후원 표시 정책
- [x] **Task 3.1: 후원 mapper RED 테스트 작성**
- 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationMapperTest.kt`
- 작업:
- 랭킹 순위가 응답 순서 기준 1부터 부여되는 테스트를 작성한다.
- `rankings`는 서버 최대 8명 보장 전제를 유지하되 mapper가 응답 순서를 유지하는지 테스트한다.
- `createdAtUtc``UtcRelativeTimeTextFormatter`를 통해 표시되는 테스트를 작성한다.
- `message`가 blank이면 기존 fallback `%d캔을 후원하였습니다.` 문구를 사용하는 테스트를 작성한다.
- `can` 범위별 header 색상이 기존 홈 탭 정책과 동일한지 테스트한다.
- 비밀 후원 전용 분기를 만들지 않는다는 source/mapper 계약 테스트를 작성한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationMapperTest"`
- 기대 결과: 구현 전에는 신규 mapper 부재로 FAIL한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelDonationMapperTest`를 작성한 뒤 focused 테스트를 실행했다. 구현 전 donation data/model/mapper 부재로 `:app:compileDebugUnitTestKotlin`이 FAIL하여 RED를 확인했다.
- [x] **Task 3.2: 후원 UI model과 mapper 구현**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/model/CreatorChannelDonationUiModels.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/model/CreatorChannelDonationMappers.kt`
- 작업:
- `CreatorChannelDonationRankingUiModel(rank, userId, nickname, profileImageUrl, donationCan)`을 정의한다.
- `CreatorChannelDonationUiModel(nickname, profileImageUrl, can, message, createdAtText, headerColorResId)`를 정의한다.
- mapper는 `MemberDonationRankingResponse.profileImage``profileImageUrl` UI field로 변환한다.
- mapper는 `CreatorChannelDonationResponse.message.ifBlank { fallback }` 정책을 적용한다.
- header 색상은 기존 홈 탭 `calculateCreatorChannelDonationHeaderColorRes(can)`와 동일한 정책을 재사용하거나 후원 공통 위치로 이동한다.
- 공통 위치로 이동할 경우 기존 홈 탭 import를 함께 갱신하고 동작 변경 없이 테스트를 유지한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationMapperTest"`
- 기대 결과: mapper 테스트가 PASS한다.
- 검증 기록:
- 2026-06-22: 후원 랭킹/후원 목록 UI model과 mapper를 추가했다. 랭킹 rank는 응답 순서 기준 1부터 부여하고, `profileImage``profileImageUrl`로 매핑하며, blank message는 기존 `creator_channel_donation_fallback_message`를 사용한다. header 색상은 홈 helper와 동일한 기준을 donation mapper 내부 함수로 구현했다. focused GREEN 명령에서 mapper 테스트가 PASS했다.
### Phase 4: 후원 탭 content UI 구현
- [x] **Task 4.1: 후원 탭 layout/source RED 테스트 작성**
- 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragmentLayoutTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationActionTest.kt`
- 작업:
- `fragment_creator_channel_donation.xml`에 RecyclerView, error/retry, empty, floating donation button id가 존재해야 한다는 source test를 작성한다.
- `item_creator_channel_donation_ranking.xml``후원 랭킹`, ranking container, `전체보기` button id가 존재해야 한다는 source test를 작성한다.
- `item_creator_channel_donation.xml`에 profile, nickname, createdAt, can badge, message id가 존재해야 한다는 source test를 작성한다.
- 본인 채널에서는 floating button이 숨겨진다는 Fragment/source 계약 테스트를 작성한다.
- Phase 4에서는 `전체보기` 클릭이 `CreatorChannelDonationFragment.Host` callback으로 전달된다는 source 계약 테스트를 작성한다. `UserProfileDonationAllViewActivity``Constants.EXTRA_USER_ID` 연결은 Phase 6에서 검증한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationActionTest"`
- 기대 결과: 구현 전에는 신규 layout/Fragment 부재로 FAIL한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelDonationFragmentLayoutTest`, `CreatorChannelDonationActionTest`를 작성한 뒤 production UI 파일 추가 전 focused 테스트를 실행했다. `fragment_creator_channel_donation` 및 후원 layout/id 미존재로 `:app:compileDebugUnitTestKotlin`이 FAIL하여 RED를 확인했다. Phase 4 범위에 맞춰 전체보기는 Activity 직접 이동이 아니라 `Host.onCreatorChannelDonationRankingAllClicked()` callback 계약으로 검증했다.
- [x] **Task 4.2: 후원 content layout과 adapter 구현**
- 생성:
- `app/src/main/res/layout/fragment_creator_channel_donation.xml`
- `app/src/main/res/layout/item_creator_channel_donation_ranking.xml`
- `app/src/main/res/layout/item_creator_channel_donation.xml`
- 필요 시 `app/src/main/res/layout/item_creator_channel_donation_ranking_member.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/ui/CreatorChannelDonationAdapter.kt`
- 필요 시 `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/ui/CreatorChannelDonationRankingAdapter.kt`
- 작업:
- content 상태에서는 랭킹 섹션, Sort-bar without sort, 후원 내역 목록을 표시한다.
- Sort-bar는 좌측 `전체``donationCount`만 표시하고 정렬 UI를 만들지 않는다.
- 랭킹 섹션은 Figma `290:9097` 기준으로 75dp 원형 프로필, 닉네임, 순위 숫자를 최대 8명 표시한다.
- 랭킹 `전체보기` button은 adapter callback으로 Fragment/Activity에 전달한다.
- 후원 item은 Figma `support` card 구조를 기준으로 profile, nickname, relative time, can badge, message를 표시한다.
- header 색상과 fallback message는 mapper 결과를 사용한다.
- 검증:
- 실행: `./gradlew :app:mergeDebugResources`
- 기대 결과: 신규 layout/drawable/string resource가 병합된다.
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationActionTest"`
- 기대 결과: layout/source 계약 테스트가 PASS한다.
- 검증 기록:
- 2026-06-22: `fragment_creator_channel_donation.xml`, 랭킹/랭킹 member/후원 item layout, 랭킹 전체보기 capsule drawable, `CreatorChannelDonationAdapter`, `CreatorChannelDonationRankingAdapter`를 추가했다. Figma `290:9093`, `290:9097` 기준으로 black background, 52dp count bar, gray_900 랭킹 카드, 75dp 랭킹 프로필 grid, 전체보기 capsule, 후원 card header/profile/can badge/message를 구현했다. `./gradlew :app:mergeDebugResources`와 Phase 4 layout/source 테스트가 PASS했다.
- [x] **Task 4.3: 후원 Fragment 구현**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragment.kt`
- 작업:
- `CreatorChannelDonationFragment.newInstance(creatorId)`를 제공한다.
- `Host` interface에 `isCreatorChannelOwner()`, `onCreatorChannelDonationContentChanged()`, `onCreatorChannelDonationRequested(onSubmit)`, `onCreatorChannelDonationRankingAllClicked()`, `onCreatorChannelDonationCompleted()`를 정의한다.
- `onCreatorChannelDonationTabSelected()`에서 `viewModel.loadDonations(creatorId, isOwner = host.isCreatorChannelOwner())`를 호출한다.
- content 상태에서 adapter에 랭킹/후원 내역을 submit하고 floating button은 `!isOwner`일 때만 표시한다.
- 후원 button 클릭 시 host의 공통 후원 dialog를 열고 submit callback에서 `viewModel.postChannelDonation(can, isSecret, message)`를 호출한다.
- 후원 성공 event를 받으면 `host.onCreatorChannelDonationCompleted()`를 호출해 홈 탭도 갱신 가능하게 한다.
- pagination/action toast consume 패턴은 FanTalk Fragment와 동일하게 구현한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`
- 기대 결과: 후원 탭 단위 테스트가 PASS한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelDonationFragment`를 추가해 `newInstance(creatorId)`, `Host` interface, 탭 최초 로드, refresh/loadMore/viewport entry, content adapter submit, owner floating button 숨김, 후원 요청 submit callback, pagination/action toast consume, 후원 성공 event callback을 구현했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`가 PASS했다.
### Phase 5: Empty/Error/문자열 리소스 구현
- [x] **Task 5.1: empty/error 문자열과 다국어 리소스 추가**
- 수정:
- `app/src/main/res/values/strings.xml`
- `app/src/main/res/values-en/strings.xml`
- `app/src/main/res/values-ja/strings.xml`
- 작업:
- `creator_channel_donation_empty_message`: `아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!`
- `creator_channel_donation_action`: `후원하기`
- `creator_channel_donation_all_label`: `전체`
- `creator_channel_donation_ranking_title`: `후원 랭킹`
- `creator_channel_donation_ranking_all`: `전체보기`
- `creator_channel_donation_error_message`, `creator_channel_donation_retry`는 기존 FanTalk/Community 문구 패턴에 맞춰 추가하거나 기존 공통 문구를 재사용한다.
- 검증:
- 실행: `./gradlew :app:mergeDebugResources`
- 기대 결과: values/en/ja 문자열 병합이 PASS한다.
- 검증 기록:
- 2026-06-22: 기존 `creator_channel_donation_*` 문자열이 `values`, `values-en`, `values-ja`에 이미 추가되어 있음을 확인했다. Figma `290:9008` 및 계획 문구에 맞춰 한국어 `creator_channel_donation_empty_title``아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!`로 갱신했다. `./gradlew :app:mergeDebugResources`가 PASS해 문자열 병합을 확인했다.
- [x] **Task 5.2: Figma `290:9008` empty 상태 구현**
- 수정:
- `app/src/main/res/layout/fragment_creator_channel_donation.xml`
- 필요 시 `app/src/main/res/drawable/bg_creator_channel_donation_empty_button.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/donation/CreatorChannelDonationFragment.kt`
- 작업:
- empty 상태에서는 RecyclerView, Sort-bar, floating button을 숨긴다.
- empty 문구는 16sp regular, `gray/500`, center 정렬로 표시한다.
- 타인 채널에서는 문구 아래 `후원하기` capsule button을 표시한다.
- 본인 채널에서는 empty `후원하기` button과 floating button을 모두 숨긴다.
- empty `후원하기` button 터치 시 content 상태 floating button과 동일한 host 후원 dialog를 호출한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest"`
- 기대 결과: empty 상태 visibility/source 계약이 PASS한다.
- 수동 확인: Figma `290:9008`과 empty 문구 위치, button 색상/icon/text, owner 버튼 숨김을 대조한다.
- 검증 기록:
- 2026-06-22: RED로 `CreatorChannelDonationFragmentLayoutTest`에 empty 중앙 `btn_creator_channel_donation_empty_write`/icon/text id 및 Figma capsule source 계약을 추가하고, `CreatorChannelDonationActionTest`에 empty 중앙 button이 타인 채널에서만 표시되고 기존 후원 요청 callback을 재사용해야 한다는 source 계약을 추가했다. 구현 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationActionTest"`가 신규 id 미존재(`btn_creator_channel_donation_empty_write`, `iv_creator_channel_donation_empty_write`, `tv_creator_channel_donation_empty_write`)로 FAIL해 RED를 확인했다.
- 2026-06-22: `fragment_creator_channel_donation.xml`에 Figma `290:9008` 기준 중앙 capsule 후원하기 button을 추가하고, `CreatorChannelDonationFragment`에서 empty 상태일 때 `!state.isOwner`인 경우만 중앙 button을 표시하도록 연결했다. empty 중앙 button 클릭은 content floating button과 같은 `host.onCreatorChannelDonationRequested { can, isSecret, message -> viewModel.postChannelDonation(...) }` 경로를 사용한다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationActionTest"`가 PASS했다. 수동 대조는 Figma `290:9008` screenshot을 확인해 16sp gray/500 중앙 문구, 14dp 간격, soda capsule button 구조를 반영했다.
- 2026-06-22: Phase 5/6 코드 리뷰 중 empty 중앙 `후원하기` button 배경이 `@color/white`로 되어 있어 PRD의 `soda/400` 배경 및 흰색 icon/text 요구와 맞지 않는 문제를 확인했다. RED로 `CreatorChannelDonationFragmentLayoutTest``bg_creator_channel_donation_empty_button.xml``@color/soda_400` 계약을 추가했고, 기존 구현에서 실패하는 것을 확인한 뒤 배경을 `@color/soda_400`으로 수정했다.
- 2026-06-22: Phase 5/6 코드 리뷰 중 empty 상태 중앙 `후원하기` button으로 후원 API 실패 시 `CreatorChannelDonationViewModel.setActionToastMessage()``Content` 상태에서만 메시지를 보관해 실패 toast가 사라지는 문제를 확인했다. RED로 `CreatorChannelDonationViewModelTest``Empty 상태에서 채널 후원 실패는 action toast message를 emit하고 consume한다`를 추가했고, 기존 구현에서 `Empty.actionToastMessage` 부재로 FAIL하는 것을 확인했다. 이후 `Empty` 상태에도 `actionToastMessage`를 추가하고 `CreatorChannelDonationFragment.observeViewModel()`의 공통 경로에서 empty/content action toast를 표시하도록 수정했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest"`가 PASS했다.
### Phase 6: Activity 연결, 탭 연결, 전체보기/후원 액션 연결
- [x] **Task 6.1: PagerAdapter와 Activity source RED 테스트 작성**
- 수정:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- 작업:
- `CreatorChannelTab.Donation``CreatorChannelDonationFragment`를 생성해야 한다는 테스트를 추가한다.
- Activity가 `CreatorChannelDonationFragment.Host`를 구현해야 한다는 source test를 추가한다.
- Activity의 하단 스크롤 dispatcher와 load-more 탭 판정에 Donation이 포함되어야 한다는 source test를 추가한다.
- Activity가 기존 `LiveRoomDonationDialog` 표시 helper를 홈/후원에서 재사용해야 한다는 source test를 추가한다.
- Activity가 `UserProfileDonationAllViewActivity``Constants.EXTRA_USER_ID`를 사용해야 한다는 source test를 추가한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- 기대 결과: 구현 전에는 Donation 연결 부재로 FAIL한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelPagerAdapterTest``CreatorChannelTab.Donation``CreatorChannelDonationFragment`를 생성해야 한다는 assertion을 추가하고, placeholder 유지 대상에서 Donation을 제외했다. `CreatorChannelActivitySourceTest`에는 `CreatorChannelDonationFragment.Host`, `findDonationFragment()`, 탭 선택/헤더 변경 시 load, scroll bottom dispatcher, load-more 판정, viewport 전달, 공통 `showCreatorChannelDonationDialog()` helper, 후원 완료 홈 refresh hook, `UserProfileDonationAllViewActivity` + `Constants.EXTRA_USER_ID` 이동 계약을 추가했다. 구현 전 focused RED 실행에서 Phase 5 신규 id 미존재로 unit test compile이 먼저 FAIL했고, 같은 RED 테스트 묶음에서 Donation 연결 production 코드 부재 상태를 고정했다.
- [x] **Task 6.2: PagerAdapter Donation 연결**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
- 작업:
- `CreatorChannelTab.Donation -> CreatorChannelDonationFragment.newInstance(creatorId)` 분기를 추가한다.
- placeholder fallback은 아직 구현되지 않은 탭에만 남긴다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"`
- 기대 결과: PagerAdapter 테스트가 PASS한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelPagerAdapter``CreatorChannelDonationFragment` import와 `CreatorChannelTab.Donation -> CreatorChannelDonationFragment.newInstance(creatorId)` 분기를 추가했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 재실행에서 PagerAdapter Donation 생성 계약이 PASS했다.
- [x] **Task 6.3: Activity Host와 scroll/viewport 연결**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- 작업:
- `CreatorChannelDonationFragment.Host`를 구현한다.
- `findDonationFragment()`를 추가한다.
- `notifyCurrentCreatorChannelTabScrolledToBottom()`에 Donation 분기를 추가한다.
- `isCreatorChannelLoadMoreTab()`에 Donation을 추가한다.
- 탭 선택 callback에서 Donation 선택 시 `onCreatorChannelDonationTabSelected()`가 호출되도록 연결한다.
- `updateCreatorChannelTabViewportHeight()`에 Donation viewport height 전달을 추가한다.
- `onCreatorChannelDonationContentChanged()`에서 `updateViewPagerHeight()`를 호출한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- 기대 결과: Activity source 계약이 PASS한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelActivity``CreatorChannelDonationFragment.Host`를 구현하도록 추가하고, `findDonationFragment()`, 탭 선택/헤더 변경 시 `onCreatorChannelDonationTabSelected()`, `notifyCurrentCreatorChannelTabScrolledToBottom()`의 Donation 분기, `isCreatorChannelLoadMoreTab()` Donation 포함, `updateCreatorChannelTabViewportHeight()` Donation viewport 전달, `onCreatorChannelDonationContentChanged()``updateViewPagerHeight { postCheckCreatorChannelCurrentTabNeedsMore() }` 연결을 구현했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`가 PASS했다.
- [x] **Task 6.4: Activity 후원 dialog 공통 helper와 후원 완료 hook 연결**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelHomeFragment.kt` 필요 시 interface 유지 확인
- 작업:
- 기존 `onCreatorChannelDonationClicked()` 내부 dialog 생성 코드를 private helper로 분리한다.
- helper signature는 `showCreatorChannelDonationDialog(onSubmit: (can: Int, isSecret: Boolean, message: String) -> Unit)`처럼 후원 submit callback을 받을 수 있게 둔다.
- 기존 홈 탭 호출은 helper에 `homeActionDelegate?.postChannelDonation(...)`를 전달해 동작을 유지한다.
- 후원 탭 Host 구현은 helper에 `CreatorChannelDonationFragment`의 ViewModel submit callback을 전달한다.
- `onCreatorChannelDonationCompleted()`에서는 `homeActionDelegate?.refreshHome()`를 호출해 홈 탭 후원 요약도 최신화한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- 기대 결과: 홈 후원 액션 유지와 후원 탭 후원 dialog 재사용 source 계약이 PASS한다.
- 검증 기록:
- 2026-06-22: 기존 `onCreatorChannelDonationClicked()``LiveRoomDonationDialog` 생성 코드를 `showCreatorChannelDonationDialog(onSubmit)` helper로 분리했다. 홈 탭 후원은 helper submit에서 기존 `homeActionDelegate?.postChannelDonation(can = can, isSecret = isSecret, message = message)`를 호출해 동작을 유지하고, 후원 탭 Host의 `onCreatorChannelDonationRequested()`는 같은 helper에 Fragment ViewModel submit callback을 전달하도록 연결했다. `onCreatorChannelDonationCompleted()`에서는 `homeActionDelegate?.refreshHome()`를 호출한다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`가 PASS했다.
- [x] **Task 6.5: 랭킹 전체보기 이동 연결**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- 작업:
- `onCreatorChannelDonationRankingAllClicked()`에서 `Intent(this, UserProfileDonationAllViewActivity::class.java)`를 생성한다.
- `putExtra(Constants.EXTRA_USER_ID, creatorId)`로 기존 Activity 호출 계약을 맞춘다.
- Task 1.3에서 식별자 불일치가 확인된 경우 이 Task를 진행하지 않고 문서를 먼저 갱신한다.
- 검증:
- 실행: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationActionTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- 기대 결과: 전체보기 이동 source 계약이 PASS한다.
- 검증 기록:
- 2026-06-22: `onCreatorChannelDonationRankingAllClicked()`에서 `Intent(this, UserProfileDonationAllViewActivity::class.java)`를 생성하고 `putExtra(Constants.EXTRA_USER_ID, creatorId)`로 기존 전체보기 Activity 호출 계약에 맞춰 연결했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationActionTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 범위는 Phase 5/6 focused GREEN 명령들로 분리 검증했으며 모두 PASS했다.
### Phase 7: 통합 검증과 수동 확인
- [ ] **Task 7.1: 후원 탭 focused 테스트 실행**
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`
- 기대 결과:
- 후원 탭 mapper/ViewModel/pagination/layout/action 테스트가 모두 PASS한다.
- 검증 기록:
- 미실행. 구현 시 기록한다.
- [ ] **Task 7.2: 크리에이터 채널 회귀 테스트 실행**
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Donation*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`
- 기대 결과:
- 기존 Home/Live/Audio/Series/Community/FanTalk 탭 테스트와 신규 Donation 탭 테스트가 PASS한다.
- 검증 기록:
- 미실행. 구현 시 기록한다.
- [ ] **Task 7.3: 리소스/컴파일/스타일 검증**
- 실행:
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:compileDebugKotlin`
- `./gradlew :app:ktlintCheck`
- `git diff --check`
- 기대 결과:
- resource merge, Kotlin compile, ktlint, whitespace check가 모두 PASS한다.
- 검증 기록:
- 미실행. 구현 시 기록한다.
- [ ] **Task 7.4: 수동 UI 확인**
- 확인 항목:
- 후원 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/donations?page=0&size=20`이 호출된다.
- Sort-bar는 `전체``donationCount`만 표시하고 정렬 UI가 없다.
- 랭킹 섹션은 Figma `290:9097` 기준으로 최대 8명, 순위 1부터 표시한다.
- `전체보기` 버튼 터치 시 `UserProfileDonationAllViewActivity`로 이동한다.
- 후원 내역 item은 Figma `290:9093` 기준으로 profile, nickname, relative time, can badge, message를 표시한다.
- 목록 하단 스크롤 시 `hasNext == true`이면 다음 페이지가 append된다.
- 타인 채널 content 상태에서는 floating 후원하기 버튼이 표시되고, 후원 성공 후 첫 페이지가 재조회된다.
- 타인 채널 empty 상태에서는 Figma `290:9008` 기준 문구와 중앙 `후원하기` button이 표시된다.
- 본인 채널에서는 empty 중앙 `후원하기` button과 floating 후원하기 버튼이 모두 숨겨진다.
- 비밀 후원 전용 UI/표시 분기는 없다.
- 검증 기록:
- 미실행. 구현 시 기록한다.
---
## Verification Log
- 2026-06-22: 후원 랭킹 item 순위 text 하단이 보이지 않는 원인을 재확인했다. rank `TextView`는 profile `1:1` 컨테이너 안에서 `0.63` 지점부터 아래로 그려져야 하는데, profile 컨테이너와 root item의 child drawing이 bounds 안으로 clipping되어 컨테이너 밖으로 내려오는 하단 영역이 보이지 않는 구조였다. 따라서 앞선 padding 접근은 제거하고, root와 profile 컨테이너에 `android:clipChildren="false"`를 적용해 rank text가 profile 하단 밖으로 overflow되어도 보이도록 수정했다. 순위 text 상단 위치는 사용자 확인 기준대로 `0.63` guideline에 맞췄다. RED로 `CreatorChannelDonationFragmentLayoutTest`가 기존 XML에서 FAIL하는 것을 확인했고, 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:mergeDebugResources`, `git diff --check`가 PASS했다. `:app:mergeDebugResources`는 sandbox에서 Gradle wrapper lock 파일 접근 권한 문제로 1회 실패했고, 외부 권한으로 동일 명령을 재실행해 PASS를 확인했다.
- 2026-06-22: 사용자 추가 확인에 따라 후원 랭킹 item 순위 text 하단 clipping과 위치를 재조정한다. 기존 `0.68` guideline은 눈으로 봤을 때 Figma보다 낮게 보이므로 `0.63`으로 올리고, `@font/pattaya_regular` + shadow가 `includeFontPadding=false` 상태에서 하단 여유 없이 그려져 잘릴 수 있어 rank text에 bottom padding을 추가한다. 구현 전 RED로 `CreatorChannelDonationFragmentLayoutTest`가 기존 XML에서 FAIL하는 것을 확인했다.
- 2026-06-22: 후원 랭킹 item 순위 표시 위치를 Figma `290:9097` 기준으로 수정했다. `get_design_context`와 screenshot에서 `75dp` profile 기준 rank text `top=51dp`를 확인했고, profile 높이의 약 `68%` 지점을 기준으로 rank text가 시작되도록 profile 영역을 `1:1` nested `ConstraintLayout`으로 분리한 뒤 horizontal guideline `0.68`에 rank top을 연결했다. 기존 bottom constraint + `translationY` 방식은 제거했다. RED로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest"`가 기존 XML에서 FAIL하는 것을 확인했고, 수정 후 동일 focused 테스트가 PASS했다. 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Donation*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다. `:app:mergeDebugResources`는 sandbox에서 Gradle wrapper lock 파일 접근 권한 문제로 1회 실패했고, 외부 권한으로 동일 명령을 재실행해 PASS를 확인했다.
- 2026-06-22: 사용자 추가 확인에 따라 후원 랭킹 item 순위 표시 위치를 Figma `290:9097` 기준으로 재검토했다. Figma에서 순위 text는 `75dp` profile 기준 `top=51dp` 지점에 시작하므로 profile 높이의 약 `68%` 지점을 기준으로 overlay되도록 후속 수정한다. 구현 전 RED 테스트로 기존 bottom constraint + `translationY` 방식이 Figma 위치 계약과 다름을 고정한다.
- 2026-06-22: 사용자 피드백에 따라 후원 랭킹 섹션과 우측 하단 채널 후원 floating button을 Figma 기준으로 후속 수정했다. Figma `290:9097`, `290:9093` design context/screenshot을 재확인했고, 랭킹 grid는 고정 `75dp`가 아니라 가용 폭에서 `14dp` 간격 3개를 제외한 뒤 4등분되도록 `GridLayoutManager` + `CreatorChannelDonationRankingItemDecoration`으로 수정했다. 랭킹 member profile은 column 폭에 맞춰 `1:1` 비율로 측정되도록 바꾸고, 순위 표시는 `@font/pattaya_regular`, `28sp`, profile 하단 overlay 기준으로 조정했다. 우측 하단 후원 floating button은 `fragment_creator_channel_donation.xml`에서 제거하고 `activity_creator_channel.xml` overlay로 옮겨 `66dp` 크기, `14dp` end/bottom margin, `38dp` icon, Activity 우하단 고정 위치로 표시되게 했다. `CreatorChannelDonationFragment`는 content/owner 상태에 따라 Activity에 floating button visibility를 전달하고, Activity button click은 현재 Donation Fragment의 `onCreatorChannelDonationFloatingButtonClicked()`를 호출한다.
- 2026-06-22: 후속 수정 RED로 `CreatorChannelDonationFragmentLayoutTest`에 responsive ranking grid/item 계약을 추가하고, `CreatorChannelActivitySourceTest`에 Activity overlay floating button 계약을 추가했다. 기존 구현에서 랭킹 grid 폭/간격/순위 폰트 및 Fragment 내부 floating button 구조 때문에 FAIL하는 것을 확인한 뒤 수정했다. 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Donation*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다. `:app:mergeDebugResources`는 sandbox에서 Gradle wrapper lock 파일 접근 권한 문제로 1회 실패했고, 외부 권한으로 동일 명령을 재실행해 PASS를 확인했다. 참고로 `donation.*``*Donation*` 테스트 필터를 한 Gradle 명령에 합쳐 실행하면 `SharedPreferenceManager.can` 전역 상태를 사용하는 테스트들이 겹쳐 기존 ViewModel 테스트가 흔들릴 수 있어, 계획 문서의 명령처럼 분리 실행했다.
- 2026-06-22: 사용자 확인 사항에 따라 후속 수정 범위를 추가했다. 후원 랭킹 섹션은 Figma `290:9097` 기준으로 grid gap `14dp`, 4열 responsive profile cell, rank overlay `28sp`/profile 하단 위치를 맞춘다. `402dp` 화면에서는 profile cell이 `75dp`가 되지만 고정값으로 두지 않고 가용 폭에 따라 계산되도록 한다. 우측 하단 채널 후원 floating button은 Figma `290:9093` 및 홈 탭 owner FAB 패턴처럼 `66dp` 크기, `14dp` end/bottom margin, `38dp` icon, Activity overlay 고정 위치로 옮기고, 후원 탭 content/empty 상태와 owner 여부에 따라 표시만 제어한다. 구현 전 RED 테스트로 layout/source 계약을 먼저 고정한다.
- 2026-06-22: Phase 5, 6 코드 리뷰 및 검증을 수행했다. 리뷰에서 empty 중앙 `후원하기` button 배경색이 `@color/white``soda/400` 요구와 맞지 않는 문제, empty 상태 후원 실패 메시지가 `Content` 전용 action toast 상태에 막혀 사용자에게 표시되지 않을 수 있는 문제를 발견해 RED 테스트 후 수정했다. 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationFragmentLayoutTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.CreatorChannelDonationViewModelTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Donation*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다. `:app:mergeDebugResources`는 sandbox에서 Gradle wrapper lock 파일 접근 권한 문제로 1회 실패했고, 외부 권한으로 동일 명령을 재실행해 PASS를 확인했다.
- 2026-06-22: Phase 2, 3, 4 코드 리뷰 및 재검증을 수행했다. API/DTO/Repository/ViewModel 계약, mapper 정책, content layout/adapter/Fragment 흐름을 대조했으며 차단급 코드 이슈는 발견하지 않았다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Donation*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다. `:app:mergeDebugResources`는 sandbox에서 Gradle wrapper lock 파일 접근 권한 문제로 1회 실패했고, 외부 권한으로 동일 명령을 재실행해 PASS를 확인했다.
- 2026-06-22: 리뷰 지적에 따라 후원 성공 event가 `Content` 상태에 종속되지 않도록 후속 수정했다. RED로 `CreatorChannelDonationActionTest``후원 fragment source는 성공 이벤트를 content 상태와 독립적으로 전달한다`를 추가했고, 기존 구현에서 `handleDonationSuccessEvent()` 부재로 FAIL하는 것을 확인했다. 이후 `CreatorChannelDonationFragment.observeViewModel()`의 상태 bind 후 공통 경로에서 `handleDonationSuccessEvent()`를 호출하도록 변경해 `Empty`, `Error`, `Content` 재조회 결과 모두 `Host.onCreatorChannelDonationCompleted()` 전달 대상이 되도록 했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다.
- 2026-06-22: `docs/20260622_크리에이터_채널_후원_탭/prd.md`, `docs/agent-guides/work-plan-docs.md`, 기존 `docs/20260622_FanTalk_탭/plan-task.md` 구조를 확인해 후원 탭 구현 계획/TASK 문서를 작성했다. 이번 단계는 문서 작성만 수행했으며 구현/빌드/테스트는 실행하지 않았다.
- 2026-06-22: Phase 2, 3, 4 코드 리뷰 및 검증을 수행했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.donation.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`가 PASS했다. 최초 `mergeDebugResources` 병렬 실행은 Gradle wrapper lock 파일 접근 권한 문제로 실패했으나 동일 명령을 단독 재실행해 PASS를 확인했다. 리뷰 결과, 후원 성공 event가 `Content` 상태 bind 중에만 `Host.onCreatorChannelDonationCompleted()`로 전달되어 후원 성공 후 첫 페이지 재조회가 `Empty` 또는 `Error`로 끝나는 경우 홈 탭 refresh hook이 호출되지 않을 수 있는 위험을 확인했다.