From 265ac75eab26c6aaf7f50c6ff5766287220e1dfd Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Sat, 4 Jul 2026 22:44:20 +0900 Subject: [PATCH] =?UTF-8?q?docs(creator):=20=ED=9B=84=EC=9B=90=20=ED=83=AD?= =?UTF-8?q?=20=EA=B3=84=ED=9A=8D=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 226 ++++++++++++++++++ docs/20260704_크리에이터_채널_후원_탭/prd.md | 182 ++++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 docs/20260704_크리에이터_채널_후원_탭/plan-task.md create mode 100644 docs/20260704_크리에이터_채널_후원_탭/prd.md diff --git a/docs/20260704_크리에이터_채널_후원_탭/plan-task.md b/docs/20260704_크리에이터_채널_후원_탭/plan-task.md new file mode 100644 index 00000000..2e69d864 --- /dev/null +++ b/docs/20260704_크리에이터_채널_후원_탭/plan-task.md @@ -0,0 +1,226 @@ +# 크리에이터 채널 후원 탭 구현 계획 + +## 기준 문서 + +- PRD: `docs/20260704_크리에이터_채널_후원_탭/prd.md` +- 기존 크리에이터 채널 홈 PRD: `docs/20260701_크리에이터_채널_홈/prd.md` +- 기존 크리에이터 채널 홈 구현 계획: `docs/20260701_크리에이터_채널_홈/plan-task.md` +- 기존 크리에이터 채널 오디오 탭 구현 계획: `docs/20260704_크리에이터_채널_오디오_탭/plan-task.md` +- 코드 스타일: `docs/agent-guides/code-style.md` +- 빌드/검증: `docs/agent-guides/build-test-verification.md` +- 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` +- Figma 후원 랭킹 섹션: `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` +- Figma 후원 랭킹 최신 섹션: `665:19056`, `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=665-19056&m=dev` +- Figma 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` + +## 구현 원칙 + +- 기존 `CreatorChannelView`의 공통 shell, header, sticky tab-bar, title bar 동작은 유지한다. +- `CreatorChannelTab.donation` 콘텐츠만 신규 `CreatorChannelDonationTabView`로 교체한다. +- 후원 mutation은 기존 `LiveRoomDonationDialogView`와 `ChannelDonationViewModel.postChannelDonation` 흐름을 재사용한다. +- 후원 성공 후 홈 데이터와 후원 탭 첫 페이지를 모두 다시 조회한다. +- 후원 랭킹은 최대 8명, 한 줄에 4명씩 표시하고 `전체보기`는 기존 `AppStep.userProfileDonationAll(userId:)`로 이동한다. +- sort-bar는 정렬 없이 `전체 {donationCount}`만 표시한다. +- `rankings.isEmpty && donationCount == 0`이면 empty view 내부 후원하기 버튼만 표시하고 하단 고정 후원하기 버튼은 숨긴다. +- 단, 내 채널인 경우 empty view 내부 후원하기 버튼과 우측하단 후원하기 버튼을 모두 표시하지 않는다. +- `donationCount != 0 || !rankings.isEmpty`이면 홈 탭 plus button 위치에 `ic_new_donation` icon-only 후원하기 버튼을 표시한다. +- `CreatorChannelDonationResponse`는 홈 탭 모델의 기존 타입을 재사용한다. +- 후원 item UI는 홈 탭과 후원 탭에서 같은 표시 규칙을 갖도록 공용화한다. +- 서버 응답에 없는 secret 여부는 클라이언트에서 추론하지 않는다. + +## 주요 대상 파일 + +### 생성 +- `SodaLive/Sources/V2/CreatorChannel/Donation/Models/CreatorChannelDonationTabResponse.swift` +- `SodaLive/Sources/V2/CreatorChannel/Donation/Repository/CreatorChannelDonationApi.swift` +- `SodaLive/Sources/V2/CreatorChannel/Donation/Repository/CreatorChannelDonationRepository.swift` +- `SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationViewModel.swift` +- `SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationTabView.swift` +- `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift` +- `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCountBar.swift` +- `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationEmptyView.swift` +- `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationFloatingButton.swift` + +### 수정 +- `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` +- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` +- `SodaLive/Sources/I18n/I18n.swift` +- 필요 시 `SodaLive.xcodeproj/project.pbxproj` + +## TASK 체크리스트 + +### Phase 1: 문서와 API 모델 + +- [x] **Task 1.1: PRD와 계획 문서 생성** + - 대상 파일: + - 생성: `docs/20260704_크리에이터_채널_후원_탭/prd.md` + - 생성: `docs/20260704_크리에이터_채널_후원_탭/plan-task.md` + - 검증 기준: + - 실행 명령: `rg "290:9093|290:9097|290:9009|CreatorChannelDonationTabResponse|MemberDonationRankingResponse|/api/v2/creator-channels/.*/donations|UserProfileDonationAllView" docs/20260704_크리에이터_채널_후원_탭` + - 기대 결과: Figma URL, 응답 모델, API path 요구사항이 문서에 기록된다. + +- [x] **Task 1.2: 후원 탭 Response model 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/Models/CreatorChannelDonationTabResponse.swift` + - 작업 내용: + - `CreatorChannelDonationTabResponse`와 `MemberDonationRankingResponse`를 `Decodable`로 생성한다. + - `CreatorChannelDonationTabResponse.donations`는 기존 `[CreatorChannelDonationResponse]`를 사용한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelDonationTabResponse|donationCount|rankings|donations|hasNext|struct MemberDonationRankingResponse|donationCan" SodaLive/Sources/V2/CreatorChannel/Donation/Models/CreatorChannelDonationTabResponse.swift` + +### Phase 2: API, Repository, ViewModel + +- [x] **Task 2.1: 후원 탭 API endpoint 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/Repository/CreatorChannelDonationApi.swift` + - 작업 내용: + - `CreatorChannelDonationApi.getDonations(creatorId:page:size:)`를 만든다. + - path는 `/api/v2/creator-channels/{creatorId}/donations`로 구성한다. + - query parameter는 `page`, `size`만 보낸다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationApi|getDonations|/api/v2/creator-channels/|/donations|page|size|URLEncoding.queryString" SodaLive/Sources/V2/CreatorChannel/Donation/Repository/CreatorChannelDonationApi.swift` + +- [x] **Task 2.2: 후원 탭 Repository 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/Repository/CreatorChannelDonationRepository.swift` + - 작업 내용: + - `MoyaProvider`를 사용하는 `getDonations(...)` publisher를 제공한다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationRepository|MoyaProvider|AnyPublisher|getDonations" SodaLive/Sources/V2/CreatorChannel/Donation/Repository/CreatorChannelDonationRepository.swift` + +- [x] **Task 2.3: 후원 탭 ViewModel 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationViewModel.swift` + - 작업 내용: + - 상태는 loading, loadingNextPage, response, rankings, donations, page, size, hasNext, hasLoaded, error toast를 둔다. + - `fetchFirstPage(creatorId:)`는 `page=0`, `size=20`으로 조회한다. + - `fetchNextPageIfNeeded(creatorId:currentItem:)`는 마지막 item 기준으로 다음 페이지를 조회한다. + - 첫 페이지는 목록 교체, 다음 페이지는 append한다. + - 최신 요청만 반영하도록 `latestRequestId` 패턴을 사용한다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationViewModel|fetchFirstPage|fetchNextPageIfNeeded|isLoadingNextPage|latestRequestId|rankings|donations|hasNext" SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationViewModel.swift` + +### Phase 3: Figma 섹션별 UI + +- [x] **Task 3.1: 후원 count bar 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCountBar.swift` + - 작업 내용: + - Figma sort-bar 기준으로 높이 52pt, 좌측 `전체 {donationCount}`만 표시한다. + - 정렬 버튼/아이콘/action은 포함하지 않는다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationCountBar|donationCount|전체|frame\\(height: 52\\)|ic_new_sort|selectedSort" SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCountBar.swift` + - 기대 결과: count bar 구현이 확인되고 정렬 관련 문자열은 없어야 한다. + +- [x] **Task 3.2: 후원 랭킹 섹션 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift` + - 작업 내용: + - `rankings`가 비어 있으면 섹션을 숨긴다. + - `후원 랭킹` title, `Color.gray900` 카드, 75pt 원형 프로필, 닉네임, rank number overlay를 구현한다. + - 후원 랭킹 item은 최대 8명만 표시하고 한 줄에 4명씩 표시한다. + - Figma `전체보기` 버튼을 표시하고 tap action은 부모에서 주입한다. + - 부모 action은 기존 `AppStep.userProfileDonationAll(userId: creatorId)` 이동으로 연결한다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationRankingSection|후원 랭킹|MemberDonationRankingResponse|rank \\+ 1|75|DownsampledKFImage|전체보기|onTapViewAll" SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift` + +- [x] **Task 3.3: 후원 item 공용화** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` + - 생성 또는 이동: `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCard.swift` + - 작업 내용: + - 홈 탭 내부 private `CreatorChannelDonationCard`를 후원 탭에서도 사용할 수 있는 공용 컴포넌트로 분리한다. + - 기존 홈 탭 표시 규칙과 preview 동작을 유지한다. + - 검증 기준: + - 실행 명령: `rg "struct CreatorChannelDonationCard|messageText|tierColor|relativeTimeText|CreatorChannelDonationSection" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift SodaLive/Sources/V2/CreatorChannel/Donation/Components` + +- [x] **Task 3.4: Empty view 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationEmptyView.swift` + - 작업 내용: + - Figma `290:9009` 기준으로 `아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!` 문구를 표시한다. + - `ic_new_donation` 아이콘과 `후원하기` 텍스트가 있는 `Color.soda400` capsule button을 표시한다. + - tap action은 부모에서 주입한다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationEmptyView|아직 후원이 없습니다|처음으로 크리에이터를 후원해 보세요|ic_new_donation|Color\\.soda400|후원하기|onTapDonate" SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationEmptyView.swift` + +- [x] **Task 3.5: 후원 탭 본문 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationTabView.swift` + - 작업 내용: + - count bar, ranking section, donation list를 순서대로 배치한다. + - 첫 진입 시 `fetchFirstPage(creatorId:)`를 호출한다. + - 마지막 donation item 노출 시 `fetchNextPageIfNeeded`를 호출한다. + - `rankings.isEmpty && donationCount == 0`이면 `CreatorChannelDonationEmptyView`를 표시한다. + - 내 채널인 경우 empty view 내부 후원하기 버튼을 숨긴다. + - `CreatorChannelDonationRankingSection`의 전체보기 action은 부모로 전달한다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationTabView|CreatorChannelDonationCountBar|CreatorChannelDonationRankingSection|CreatorChannelDonationEmptyView|CreatorChannelDonationCard|fetchFirstPage|fetchNextPageIfNeeded|onTapViewAll|onTapDonate" SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationTabView.swift` + +- [x] **Task 3.6: 후원하기 floating button 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationFloatingButton.swift` + - 작업 내용: + - 홈 탭 plus button 위치에 표시할 후원하기 floating button을 구현한다. + - 버튼은 `Color.soda400` 배경과 `ic_new_donation` 아이콘만 표시하고 텍스트는 표시하지 않는다. + - tap action은 부모에서 주입한다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationFloatingButton|ic_new_donation|Color\\.soda400|Capsule|action" SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationFloatingButton.swift` + - 실행 명령: `rg "Text\\(" SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationFloatingButton.swift` + - 기대 결과: 첫 번째 검색으로 icon-only floating button 구현이 확인되고, 두 번째 검색은 결과가 없어야 한다. + +### Phase 4: CreatorChannelView 연결과 후원 성공 갱신 + +- [x] **Task 4.1: 후원 탭 콘텐츠 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + - 작업 내용: + - `@StateObject`로 후원 탭 ViewModel을 보유한다. + - `selectedTabContent`에서 `.donation`일 때 `CreatorChannelDonationTabView`를 표시한다. + - 랭킹 `전체보기` action은 `AppState.shared.setAppStep(step: .userProfileDonationAll(userId: creatorId))`로 연결한다. + - 후원하기 floating button은 `selectedTab == .donation`이고 내 채널이 아니며 `donationCount != 0 || !rankings.isEmpty`일 때만 표시한다. + - `rankings.isEmpty && donationCount == 0`이면 empty view 내부 버튼만 사용하고 floating button은 숨긴다. + - 내 채널인 경우 empty view 내부 후원하기 버튼과 floating button을 모두 숨긴다. + - 검증 기준: + - 실행 명령: `rg "CreatorChannelDonationViewModel|CreatorChannelDonationTabView|viewModel\\.selectedTab == \\.donation|CreatorChannelDonationFloatingButton|showDonationDialog|userProfileDonationAll|donationCount|rankings\\.isEmpty" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + +- [x] **Task 4.2: 후원 성공 후 홈/후원 탭 갱신** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + - 작업 내용: + - `ChannelDonationViewModel.postChannelDonation` 성공 콜백에서 `viewModel.fetchHome(creatorId:)`와 `donationViewModel.fetchFirstPage(creatorId:)`를 호출한다. + - 검증 기준: + - 실행 명령: `rg "postChannelDonation|fetchHome\\(creatorId: creatorId\\)|donationViewModel\\.fetchFirstPage\\(creatorId: creatorId\\)" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + +### Phase 5: 프로젝트 등록과 검증 + +- [x] **Task 5.1: 프로젝트 파일 등록과 정적 검증** + - 대상 파일: + - 확인/수정: `SodaLive.xcodeproj/project.pbxproj` + - 검증 기준: + - 실행 명령: `git diff --check -- docs/20260704_크리에이터_채널_후원_탭 SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj` + - 기대 결과: 공백 오류 없이 성공한다. + - 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj` + - 기대 결과: `OK` 출력. + +- [x] **Task 5.2: 빌드 검증** + - 대상 파일: + - 확인: `SodaLive.xcworkspace` + - 확인: `SodaLive.xcodeproj` + - 검증 기준: + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - 기대 결과: `BUILD SUCCEEDED`. + - workspace 인식 실패 등 환경 문제가 있으면 `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive-dev" -configuration Debug build`를 대체 검증으로 실행하고 실패 원인을 검증 기록에 남긴다. + +## 검증 기록 + +- 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 `290:9093`, `290:9097`을 확인했다. +- 2026-07-04: `rg`와 `sed`로 기존 `CreatorChannelView`, `CreatorChannelDonationSection`, `CreatorChannelSortBar`, `ChannelDonationViewModel`, `ChannelDonationAllView` 관련 파일을 확인했다. +- 2026-07-04: 사용자 요청에 따라 랭킹 `전체보기`는 기존 `UserProfileDonationAllView`로 이동하도록 범위를 보완했다. `UserProfileDonationAllView`와 `AppStep.userProfileDonationAll(userId:)` 기존 라우팅을 확인했다. +- 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 empty view `290:9009`를 확인하고, `rankings.isEmpty && donationCount == 0` 조건과 empty 내부 후원하기 버튼 요구사항을 반영했다. +- 2026-07-04: 사용자 요청에 따라 `donationCount == 0` empty 상태에서는 하단 고정 후원하기 버튼을 숨기고, `donationCount != 0 || !rankings.isEmpty`일 때만 홈 탭 plus button 위치에 `ic_new_donation` icon-only 후원하기 버튼을 표시하도록 범위를 보완했다. + +- 2026-07-04: Task 1.2~5.1 구현 완료. 신규 `Donation/**` Swift 파일과 공용 `CreatorChannelDonationCard`, `CreatorChannelView` 연결, `project.pbxproj` 등록을 수행했고 문서의 `rg`, `plutil`, `git diff --check` 검증을 실행했다. +- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 `BUILD SUCCEEDED`를 확인했다. +- 2026-07-04: 사용자 요청에 따라 내 채널인 경우 후원 탭 empty view 내부 후원하기 버튼과 우측하단 icon-only 후원하기 버튼을 모두 표시하지 않도록 범위를 보완했다. +- 2026-07-04: 사용자 요청에 따라 후원 랭킹은 최대 8명만, 한 줄에 4명씩 표시하도록 범위를 보완했다. diff --git a/docs/20260704_크리에이터_채널_후원_탭/prd.md b/docs/20260704_크리에이터_채널_후원_탭/prd.md new file mode 100644 index 00000000..96a65a88 --- /dev/null +++ b/docs/20260704_크리에이터_채널_후원_탭/prd.md @@ -0,0 +1,182 @@ +# 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, 랭킹 섹션, 스크롤 페이징이 없다. +- 후원 후 홈 탭 후원 섹션은 갱신되지만, 후원 탭이 구현되면 후원 탭 목록도 함께 갱신되어야 한다. + +## 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`로 디코딩한다. + +```kotlin +data class CreatorChannelDonationTabResponse( + val donationCount: Int, + val rankings: List, + val donations: List, + 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` +- `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처음으로 크리에이터를 후원해 보세요!`를 표시한다. +- Empty view에는 `ic_new_donation` 아이콘과 `후원하기` 텍스트가 있는 `Color.soda400` capsule button을 표시한다. +- 단, 내 채널인 경우 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 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를 추가한다. +- 프로젝트 설정 변경은 신규 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:)` 기존 라우팅을 확인해 랭킹 전체보기 재사용 경계를 정했다.