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

193 lines
14 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 문구와 `후원하기` 버튼이 한국어로 하드코딩되어 앱 언어 설정에 맞게 표시되지 않는다.
## 3. Goals
- `CreatorChannelTab.donation` 선택 시 후원 탭 API를 호출하고 응답 데이터로 화면을 구성한다.
- 기본 query는 `page=0`, `size=20`이다.
- `donationCount`는 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 후원하기 버튼을 표시한다.
- 후원 성공 후 후원 탭 데이터와 홈 탭 데이터가 갱신되어야 한다.
## 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는 정렬이 없고 전체 개수만 포함한다.
- 좌측에 `전체``donationCount`를 표시한다.
- 우측 정렬 버튼, 정렬 아이콘, 정렬 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은 `후원 랭킹`을 표시한다.
- item은 75pt 원형 프로필, 중앙 정렬 닉네임, 프로필 하단에 rank number overlay를 표시한다.
- 랭킹 번호는 응답 배열 순서 기준으로 `index + 1`을 표시한다.
- 닉네임은 1줄 말줄임 처리한다.
- `profileImage``DownsampledKFImage`로 표시하고, 비어 있거나 실패하면 기존 프로필 placeholder 관례를 따른다.
- Figma의 `전체보기` 버튼을 표시한다.
- `전체보기` 터치 시 기존 `AppStep.userProfileDonationAll(userId:)`로 이동해 `UserProfileDonationAllView`를 연다.
- `userId`에는 현재 크리에이터 채널의 `creatorId`를 전달한다.
### 5.4 Donation list
- `donations`는 세로 목록으로 표시한다.
- 각 item은 기존 홈 탭 `CreatorChannelDonationSection` 카드와 동일한 정보 구조를 사용한다.
- `profileImageUrl`
- `nickname`
- `createdAtUtc` 상대 시간
- `can`
- `message`
- 후원 금액 pill은 기존 can icon과 `{can}캔` 형식을 사용한다.
- 카드 상단 배경색은 홈 탭 후원 카드의 `can` 구간 색상을 재사용한다.
- `1...50`: `#E2E2E2`
- `51...100`: `#73EE01`
- `101...500`: `#00EAFF`
- `501...`: `#FF4C3C`
- `can >= 501`인 후원 item은 상단 영역에서 `nickname`을 white, 상대 시간을 `gray100`으로 표시한다.
- 그 외 후원 `can` 구간의 `nickname`과 상대 시간 글자색은 기존 표시 색상을 유지한다.
- `message`가 빈 문자열이면 기존 홈 탭과 동일하게 `{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`가 호출된다.
- sort-bar에는 `전체 {donationCount}`만 표시되고 정렬 버튼/아이콘/popup은 표시되지 않는다.
- `rankings`가 있으면 `후원 랭킹` 카드에 프로필 그리드와 rank number가 표시된다.
- `후원 랭킹` 카드의 `전체보기`를 터치하면 `UserProfileDonationAllView`로 이동한다.
- `donations` 목록은 기존 홈 탭 후원 카드와 같은 후원 금액별 상단 컬러, can pill, 상대 시간, 메시지 표시 규칙을 따른다.
- 목록 하단 도달 시 `hasNext == true`이면 다음 페이지가 append된다.
- `rankings.isEmpty && donationCount == 0`이면 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는 추가하지 않는다.
- 프로젝트 설정 변경은 신규 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 구현을 수행하지 않는다.