From 3a94878020da1dc7ce15f0684c3fc9c9c8878e1b Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 22 Jun 2026 18:38:00 +0900 Subject: [PATCH] =?UTF-8?q?docs(creator):=20=ED=81=AC=EB=A6=AC=EC=97=90?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=B1=84=EB=84=90=20=ED=9B=84=EC=9B=90=20?= =?UTF-8?q?=ED=83=AD=20=EB=AC=B8=EC=84=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 521 ++++++++++++++++++ docs/20260622_크리에이터_채널_후원_탭/prd.md | 298 ++++++++++ 2 files changed, 819 insertions(+) create mode 100644 docs/20260622_크리에이터_채널_후원_탭/plan-task.md create mode 100644 docs/20260622_크리에이터_채널_후원_탭/prd.md diff --git a/docs/20260622_크리에이터_채널_후원_탭/plan-task.md b/docs/20260622_크리에이터_채널_후원_탭/plan-task.md new file mode 100644 index 00000000..07c5884c --- /dev/null +++ b/docs/20260622_크리에이터_채널_후원_탭/plan-task.md @@ -0,0 +1,521 @@ +# 크리에이터 채널 후원 탭 구현 계획/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: 기존 구조 확인과 작업 경계 고정 + +- [ ] **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와 관련 테스트 갱신 지점이 확인된다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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 차감, 홈 재조회 흐름이 확인된다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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 인자 계약과 크리에이터 채널 보유 식별자가 확인된다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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 패턴이 확인된다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +### Phase 2: API/DTO/Repository/ViewModel 계약 추가 + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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와 충돌 없이 컴파일된다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 RED 결과를 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +### Phase 3: UI model/mapper/공통 후원 표시 정책 + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 RED 결과를 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +### Phase 4: 후원 탭 content UI 구현 + +- [ ] **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 계약 테스트를 작성한다. + - `전체보기` 클릭이 `UserProfileDonationAllViewActivity`와 `Constants.EXTRA_USER_ID`를 사용한다는 source 계약 테스트를 작성한다. + - 검증: + - 실행: `./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한다. + - 검증 기록: + - 미실행. 구현 시 RED 결과를 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +### Phase 5: Empty/Error/문자열 리소스 구현 + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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 버튼 숨김을 대조한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +### Phase 6: Activity 연결, 탭 연결, 전체보기/후원 액션 연결 + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 RED 결과를 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +- [ ] **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한다. + - 검증 기록: + - 미실행. 구현 시 기록한다. + +### 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: `docs/20260622_크리에이터_채널_후원_탭/prd.md`, `docs/agent-guides/work-plan-docs.md`, 기존 `docs/20260622_FanTalk_탭/plan-task.md` 구조를 확인해 후원 탭 구현 계획/TASK 문서를 작성했다. 이번 단계는 문서 작성만 수행했으며 구현/빌드/테스트는 실행하지 않았다. diff --git a/docs/20260622_크리에이터_채널_후원_탭/prd.md b/docs/20260622_크리에이터_채널_후원_탭/prd.md new file mode 100644 index 00000000..e28b20be --- /dev/null +++ b/docs/20260622_크리에이터_채널_후원_탭/prd.md @@ -0,0 +1,298 @@ +# PRD: 크리에이터 채널 후원 탭 + +## 1. Overview +크리에이터 채널의 `후원` 탭에서 후원 랭킹, 전체 후원 개수, 후원 내역 목록을 조회하고, 우측 하단 후원하기 버튼으로 홈 탭과 동일한 채널 후원 플로우를 제공한다. + +--- + +## 2. Problem +- 크리에이터 채널에는 `후원` 탭이 존재하지만 현재 상세 탭 구현은 placeholder 상태다. +- 사용자는 크리에이터 채널 안에서 누가 많이 후원했는지와 최근 후원 내역을 한 화면에서 확인할 수 있어야 한다. +- 후원 내역은 길어질 수 있으므로 `hasNext == true`일 때 다음 페이지를 이어서 조회해야 한다. +- 후원 탭에서 후원하기를 완료하면 방금 반영된 후원 내역과 랭킹이 화면에 갱신되어야 한다. +- 홈 탭에 이미 연결된 채널 후원 플로우와 동일한 UX/API 동작을 재사용해야 한다. + +--- + +## 3. Goals +- Figma 전체 화면 `290:9093` 기준으로 크리에이터 채널 `후원` 탭 UI 요구사항을 정의한다. +- Figma 후원 랭킹 섹션 `290:9097` 기준으로 랭킹 카드 요구사항을 정의한다. +- API endpoint `GET /api/v2/creator-channels/{creatorId}/donations`를 기준으로 최초 조회와 pagination 요구사항을 정의한다. +- 최초 조회 query parameter 기본값은 `page=0`, `size=20`으로 둔다. +- Sort-bar에는 정렬 UI를 표시하지 않고 `전체` label과 `donationCount`만 표시한다. +- 후원 랭킹 섹션에는 API `rankings`를 응답 순서 기준으로 최대 8명까지 표시한다. +- 후원 내역 item에는 후원자 프로필 이미지, 닉네임, 작성 시간, 후원 캔 수량, 메시지를 표시한다. +- `createdAtUtc`는 크리에이터 채널 v2 공통 상대 시간 포맷을 따른다. +- 후원 내역 item의 header 색상과 캔 수량 badge는 기존 홈 탭 후원 카드 정책을 우선 재사용한다. +- 랭킹 섹션의 `전체보기` 버튼은 기존 `UserProfileDonationAllViewActivity`로 이동한다. +- 우측 하단 floating 후원하기 버튼은 홈 탭 후원하기 버튼 터치 액션과 동일하게 `LiveRoomDonationDialog` 기반 채널 후원 플로우를 호출한다. +- 후원 성공 후에는 후원 탭의 첫 페이지를 다시 조회해 `donationCount`, `rankings`, `donations`가 갱신되어야 한다. +- 응답의 `hasNext`가 `true`이면 현재 `page + 1` 페이지를 추가 로딩한다. + +--- + +## 4. Non-Goals +- 크리에이터 채널 상단 header, title bar, 공통 tab-bar 구조 자체를 재설계하지 않는다. +- `홈`, `라이브`, `오디오`, `시리즈`, `커뮤니티`, `팬Talk` 탭의 동작은 이번 범위에서 변경하지 않는다. +- 후원 결제/충전 dialog 내부 UX, validation, 충전 화면 진입 동작은 변경하지 않고 홈 탭 후원하기 액션을 재사용한다. +- 후원 랭킹 전체보기용 신규 화면, bottom sheet, 랭킹 확장 UI는 만들지 않는다. +- 후원 내역 item에서 프로필 이동, 신고, 삭제, 차단 등 추가 액션은 이번 범위에서 제외한다. +- 후원 내역의 비밀 후원 전용 표시 정책은 이번 범위에서 제외한다. 현재 API가 비밀 후원 여부를 별도 필드로 내려주지 않으므로 클라이언트에서 비밀 후원 UI를 분기하지 않는다. +- API schema를 임의 변경하거나 서버 응답 필드명을 클라이언트에서 새로 정의하지 않는다. +- Sort-bar에 정렬 label, 정렬 icon, 정렬 popup을 추가하지 않는다. +- Figma asset을 localhost URL 그대로 앱 코드에 직접 의존하지 않는다. +- 레거시 후원 구현 파일을 직접 수정하지 않는다. 필요한 경우 기존 후원 dialog/repository 흐름을 호출하거나 신규 wrapper/adapter를 추가해 사용한다. + +--- + +## 5. Target Users +- 크리에이터 채널에서 후원 랭킹과 후원 내역을 확인하려는 앱 사용자. +- 특정 크리에이터에게 후원하고 후원 결과가 화면에 반영되길 기대하는 앱 사용자. +- 본인 채널의 후원 현황을 확인하려는 크리에이터. +- `kr.co.vividnext.sodalive.v2` 하위 크리에이터 채널 탭을 구현/유지보수하는 Android 개발자. + +--- + +## 6. User Stories +- 사용자는 크리에이터 채널의 `후원` 탭에서 전체 후원 수를 확인하고 싶다. +- 사용자는 후원 랭킹 상위 사용자의 프로필, 닉네임, 순위를 빠르게 확인하고 싶다. +- 사용자는 각 후원 내역의 후원자, 후원 시간, 후원 캔 수량, 메시지를 확인하고 싶다. +- 사용자는 목록 하단까지 스크롤하면 다음 페이지가 자연스럽게 이어서 로딩되길 기대한다. +- 사용자는 후원 탭 우측 하단 버튼으로 바로 후원하고 싶다. +- 사용자는 후원 완료 직후 목록과 랭킹이 최신 상태로 갱신되길 기대한다. + +--- + +## 7. Core Features + +### Creator Channel Donation Tab API +`후원` 탭 진입과 추가 로딩 시 크리에이터별 후원 랭킹과 후원 내역 데이터를 조회한다. + +#### Requirements +- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/donations`이다. +- `creatorId`는 path variable로 전달한다. +- Query parameters는 `page`, `size`를 사용한다. +- 최초 조회 기본값은 `page=0`, `size=20`이다. +- 정렬 query parameter는 사용하지 않는다. +- `hasNext == true`일 때 다음 페이지 요청은 현재 응답의 `page + 1` 값을 사용한다. +- 중복 pagination 요청이 발생하지 않도록 loading 중 추가 요청을 막아야 한다. +- 다음 페이지 성공 시 기존 `donations` 뒤에 append한다. +- 후원 성공 후 갱신은 append가 아니라 `page=0`, `size=20` 최초 조회를 다시 수행한다. + +#### Response Contract +```kotlin +data class CreatorChannelDonationTabResponse( + val donationCount: Int, + val rankings: List, + val donations: List, + val page: Int, + val size: Int, + @JsonProperty("hasNext") + val hasNext: Boolean +) + +data class MemberDonationRankingResponse( + @JsonProperty("userId") val userId: Long, + @JsonProperty("nickname") val nickname: String, + @JsonProperty("profileImage") val profileImage: String, + @JsonProperty("donationCan") val donationCan: Int +) + +data class CreatorChannelDonationResponse( + val nickname: String, + val profileImageUrl: String, + val can: Int, + val message: String, + val createdAtUtc: String +) +``` + +#### Edge Cases +- `creatorId <= 0`이면 API를 호출하지 않고 기존 크리에이터 채널 탭의 종료/placeholder 정책을 따른다. +- 최초 조회 실패 시 기존 크리에이터 채널 탭의 error/retry 패턴을 따른다. +- 다음 페이지 로딩 실패 시 기존 목록은 유지하고 기존 pagination 실패 표시 정책을 따른다. +- 다음 페이지 응답의 `donations`가 비어 있어도 `hasNext` 값 기준으로 이후 로딩 가능 여부를 갱신한다. +- 서버 응답의 `page`, `size`가 요청 상태와 다를 경우 서버 응답 값을 기준으로 ViewModel page 상태를 동기화한다. +- `profileImage` 또는 `profileImageUrl`이 비어 있거나 이미지 로딩에 실패하면 기존 프로필 이미지 placeholder 정책을 따른다. +- `message`가 빈 문자열이면 기존 홈 탭 후원 카드와 동일하게 `%1$d캔을 후원하였습니다.` 형식의 fallback 문구를 표시한다. + +### Donation Ranking Section +후원 랭킹 섹션은 Figma `290:9097` 기준의 카드 UI로 상위 후원자를 표시한다. + +#### Requirements +- 섹션 title은 `후원 랭킹`이다. +- 카드 배경은 `gray/900`, radius 14dp, 내부 padding 14dp를 기준으로 한다. +- 랭킹 영역은 75dp 원형 프로필, 닉네임, 순위 숫자를 하나의 profile cell로 표시한다. +- profile cell은 Figma처럼 4열 x 2행 기준으로 최대 8개까지 표시한다. +- 순위 숫자는 응답 순서 기준으로 1부터 부여한다. +- 닉네임은 한 줄로 표시하고 긴 경우 말줄임 처리한다. +- 프로필 이미지는 `MemberDonationRankingResponse.profileImage`를 사용한다. +- `donationCan`은 현재 Figma 랭킹 cell에 직접 노출되지 않으므로 이번 범위에서는 UI model에 보관하되 화면에는 표시하지 않는다. +- 랭킹 섹션 하단에는 Figma 기준 `전체보기` capsule button을 표시한다. +- 서버는 `rankings`를 항상 최대 8명까지만 내려준다. +- `전체보기` 버튼을 터치하면 기존 `UserProfileDonationAllViewActivity`로 이동한다. +- `UserProfileDonationAllViewActivity`는 기존 호출 계약처럼 `Constants.EXTRA_USER_ID`를 전달해 실행한다. + +#### Edge Cases +- `rankings`가 비어 있으면 랭킹 grid는 표시하지 않는다. +- `rankings`가 1~7개이면 내려온 개수만 표시하고 빈 cell을 만들지 않는다. +- `profileImage`가 비어 있거나 이미지 로딩에 실패하면 기존 프로필 이미지 placeholder를 표시한다. +- 동일 `userId`가 중복으로 내려오면 API 응답 순서를 유지해 그대로 표시한다. +- `UserProfileDonationAllViewActivity` 진입에 필요한 id와 크리에이터 채널의 `creatorId`가 다른 식별자인 경우, 구현 계획에서 기존 Activity 호출 계약에 맞는 식별자 확보 방식을 먼저 확정한다. + +### Sort Bar without Sort +Sort-bar는 전체 후원 수만 표시한다. + +#### Requirements +- Figma 전체 화면 기준 Sort-bar는 `290:9093` 내 `sort-bar`이다. +- 좌측에는 `전체`와 `donationCount`를 표시한다. +- 우측 정렬 label, 정렬 icon, 정렬 popup 진입 영역은 표시하지 않는다. +- 후원 탭에서는 정렬 상태를 보관하거나 API query로 전달하지 않는다. + +#### Edge Cases +- `donationCount == 0`이고 표시 가능한 후원 내역이 없으면 empty 상태 정책을 우선한다. +- 다국어 label 길이가 길어져도 전체 개수와 겹치지 않아야 한다. + +### Donation List Item +후원 내역 목록은 Figma의 `support` item 구조를 기준으로 표시한다. + +#### Requirements +- 각 item은 `gray/900` 배경, radius 14dp 카드로 표시한다. +- item header에는 후원자 프로필 이미지, 닉네임, 작성 시간, 후원 캔 수량 badge를 표시한다. +- header 배경색은 기존 홈 탭 후원 카드의 `calculateCreatorChannelDonationHeaderColorRes(can)` 정책을 우선 재사용한다. +- 후원 캔 수량은 can icon과 `%d캔` 텍스트로 표시한다. +- `createdAtUtc`는 크리에이터 채널 v2 공통 상대 시간 포맷을 따른다. +- 메시지는 header 아래에 16sp regular white text로 표시하고 긴 내용은 여러 줄로 확장한다. +- `message`가 빈 문자열이면 기존 홈 탭 후원 카드 fallback 문구를 표시한다. +- 현재 후원 탭 API는 비밀 후원 여부를 별도로 내려주지 않으므로 비밀 후원 전용 아이콘, 안내 문구, 익명 처리 분기를 구현하지 않는다. + +#### Edge Cases +- 닉네임이 긴 경우 header의 텍스트 영역에서 말줄임 처리한다. +- `createdAtUtc` 파싱 실패 시 기존 날짜 표시 fallback 정책을 따른다. +- `can <= 0` 값이 내려와도 앱에서 크래시가 발생하지 않아야 하며, 서버 응답 값을 그대로 표시한다. +- 메시지가 매우 긴 경우 item 높이는 자연스럽게 확장하되 인접 item과 겹치지 않아야 한다. + +### Floating Donation Button +우측 하단 floating 후원하기 버튼은 홈 탭 후원하기 액션과 동일하게 동작한다. + +#### Requirements +- Figma 전체 화면 기준 우측 하단 `button-floating`을 표시한다. +- 버튼은 목록 위에 floating 형태로 배치한다. +- 버튼 icon과 색상은 Figma와 대응되는 기존 프로젝트 asset/token을 우선 사용한다. +- 타인 채널에서 버튼을 누르면 홈 탭의 `onCreatorChannelDonationClicked()`와 동일한 후원 dialog를 연다. +- 후원 dialog는 기존 홈 탭과 동일하게 `LiveRoomDonationDialog`를 사용한다. +- 후원 dialog 옵션과 동작은 홈 탭 후원하기 액션과 동일하게 유지한다. +- 후원 API는 기존 `CreatorChannelRepository.postChannelDonation()` 흐름을 재사용한다. +- 후원 성공 시 `SharedPreferenceManager.can` 차감 정책은 홈 탭과 동일하게 적용한다. +- 후원 성공 후 후원 탭은 `GET /api/v2/creator-channels/{creatorId}/donations?page=0&size=20`을 다시 호출해 화면을 갱신한다. +- 같은 채널의 홈 탭 데이터가 이미 로드되어 있다면 홈 탭도 기존 delegate를 통해 갱신할 수 있다. +- 본인 채널에서는 floating 후원하기 버튼을 숨긴다. + +#### Edge Cases +- 후원 dialog를 닫거나 취소하면 후원 API를 호출하지 않는다. +- 후원 API 실패 시 기존 홈 탭 후원 실패 toast/error 정책을 따른다. +- 후원 요청 중 중복 터치로 API가 중복 호출되지 않아야 한다. +- 후원 성공 후 재조회가 실패하면 기존 후원 성공 처리 자체는 유지하고, 후원 탭은 error/retry 또는 기존 목록 유지 정책 중 구현 계획에서 결정한다. +- floating button은 목록 하단 item, 시스템 navigation bar, keyboard와 겹치지 않아야 한다. + +### Pagination +후원 내역 목록은 스크롤 하단 접근 시 다음 페이지를 로딩한다. + +#### Requirements +- `CreatorChannelDonationTabResponse.hasNext == true`일 때만 다음 페이지를 요청한다. +- 다음 페이지는 마지막 성공 응답의 `page + 1`로 요청한다. +- 다음 페이지 요청에는 `size=20`을 유지한다. +- 다음 페이지 로딩 중에는 추가 page 요청을 중복으로 보내지 않는다. +- 다음 페이지 성공 시 기존 `donations` 뒤에 append한다. +- 첫 페이지 새로고침 또는 후원 성공 갱신 시 기존 pagination 상태는 초기화한다. + +#### Edge Cases +- 빠른 스크롤로 load-more trigger가 반복 발생해도 page가 중복 append되지 않아야 한다. +- Fragment/View 재생성 후 현재 목록과 page 상태는 ViewModel 상태 보존 정책에 따라 유지되어야 한다. +- 마지막 페이지 응답 이후 `hasNext == false`이면 이후 load-more trigger를 무시한다. + +### Empty State +후원 내역이 없으면 목록 대신 empty 상태를 표시한다. + +#### Requirements +- `donationCount == 0` 또는 표시 가능한 `donations`가 없는 전체 empty 상태이면 empty 상태를 표시한다. +- empty 상태에서는 후원 내역 목록을 표시하지 않는다. +- empty 상태에서는 Sort-bar를 숨긴다. +- 후원 랭킹 섹션은 `rankings`가 비어 있으면 숨긴다. +- empty 상태는 Figma `290:9008`을 기준으로 표시한다. +- empty 문구는 `아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!`이다. +- empty 문구는 16sp regular, `gray/500`, center 정렬로 표시한다. +- 타인 채널 empty 상태에서는 문구 아래에 `후원하기` capsule button을 표시한다. +- empty `후원하기` button은 Figma처럼 `soda/400` 배경, gift icon, 16sp medium white text를 기준으로 한다. +- empty `후원하기` button 터치 시 floating 후원하기 버튼과 동일하게 홈 탭 후원하기 액션을 호출한다. +- 본인 채널 empty 상태에서는 문구 아래의 `후원하기` button을 숨긴다. +- 본인 채널 empty 상태에서는 floating 후원하기 버튼도 숨긴다. + +#### Edge Cases +- API 최초 조회 실패 상태는 empty 상태로 취급하지 않고 기존 error/retry 패턴을 따른다. +- `donationCount > 0`이지만 첫 페이지 `donations`가 비어 있고 표시 가능한 item이 없으면 empty 상태를 표시하되, 전체 개수는 서버 응답의 `donationCount` 값을 유지한다. + +--- + +## 8. UX / UI Expectations +- 후원 탭 선택 시 공통 header와 tab-bar는 기존 크리에이터 채널 컨테이너 구조를 유지한다. +- 후원 탭 컨텐츠는 Figma `290:9093`처럼 black 배경 위에 구성한다. +- Sort-bar는 52dp 높이 기준으로 `전체`와 개수만 표시한다. +- 랭킹 카드와 후원 내역 카드는 좌우 14dp margin 기준으로 배치한다. +- 카드 간 vertical spacing은 Figma 기준을 우선하되 기존 크리에이터 채널 탭 spacing token과 충돌하면 기존 token을 우선한다. +- 모든 텍스트는 Pretendard 기반 기존 앱 폰트 스타일을 따른다. +- 후원 목록 스크롤 중 floating button이 주요 텍스트를 과도하게 가리지 않아야 한다. +- empty 상태의 `후원하기` button과 floating 후원하기 버튼은 같은 후원 액션을 호출하지만, 본인 채널에서는 둘 다 노출하지 않는다. +- 이미지 로딩 중/실패 시 기존 프로필 placeholder를 사용해 레이아웃이 흔들리지 않아야 한다. + +--- + +## 9. Technical Constraints +- 신규 후원 탭 전용 Fragment/ViewModel/DTO/mapper/adapter는 `kr.co.vividnext.sodalive.v2.creator.channel.donation` 하위에 작성한다. +- API/Repository endpoint 추가는 기존 채널 공통 `CreatorChannelApi`/`CreatorChannelRepository`에 최소 변경으로 추가한다. +- 기존 `CreatorChannelPagerAdapter`의 `CreatorChannelTab.Donation` placeholder를 신규 `CreatorChannelDonationFragment`로 교체한다. +- 기존 홈 탭 후원 API 호출 흐름인 `CreatorChannelRepository.postChannelDonation()`을 재사용한다. +- 기존 홈 탭 후원 item의 색상 계산, fallback message, 상대 시간 formatter는 가능한 범위에서 재사용한다. +- 랭킹 `전체보기` 이동은 기존 `UserProfileDonationAllViewActivity`를 재사용하고 신규 전체보기 화면을 만들지 않는다. +- 기존 `UserProfileDonationAllViewActivity`는 `Constants.EXTRA_USER_ID`를 요구하므로, 구현 계획에서 크리에이터 채널에서 전달할 식별자가 기존 Activity 계약과 맞는지 확인한다. +- 레거시 후원 dialog 또는 repository 파일은 직접 수정하지 않는다. +- DTO annotation은 프로젝트 Retrofit/Gson 관례에 맞춰 구현 계획에서 `@SerializedName` 사용 여부를 확인한다. PRD의 `@JsonProperty`는 서버 계약 설명으로 기록한다. +- 구현 전 `plan-task.md`를 작성하고, 해당 문서의 Phase/Task별 검증 기준에 따라 최소 테스트를 작성한다. + +--- + +## 10. Metrics +- 후원 탭 최초 로딩 성공률. +- 후원 탭 pagination 성공률과 중복 요청 방지 여부. +- 후원하기 버튼 터치 후 dialog 노출률. +- 후원 성공 후 후원 탭 재조회 성공률. +- 후원 성공 후 `donationCount`, `rankings`, `donations` 최신화 여부. + +--- + +## 11. Resolved Decisions +- 랭킹 섹션의 `전체보기` 버튼은 기존 `UserProfileDonationAllViewActivity`로 이동한다. +- `rankings`는 서버가 항상 최대 8명만 내려준다. +- 후원 내역 empty 상태는 Figma `290:9008`을 기준으로 구현한다. +- 본인 채널에서는 empty 상태의 하단 `후원하기` button과 floating 후원하기 버튼을 모두 숨긴다. +- 현재 API는 비밀 후원 여부를 별도로 내려주지 않으므로 이번 범위에서는 비밀 후원 관련 UI/표시 분기를 구현하지 않는다. + +--- + +## 12. References +- 전체 Figma: https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9093&m=dev +- 후원 랭킹 섹션 Figma: https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9097&m=dev +- 후원 empty 상태 Figma: https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=290-9008&m=dev +- 샘플 PRD: `docs/prd/sample-prd.md` +- 작업 문서 규칙: `docs/agent-guides/work-plan-docs.md` +- 기존 홈 탭 PRD: `docs/20260611_크리에이터_채널_홈_탭/prd.md` +- 기존 FanTalk 탭 PRD: `docs/20260622_FanTalk_탭/prd.md` +- 기존 후원 전체보기 Activity: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/donation/UserProfileDonationAllViewActivity.kt` + +--- + +## 13. Verification Log +- 2026-06-22: `docs/prd/sample-prd.md`와 `docs/agent-guides/work-plan-docs.md`를 확인해 신규 문서 경로와 PRD 섹션 구성을 맞췄다. +- 2026-06-22: Figma `290:9093`, `290:9097`의 design context와 screenshot을 확인해 후원 랭킹 카드, Sort-bar, 후원 내역 item, floating button 구조를 PRD에 반영했다. +- 2026-06-22: 기존 홈 탭 PRD와 `CreatorChannelActivity.onCreatorChannelDonationClicked()`, `CreatorChannelHomeViewModel.postChannelDonation()` 흐름을 확인해 후원 탭 후원하기 액션 재사용 및 성공 후 갱신 요구를 PRD에 반영했다. +- 2026-06-22: 사용자 확인사항을 반영해 랭킹 `전체보기`는 기존 `UserProfileDonationAllViewActivity` 이동으로 확정하고, `rankings` 최대 8명 서버 보장, Figma `290:9008` empty 상태, 본인 채널 후원하기 버튼 숨김, 비밀 후원 표시 제외 정책을 PRD에 보강했다.