Files
sodalive-ios/docs/20260704_크리에이터_채널_후원_탭/prd.md

223 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PRD: 크리에이터 채널 후원 탭
## 1. Overview
크리에이터 채널 공통 shell의 `후원` 탭에서 채널 후원 랭킹과 후원 내역 목록을 제공한다. 상단 title bar, header, sticky tab-bar는 기존 크리에이터 채널 홈/라이브/오디오/시리즈 구현을 재사용하고, tab-bar 아래 콘텐츠만 후원 탭 전용 API 응답으로 구성한다.
API는 `GET /api/v2/creator-channels/{creatorId}/donations`를 사용한다. `creatorId`는 path variable이며, query parameter는 `page`, `size`를 사용한다. 기본값은 `page=0`, `size=20`이다.
Figma 참조:
- 전체 화면: `290:9093`, `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`
- 후원 랭킹 섹션: `290:9097`, `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: `290:9009`, `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-9009&m=dev`
## 2. Problem
- 현재 `CreatorChannelTab.donation`은 placeholder로 표시되어 실제 후원 랭킹과 후원 내역을 확인할 수 없다.
- 기존 홈 탭에는 채널 후원 요약 섹션과 후원하기 액션이 있지만, 후원 탭 전용 API, 랭킹 섹션, 스크롤 페이징이 없다.
- 후원 후 홈 탭 후원 섹션은 갱신되지만, 후원 탭이 구현되면 후원 탭 목록도 함께 갱신되어야 한다.
- 후원 탭 empty 문구와 `후원하기` 버튼이 한국어로 하드코딩되어 앱 언어 설정에 맞게 표시되지 않는다.
- 후원 랭킹 title·전체보기 action과 공용 후원 카드의 can 표기·빈 메시지 fallback이 한국어로 하드코딩되어 영어·일본어 설정에서도 한국어가 노출된다.
- `rankings.isEmpty && donationCount == 0`인 empty 상태에서도 count bar가 `전체 0`으로 표시된다.
- 후원 item 우측 상단 can badge가 icon과 숫자 뒤에 언어별 단위까지 중복 표시한다.
## 3. Goals
- `CreatorChannelTab.donation` 선택 시 후원 탭 API를 호출하고 응답 데이터로 화면을 구성한다.
- 기본 query는 `page=0`, `size=20`이다.
- `rankings.isEmpty && donationCount == 0`인 empty 상태에서는 count bar를 숨기고, 그 외에는 sort-bar 좌측에 `전체 {donationCount}`를 표시한다.
- sort-bar에는 정렬 UI를 표시하지 않는다.
- `rankings`는 Figma 후원 랭킹 섹션(`290:9097`)처럼 `후원 랭킹` 카드 안에 프로필 그리드로 표시한다.
- 후원 랭킹 섹션의 `전체보기`를 터치하면 기존 `UserProfileDonationAllView`로 이동한다.
- `donations`는 기존 홈 탭 후원 카드의 시각 규칙을 재사용해 세로 목록으로 표시한다.
- 목록 하단 도달 시 `hasNext == true`이면 `page + 1`을 조회해 append한다.
- `rankings.isEmpty && donationCount == 0`이면 Figma empty view(`290:9009`)를 표시한다.
- Empty view 안의 `후원하기` 버튼은 홈 탭 후원하기 버튼과 동일한 액션을 사용한다.
- `donationCount == 0` empty 상태에서는 하단 고정 후원하기 버튼을 숨긴다.
- `donationCount != 0 || !rankings.isEmpty`이면 홈 탭 plus button 위치에 icon-only 후원하기 버튼을 표시한다.
- 후원 성공 후 후원 탭 데이터와 홈 탭 데이터가 갱신되어야 한다.
- 후원 랭킹과 공용 후원 카드의 고정 UI 문구는 기존 I18n을 우선 재사용하고, 재사용할 수 없는 빈 메시지 fallback만 `I18n.CreatorChannelDonation`에 추가한다.
- 홈·후원 탭 공용 item의 can badge는 기존 can icon과 숫자만 표시하고 숫자 뒤 ko/en/ja 단위 문자열은 제거한다.
## 4. Non-Goals
- 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다.
- 채널 후원 mutation API나 `LiveRoomDonationDialogView`를 새로 만들지 않는다.
- 기존 `ChannelDonationAllView`를 확장하지 않는다.
- 랭킹 전체보기 전용 화면/API를 새로 만들지 않고 기존 `UserProfileDonationAllView`를 재사용한다.
- 서버 응답에 없는 비밀 후원 여부를 클라이언트에서 추론하지 않는다.
- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다.
## 5. Core Requirements
### 5.1 API
- Method: `GET`
- Path: `/api/v2/creator-channels/{creatorId}/donations`
- Path parameter:
- `creatorId`
- Query parameters:
- `page`: 기본값 `0`
- `size`: 기본값 `20`
- 응답 래퍼는 기존 패턴대로 `ApiResponse<CreatorChannelDonationTabResponse>`로 디코딩한다.
```kotlin
data class CreatorChannelDonationTabResponse(
val donationCount: Int,
val rankings: List<MemberDonationRankingResponse>,
val donations: List<CreatorChannelDonationResponse>,
val page: Int,
val size: Int,
val hasNext: Boolean
)
data class MemberDonationRankingResponse(
val userId: Long,
val nickname: String,
val profileImage: String,
val donationCan: Int
)
data class CreatorChannelDonationResponse(
val nickname: String,
val profileImageUrl: String,
val can: Int,
val message: String,
val createdAtUtc: String
)
```
- Kotlin `Long`은 기존 V2 모델 관례대로 Swift `Int`로 선언한다.
- `CreatorChannelDonationResponse`는 홈 탭 모델과 동일한 필드이므로 중복 타입 생성을 피하고 기존 타입을 재사용한다.
### 5.2 Sort-bar
- 후원 탭 sort-bar는 정렬이 없고 전체 개수만 포함한다.
- `rankings.isEmpty && donationCount == 0`이 아닐 때만 좌측에 `전체``donationCount`를 표시한다.
- `rankings.isEmpty && donationCount == 0`인 empty 상태에서는 `CreatorChannelDonationCountBar` 전체를 표시하지 않는다.
- 우측 정렬 버튼, 정렬 아이콘, 정렬 context popup, BottomSheet는 표시하지 않는다.
- 기존 `CreatorChannelSortBar`가 정렬 버튼을 필수로 노출하므로, 후원 탭에서는 별도 `CreatorChannelDonationCountBar`를 만들거나 `CreatorChannelSortBar`를 선택적으로 정렬 숨김 처리할 수 있다.
### 5.3 Ranking section
- `rankings`가 비어 있으면 후원 랭킹 섹션은 숨긴다.
- `rankings`가 있으면 Figma `290:9097``665:19056` 기준의 카드로 표시한다.
- 후원 랭킹은 최대 8명만 표시하고, 한 줄에 4명씩 표시한다.
- 카드 배경은 `Color.gray900`, corner radius는 14pt, 내부 padding과 item 간격은 14pt 기준을 사용한다.
- title은 기존 `I18n.Explorer.donationRankingTitle`을 재사용해 한국어 `후원랭킹`, 영어 `Donation ranking`, 일본어 `後援ランキング`을 표시한다.
- item은 75pt 원형 프로필, 중앙 정렬 닉네임, 프로필 하단에 rank number overlay를 표시한다.
- 랭킹 번호는 응답 배열 순서 기준으로 `index + 1`을 표시한다.
- 닉네임은 1줄 말줄임 처리한다.
- `profileImage``DownsampledKFImage`로 표시하고, 비어 있거나 실패하면 기존 프로필 placeholder 관례를 따른다.
- Figma의 `전체보기` 버튼은 기존 `I18n.CreatorChannelHome.viewAll`을 재사용한다.
- `전체보기` 터치 시 기존 `AppStep.userProfileDonationAll(userId:)`로 이동해 `UserProfileDonationAllView`를 연다.
- `userId`에는 현재 크리에이터 채널의 `creatorId`를 전달한다.
### 5.4 Donation list
- `donations`는 세로 목록으로 표시한다.
- 각 item은 기존 홈 탭 `CreatorChannelDonationSection` 카드와 동일한 정보 구조를 사용한다.
- `profileImageUrl`
- `nickname`
- `createdAtUtc` 상대 시간
- `can`
- `message`
- 홈·후원 탭 공용 item의 후원 금액 pill은 기존 can icon과 숫자만 표시하고 `I18n.LiveChat.canWithUnit(_:)`의 ko/en/ja 단위 문자열은 표시하지 않는다.
- `CreatorChannelDonationCard`의 기존 공용 구조를 유지해 두 탭에 동일하게 적용한다.
- 카드 상단 배경색은 홈 탭 후원 카드의 `can` 구간 색상을 재사용한다.
- `1...50`: `#E2E2E2`
- `51...100`: `#73EE01`
- `101...500`: `#00EAFF`
- `501...`: `#FF4C3C`
- `can >= 501`인 후원 item은 상단 영역에서 `nickname`을 white, 상대 시간을 `gray100`으로 표시한다.
- 그 외 후원 `can` 구간의 `nickname`과 상대 시간 글자색은 기존 표시 색상을 유지한다.
- `message`가 빈 문자열이면 홈 탭과 후원 탭에서 공용 `I18n.CreatorChannelDonation.defaultMessage(_:)`를 사용한다.
- 한국어: `{can}캔을 후원하였습니다`
- 영어: `Donated {can} cans.`
- 일본어: `{can}canをサポートしました。`
- 서버 응답에 비밀 후원 여부가 없으므로, 클라이언트는 서버가 내려준 `nickname`, `profileImageUrl`, `message`를 그대로 표시한다.
### 5.5 Pagination
- 첫 진입 시 `page=0`, `size=20`으로 조회한다.
- 다음 페이지 로딩 중 중복 호출을 막는다.
- 마지막 item이 화면에 나타났고 `hasNext == true`이면 `page + 1`을 조회한다.
- 다음 페이지 성공 시 기존 `donations` 뒤에 append한다.
- 첫 페이지 재조회 시 기존 목록을 교체한다.
- 빈 페이지가 내려오고 `hasNext == true`인 경우 무한 루프를 피하기 위해 한 번의 다음 페이지 요청만 수행하고 이후 서버 응답 기준으로 상태를 갱신한다.
### 5.6 Donation action
- 후원 탭의 모든 `후원하기` button tap은 홈 탭의 후원하기 버튼 tap action과 동일하게 동작한다.
- 기존 `LiveRoomDonationDialogView``ChannelDonationViewModel.postChannelDonation` 흐름을 재사용한다.
- Dialog 설정은 홈 탭 채널 후원과 동일하게 사용한다.
- `isAudioContentDonation: false`
- `messageLimit: 100`
- `I18n.MemberChannel.secretDonationLabel`
- `I18n.MemberChannel.secretDonationMinimumCanMessage`
- `shouldPrefixSecretInMessagePlaceholder: false`
- 후원 성공 시:
- `CreatorChannelViewModel.fetchHome(creatorId:)`로 홈 탭 데이터를 갱신한다.
- 후원 탭 ViewModel의 첫 페이지를 다시 조회해 랭킹과 목록을 갱신한다.
### 5.7 Empty and floating donation button
- `rankings.isEmpty && donationCount == 0`이면 Figma `290:9009` 기준의 empty view를 표시한다.
- Empty 문구는 아래 언어별 값을 사용한다. `\n`은 명시적 줄바꿈이다.
- 한국어: `아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!`
- 영어: `No support yet.\nBe the first to support this creator!`
- 일본어: `まだサポートがありません。\n最初のサポートをしてみましょう`
- Empty view에는 `ic_new_donation` 아이콘과 기존 `I18n.ContentDetail.DonationDialog.title` 텍스트가 있는 `Color.soda400` capsule button을 표시하고, 아이콘과 텍스트는 white를 사용한다.
- `후원하기`용 신규 I18n 키를 만들지 않는다.
- 단, 내 채널인 경우 Empty view 내부 후원하기 버튼을 표시하지 않는다.
- Empty view의 후원하기 버튼 tap은 홈 탭 후원하기 버튼 tap action과 동일하게 동작한다.
- Empty 상태에서는 하단 고정 후원하기 버튼을 표시하지 않는다.
- `donationCount != 0 || !rankings.isEmpty`이면 홈 탭 plus button 위치에 후원하기 floating button을 표시한다.
- 단, 내 채널인 경우 후원하기 floating button을 표시하지 않는다.
- 후원하기 floating button은 `Color.soda400` 배경과 `ic_new_donation` 아이콘만 표시하고 텍스트는 표시하지 않는다.
## 6. Success Criteria
- 후원 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/donations?page=0&size=20`가 호출된다.
- empty 상태가 아니면 sort-bar에 `전체 {donationCount}`만 표시되고 정렬 버튼/아이콘/popup은 표시되지 않는다.
- `rankings`가 있으면 `후원 랭킹` 카드에 프로필 그리드와 rank number가 표시된다.
- `후원 랭킹` 카드의 `전체보기`를 터치하면 `UserProfileDonationAllView`로 이동한다.
- `donations` 목록은 기존 홈 탭 후원 카드와 같은 후원 금액별 상단 컬러, 상대 시간, 메시지 표시 규칙을 따르되, 후원 탭 can pill은 icon과 숫자만 표시한다.
- 앱 언어가 ko/en/ja일 때 후원 랭킹 title·전체보기 action·빈 메시지 fallback이 해당 언어로 표시된다.
- 후원 탭 can pill은 앱 언어와 관계없이 can icon과 숫자만 표시되고 숫자 뒤 단위 문자열은 표시되지 않는다.
- 홈 탭의 공용 후원 카드도 can icon과 숫자만 표시되고 숫자 뒤 단위 문자열은 표시되지 않는다.
- 목록 하단 도달 시 `hasNext == true`이면 다음 페이지가 append된다.
- `rankings.isEmpty && donationCount == 0`이면 count bar 없이 Figma `290:9009` empty view가 표시되고, 하단 고정 후원하기 버튼은 숨겨진다.
- Empty 문구가 지정된 ko/en/ja 값과 줄바꿈으로 표시되고, 버튼 문구는 기존 `I18n.ContentDetail.DonationDialog.title`을 사용한다.
- 내 채널인 경우 Empty view의 후원하기 버튼과 우측하단 icon-only 후원하기 버튼이 모두 표시되지 않는다.
- Empty view의 후원하기 버튼을 누르면 홈 탭과 동일한 후원 dialog가 열린다.
- `donationCount != 0 || !rankings.isEmpty`이면 홈 탭 plus button 위치에 `ic_new_donation` icon-only 후원하기 버튼이 표시된다.
- Icon-only 후원하기 버튼을 누르면 홈 탭과 동일한 후원 dialog가 열린다.
- 후원 성공 후 홈 탭 후원 섹션과 후원 탭 랭킹/목록이 갱신된다.
- 서버 응답에 없는 secret 여부는 클라이언트에서 임의로 만들지 않는다.
## 7. Technical Constraints
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
- 후원 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/Donation/**` 아래에 둔다.
- `CreatorChannelDonationResponse``Home/Models/CreatorChannelHomeResponse.swift`의 기존 타입을 재사용한다.
- 후원 item UI는 기존 홈 탭 구현과 중복을 줄이기 위해 공용 컴포넌트로 추출하거나, 홈/후원 탭 양쪽에서 같은 하위 컴포넌트를 사용하도록 최소 범위로 조정한다.
- 신규 문구가 필요하면 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
- Empty 문구는 `pick(ko:en:ja:)`로 관리하고, 후원 버튼은 기존 `I18n.ContentDetail.DonationDialog.title`을 재사용한다. 신규 후원 버튼 문구, route, API, dependency는 추가하지 않는다.
- 랭킹 title은 `I18n.Explorer.donationRankingTitle`, 전체보기는 `I18n.CreatorChannelHome.viewAll`을 재사용한다.
- 홈·후원 탭 공용 can pill은 단위 formatter나 신규 I18n을 추가하지 않고 서버의 `can` 숫자를 그대로 표시한다.
- 빈 후원 메시지 fallback만 `I18n.CreatorChannelDonation.defaultMessage(_:)`로 추가하며 신규 dependency나 공용 formatter abstraction은 만들지 않는다.
- 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다.
## 8. Open Questions
- 해당 없음
## 9. Verification Notes
- 2026-07-04: Figma `get_design_context``get_screenshot`으로 `290:9093`, `290:9097`을 확인했다.
- 2026-07-04: `CreatorChannelView`, `CreatorChannelDonationSection`, `CreatorChannelSortBar`, `ChannelDonationViewModel`, 기존 후원 전체 화면 코드를 확인해 재사용 경계를 정했다.
- 2026-07-04: Figma `get_design_context``get_screenshot`으로 empty view `290:9009`를 확인했다.
- 2026-07-04: `UserProfileDonationAllView``AppStep.userProfileDonationAll(userId:)` 기존 라우팅을 확인해 랭킹 전체보기 재사용 경계를 정했다.
- 2026-08-14: 사용자 인터뷰로 후원 탭 empty 문구의 ko/en/ja 값을 확정했다. `후원하기`는 기존 `I18n.ContentDetail.DonationDialog.title`을 재사용하며 이번 작업에서는 Swift 구현을 수행하지 않는다.
- 2026-08-26: `CreatorChannelDonationRankingSection`, 공용 `CreatorChannelDonationCard`, 두 컴포넌트의 호출 경로를 정적 점검해 런타임 하드코딩 4곳을 확정했다. Preview fixture의 샘플 문구는 런타임 제품 문구가 아니므로 제외했다.
- 2026-08-26: 사용자 인터뷰로 랭킹 title은 기존 `I18n.Explorer.donationRankingTitle`을 재사용하고, 빈 후원 메시지는 카드 전용 ko/en/ja 문구를 추가하기로 결정했다. 이번 작업에서는 문서만 갱신하고 Swift·I18n 구현과 빌드는 수행하지 않는다.
- 2026-08-26: Phase 7의 다국어 요구사항을 구현했다. `§5.3`, `§5.4`, `§6`, `§7`의 랭킹 title·전체보기·can pill·빈 메시지 fallback과 홈·후원 탭 공용 적용을 정적 계약, Debug 빌드, 독립 리뷰로 확인했다. 이 PRD에는 체크박스 항목이 없으며, 구현 체크 상태는 `plan-task.md` Phase 7에 기록했다. 사용자 요청에 따라 visual QA는 실행하지 않았다.
- 2026-08-26: `CreatorChannelDonationTabView`, `CreatorChannelDonationCountBar`, 공용 `CreatorChannelDonationCard`와 홈 탭 호출부를 정적 확인했다. empty 상태의 count bar 숨김과 후원 탭 can 단위 제거 요구사항을 문서에 반영했으며, 공용 카드의 홈 탭 영향 범위는 미결로 남겼다. Swift 구현과 빌드는 수행하지 않았다.
- 2026-08-26: 사용자 인터뷰에서 A안을 선택해 can badge 단위 제거를 공용 `CreatorChannelDonationCard`를 사용하는 홈·후원 탭에 함께 적용하기로 확정했다. `OQ-DONATION-BUG-001`을 종료했으며 Swift 구현과 빌드는 수행하지 않았다.
- 2026-08-26: Phase 8을 구현해 후원 empty count bar를 숨기고 홈·후원 공용 카드의 can badge를 숫자만 표시하도록 수정했다. 정적 계약, `git diff --check`, `SodaLive-dev` Debug 빌드와 독립 사양·품질 리뷰가 통과했다. PRD에는 체크박스가 없으며 `§3`, `§5.2`, `§5.4`, `§5.7`, `§6`, `§7`, `DEC-DONATION-BUG-001`의 구현 누락이 없음을 대조했다. 사용자 요청에 따라 visual QA와 실제 화면 수동 검증은 수행하지 않았다.
## 10. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 범위 |
|---|---|---|---|---|---|
| 2026-08-26 | DEC-DONATION-I18N-001 | 확정 | 랭킹 title은 기존 `I18n.Explorer.donationRankingTitle`을 재사용한다. | 사용자 인터뷰에서 기존 `후원랭킹` 표기를 선택했다. | Ranking section |
| 2026-08-26 | DEC-DONATION-I18N-002 | 확정 | 빈 후원 메시지는 `I18n.CreatorChannelDonation.defaultMessage(_:)` 전용 문구로 관리한다. | 공개 카드에서 `You donated`가 작성자를 오인하게 하므로 사용자 인터뷰에서 전용 문구를 선택했다. | Home·Donation 공용 card |
| 2026-08-26 | DEC-DONATION-BUG-001 | 확정 | 공용 후원 카드의 can badge는 홈·후원 탭 모두 icon과 숫자만 표시하고 단위 문자열은 제거한다. | 사용자 인터뷰 A안 선택 | `§3`, `§5.4`, `§6`, Phase 8 |