feat(creator): 팬Talk 탭을 추가한다
This commit is contained in:
228
docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md
Normal file
228
docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# 크리에이터 채널 팬Talk 탭 구현 계획
|
||||
|
||||
## 기준 문서
|
||||
|
||||
- PRD: `docs/20260704_크리에이터_채널_팬Talk_탭/prd.md`
|
||||
- 기존 크리에이터 채널 홈 PRD: `docs/20260701_크리에이터_채널_홈/prd.md`
|
||||
- 기존 크리에이터 채널 후원 탭 구현 계획: `docs/20260704_크리에이터_채널_후원_탭/plan-task.md`
|
||||
- 코드 스타일: `docs/agent-guides/code-style.md`
|
||||
- 빌드/검증: `docs/agent-guides/build-test-verification.md`
|
||||
- Figma 전체 화면: `290:9139`, `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-9139&m=dev`
|
||||
- Figma 아이템 뷰(답글 없음): `290:9145`, `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-9145&m=dev`
|
||||
- Figma 아이템 뷰(답글 있음): `290:9147`, `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-9147&m=dev`
|
||||
- Figma 더보기 anchored context popup: `666:36114`, `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=666-36114&m=dev`
|
||||
- Figma Empty: `290:9001`, `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-9001&m=dev`
|
||||
|
||||
## 구현 원칙
|
||||
|
||||
- 기존 `CreatorChannelView`의 공통 shell, header, sticky tab-bar, title bar 동작은 유지한다.
|
||||
- `CreatorChannelTab.fanTalk` 콘텐츠만 신규 `CreatorChannelFanTalkTabView`로 교체한다.
|
||||
- 팬Talk API 모델은 홈 탭 모델과 타입명 충돌이 나지 않도록 분리한다.
|
||||
- sort-bar는 정렬 없이 `전체 {fanTalkCount}`만 표시한다.
|
||||
- 신고는 기존 `CheersReportDialogView`와 `ReportRepository.report`의 `ReportType.CHEERS` 흐름을 재사용한다.
|
||||
- 삭제는 기존 `ExplorerRepository.modifyCheers(cheersId:content:isActive:)`에 `isActive=false`를 전달하는 흐름을 재사용한다.
|
||||
- 더보기 버튼은 `ic_new_more`를 사용한다.
|
||||
- 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시하고, 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다.
|
||||
- 글쓰기와 수정하기 action은 신규 글쓰기 페이지 구현 전까지 no-op placeholder로 둔다.
|
||||
- 내 채널인 경우 우측하단 글쓰기 floating button과 empty view 내부 `응원 남기기` 버튼을 모두 표시하지 않는다.
|
||||
- 우측하단 plus button은 `CreatorChannelFloatingActionMenu`의 private `floatingButton`과 동일한 UI이므로, 팬Talk 전용으로 별도 구현하지 않고 `CreatorChannel/Components` 하위 공용 icon floating button으로 추출해 재사용한다.
|
||||
|
||||
## 주요 대상 파일
|
||||
|
||||
### 생성
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkApi.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkRepository.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkViewModel.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkCountBar.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
|
||||
|
||||
### 수정
|
||||
- `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift`
|
||||
- `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift`
|
||||
- `SodaLive/Sources/I18n/I18n.swift`
|
||||
- 필요 시 `SodaLive.xcodeproj/project.pbxproj`
|
||||
|
||||
## TASK 체크리스트
|
||||
|
||||
### Phase 1: 문서와 API 모델
|
||||
|
||||
- [x] **Task 1.1: PRD와 계획 문서 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `docs/20260704_크리에이터_채널_팬Talk_탭/prd.md`
|
||||
- 생성: `docs/20260704_크리에이터_채널_팬Talk_탭/plan-task.md`
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "290:9139|290:9145|290:9147|666:36114|290:9001|CreatorChannelFanTalkTabResponse|/api/v2/creator-channels/.*/fan-talks|ic_new_more" docs/20260704_크리에이터_채널_팬Talk_탭`
|
||||
- 기대 결과: Figma URL, 응답 모델, API path, 액션 요구사항이 문서에 기록된다.
|
||||
|
||||
- [x] **Task 1.2: 팬Talk 탭 Response model 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift`
|
||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift`
|
||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelFanTalkTabResponse`, `CreatorChannelFanTalkItemResponse`, `CreatorChannelFanTalkReplyResponse`를 `Decodable`로 생성한다.
|
||||
- 홈 탭 기존 `CreatorChannelFanTalkResponse`와 충돌하지 않도록 홈 탭 타입명을 변경하거나 신규 타입명을 다르게 둔다.
|
||||
- `Identifiable`은 `fanTalkId`를 `id`로 사용한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "struct CreatorChannelFanTalkTabResponse|fanTalkCount|fanTalks|hasNext|struct CreatorChannelFanTalkItemResponse|writerId|creatorReplies|struct CreatorChannelFanTalkReplyResponse" SodaLive/Sources/V2/CreatorChannel/FanTalk/Models/CreatorChannelFanTalkTabResponse.swift`
|
||||
|
||||
### Phase 2: API, Repository, ViewModel
|
||||
|
||||
- [x] **Task 2.1: 팬Talk 탭 API endpoint 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkApi.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelFanTalkApi.getFanTalks(creatorId:page:size:)`를 만든다.
|
||||
- path는 `/api/v2/creator-channels/{creatorId}/fan-talks`로 구성한다.
|
||||
- query parameter는 `page`, `size`만 보낸다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkApi|getFanTalks|/api/v2/creator-channels/|/fan-talks|page|size|URLEncoding.queryString" SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkApi.swift`
|
||||
|
||||
- [x] **Task 2.2: 팬Talk 탭 Repository 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkRepository.swift`
|
||||
- 작업 내용:
|
||||
- `MoyaProvider<CreatorChannelFanTalkApi>`를 사용하는 `getFanTalks(...)` publisher를 제공한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkRepository|MoyaProvider<CreatorChannelFanTalkApi>|AnyPublisher<Response, MoyaError>|getFanTalks" SodaLive/Sources/V2/CreatorChannel/FanTalk/Repository/CreatorChannelFanTalkRepository.swift`
|
||||
|
||||
- [x] **Task 2.3: 팬Talk 탭 ViewModel 생성**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkViewModel.swift`
|
||||
- 작업 내용:
|
||||
- 상태는 loading, loadingNextPage, response, fanTalks, page, size, hasNext, hasLoaded, error toast, 신고/삭제 dialog 상태를 둔다.
|
||||
- `fetchFirstPage(creatorId:)`는 `page=0`, `size=20`으로 조회한다.
|
||||
- `fetchNextPageIfNeeded(creatorId:currentItem:)`는 마지막 item 기준으로 다음 페이지를 조회한다.
|
||||
- 첫 페이지는 목록 교체, 다음 페이지는 append한다.
|
||||
- `report(reason:)`는 `ReportRequest(type: .CHEERS, cheersId: selectedFanTalkId, reason: reason)`를 호출한다.
|
||||
- `deleteSelectedFanTalk(creatorId:)`는 `ExplorerRepository.modifyCheers(..., isActive: false)`를 호출하고 성공 후 첫 페이지를 다시 조회한다.
|
||||
- 최신 요청만 반영하도록 `latestRequestId` 패턴을 사용한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkViewModel|fetchFirstPage|fetchNextPageIfNeeded|isLoadingNextPage|latestRequestId|report\\(|deleteSelectedFanTalk|ReportType\\.CHEERS|modifyCheers|hasNext" SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkViewModel.swift`
|
||||
|
||||
### Phase 3: Figma 섹션별 UI
|
||||
|
||||
- [x] **Task 3.1: 팬Talk count bar 구현**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkCountBar.swift`
|
||||
- 작업 내용:
|
||||
- Figma sort-bar 기준으로 높이 52pt, 좌측 `전체 {fanTalkCount}`만 표시한다.
|
||||
- 정렬 버튼/아이콘/action은 포함하지 않는다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkCountBar|fanTalkCount|전체|frame\\(height: 52\\)|ic_new_sort|selectedSort" SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkCountBar.swift`
|
||||
- 기대 결과: count bar 구현이 확인되고 정렬 관련 문자열은 없어야 한다.
|
||||
|
||||
- [x] **Task 3.2: 팬Talk item 구현**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift`
|
||||
- 작업 내용:
|
||||
- 작성자 프로필 42pt, 닉네임/상대 시간, 본문 50pt inset, 하단 separator를 구현한다.
|
||||
- 답글이 있으면 좌측 라인과 답글 카드를 표시한다.
|
||||
- `isMine || isOwnCreatorChannel`이면 `ic_new_more` 버튼을 표시한다.
|
||||
- 그 외에는 `신고` 텍스트 버튼을 표시한다.
|
||||
- 프로필 이미지와 답글 프로필 이미지는 Kingfisher 기반 기존 컴포넌트를 사용한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkListItem|writerProfileImageUrl|writerNickname|createdAtUtc|creatorReplies|ic_new_more|신고|Color\\.gray800|Color\\.gray900|42|20" SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkListItem.swift`
|
||||
|
||||
- [x] **Task 3.3: 더보기 anchored context popup 구현**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift`
|
||||
- 작업 내용:
|
||||
- Figma `666:36114` 기준으로 border `Color.gray700`, background `Color.gray900`, radius 14pt를 사용한다.
|
||||
- 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시한다.
|
||||
- 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다.
|
||||
- tap action은 부모에서 주입한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkActionPopup|수정하기|삭제하기|Color\\.gray700|Color\\.gray900|cornerRadius\\(14\\)|onTapEdit|onTapDelete" SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkActionPopup.swift`
|
||||
|
||||
- [x] **Task 3.4: Empty view 구현**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
|
||||
- 작업 내용:
|
||||
- Figma `290:9001` 기준으로 `아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!` 문구를 표시한다.
|
||||
- plus 아이콘과 `응원 남기기` 텍스트가 있는 capsule button을 표시한다.
|
||||
- 내 채널이면 button을 숨긴다.
|
||||
- tap action은 부모에서 주입한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkEmptyView|아직 응원이 없습니다|처음으로 크리에이터를 응원해 보세요|응원 남기기|showsWriteButton|onTapWrite" SodaLive/Sources/V2/CreatorChannel/FanTalk/Components/CreatorChannelFanTalkEmptyView.swift`
|
||||
|
||||
- [x] **Task 3.5: 공용 icon floating button 추출**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
|
||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
|
||||
- 작업 내용:
|
||||
- `CreatorChannelFloatingActionMenu` 내부 private `floatingButton`과 같은 66pt 원형 버튼, 38pt 아이콘, background color, plain button style을 공용 컴포넌트로 분리한다.
|
||||
- 공용 컴포넌트는 `imageName`, `backgroundColor`, `accessibilityLabel`, `action`을 받는다.
|
||||
- 기존 `CreatorChannelFloatingActionMenu`는 private `floatingButton` 구현 대신 공용 컴포넌트를 사용하도록 변경한다.
|
||||
- 팬Talk 우측하단 글쓰기 plus button은 이 공용 컴포넌트에 `imageName: "ic_plus_no_bg"`, `backgroundColor: Color.soda400`를 전달해 사용한다.
|
||||
- 글쓰기 plus button은 텍스트를 표시하지 않는다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "struct CreatorChannelFloatingIconButton|imageName|backgroundColor|accessibilityLabel|frame\\(width: 38, height: 38\\)|frame\\(width: 66, height: 66\\)|clipShape\\(Circle\\(\\)\\)" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
|
||||
- 실행 명령: `rg "CreatorChannelFloatingIconButton|ic_plus_no_bg|ic_new_upload_community_post|ic_new_upload_audio|ic_new_create_live|ic_new_x_black" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
- 실행 명령: `rg "Text\\(" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingIconButton.swift`
|
||||
- 기대 결과: 공용 icon-only floating button과 기존 floating action menu 재사용이 확인되고, 공용 컴포넌트에는 텍스트가 없어야 한다.
|
||||
|
||||
- [x] **Task 3.6: 팬Talk 탭 본문 구현**
|
||||
- 대상 파일:
|
||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift`
|
||||
- 작업 내용:
|
||||
- count bar, empty view, fanTalk list를 배치한다.
|
||||
- 첫 진입 시 `fetchFirstPage(creatorId:)`를 호출한다.
|
||||
- 마지막 fanTalk item 노출 시 `fetchNextPageIfNeeded`를 호출한다.
|
||||
- `fanTalkCount == 0`이면 `CreatorChannelFanTalkEmptyView`를 표시한다.
|
||||
- 내 채널인 경우 empty view 내부 글쓰기 버튼을 숨긴다.
|
||||
- `CheersReportDialogView`와 삭제 confirm dialog를 표시한다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkTabView|CreatorChannelFanTalkCountBar|CreatorChannelFanTalkEmptyView|CreatorChannelFanTalkListItem|fetchFirstPage|fetchNextPageIfNeeded|CheersReportDialogView|SodaDialog|onTapWrite" SodaLive/Sources/V2/CreatorChannel/FanTalk/CreatorChannelFanTalkTabView.swift`
|
||||
|
||||
### Phase 4: CreatorChannelView 연결
|
||||
|
||||
- [x] **Task 4.1: 팬Talk 탭 콘텐츠 연결**
|
||||
- 대상 파일:
|
||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
- 작업 내용:
|
||||
- `@StateObject`로 팬Talk 탭 ViewModel을 보유한다.
|
||||
- `selectedTabContent`에서 `.fanTalk`일 때 `CreatorChannelFanTalkTabView`를 표시한다.
|
||||
- 우측하단 글쓰기 floating button은 `selectedTab == .fanTalk`, 내 채널이 아니고 `fanTalkCount > 0`일 때만 `CreatorChannelFloatingIconButton`으로 표시한다.
|
||||
- 글쓰기 action은 신규 글쓰기 페이지 구현 전까지 no-op placeholder로 둔다.
|
||||
- 검증 기준:
|
||||
- 실행 명령: `rg "CreatorChannelFanTalkViewModel|CreatorChannelFanTalkTabView|viewModel\\.selectedTab == \\.fanTalk|CreatorChannelFloatingIconButton|showFanTalkWrite|fanTalkCount|ic_plus_no_bg" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||
|
||||
### Phase 5: 프로젝트 등록과 검증
|
||||
|
||||
- [x] **Task 5.1: 프로젝트 파일 등록과 정적 검증**
|
||||
- 대상 파일:
|
||||
- 확인/수정: `SodaLive.xcodeproj/project.pbxproj`
|
||||
- 검증 기준:
|
||||
- 실행 명령: `git diff --check -- docs/20260704_크리에이터_채널_팬Talk_탭 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:9139`, `290:9145`, `290:9147`, `666:36114`, `290:9001`을 확인했다.
|
||||
- 2026-07-04: `rg`와 `sed`로 기존 `CreatorChannelView`, 후원 탭 구현, `UserProfileFanTalkViewModel`, `UserProfileFanTalkCheersItemView`, `UserProfileView` 신고/삭제 흐름을 확인했다.
|
||||
- 2026-07-04: 사용자 피드백에 따라 우측하단 plus button은 `CreatorChannelFloatingActionMenu`의 private `floatingButton`과 동일 UI로 판단했다. 중복 컴포넌트를 만들지 않고 `CreatorChannelFloatingIconButton` 공용 컴포넌트로 추출해 `CreatorChannelFloatingActionMenu`와 팬Talk 글쓰기 floating button에서 재사용하는 방향으로 계획을 보완했다.
|
||||
- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -list`로 `SodaLive`, `SodaLive-dev` 앱 스킴은 확인했지만 테스트 action 구성은 확인되지 않았다.
|
||||
- 2026-07-04: `rg` 검증으로 팬Talk 탭 response model, API endpoint, Repository, ViewModel, count bar, list item, action popup, empty view, tab view, `CreatorChannelView` 연결 문자열을 확인했다.
|
||||
- 2026-07-04: `plutil -lint SodaLive.xcodeproj/project.pbxproj` 결과 `OK`를 확인했다.
|
||||
- 2026-07-04: `git diff --check -- docs/20260704_크리에이터_채널_팬Talk_탭 SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj`를 실행해 공백 오류가 없음을 확인했다.
|
||||
- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 결과 `BUILD SUCCEEDED`를 확인했다.
|
||||
- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`는 `Scheme SodaLive-dev is not currently configured for the test action.`으로 실패했다. 현재 범위에서는 새 테스트 타깃을 추가하지 않고 빌드/정적 검증으로 대체했다.
|
||||
182
docs/20260704_크리에이터_채널_팬Talk_탭/prd.md
Normal file
182
docs/20260704_크리에이터_채널_팬Talk_탭/prd.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# PRD: 크리에이터 채널 팬Talk 탭
|
||||
|
||||
## 1. Overview
|
||||
크리에이터 채널 공통 shell의 `팬Talk` 탭에서 팬이 남긴 응원글 목록과 크리에이터 답글을 제공한다. 상단 title bar, header, sticky tab-bar는 기존 크리에이터 채널 홈/라이브/오디오/시리즈/후원 구현을 재사용하고, tab-bar 아래 콘텐츠만 팬Talk 탭 전용 API 응답으로 구성한다.
|
||||
|
||||
API는 `GET /api/v2/creator-channels/{creatorId}/fan-talks`를 사용한다. `creatorId`는 path variable이며, query parameter는 `page`, `size`를 사용한다. 기본값은 `page=0`, `size=20`이다.
|
||||
|
||||
Figma 참조:
|
||||
- 전체 화면: `290:9139`, `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-9139&m=dev`
|
||||
- 아이템 뷰(답글 없음): `290:9145`, `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-9145&m=dev`
|
||||
- 아이템 뷰(답글 있음): `290:9147`, `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-9147&m=dev`
|
||||
- 더보기 anchored context popup: `666:36114`, `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=666-36114&m=dev`
|
||||
- Empty: `290:9001`, `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-9001&m=dev`
|
||||
|
||||
## 2. Problem
|
||||
- 현재 `CreatorChannelTab.fanTalk`은 placeholder로 표시되어 팬Talk 목록을 확인할 수 없다.
|
||||
- 기존 홈 탭에는 최신 팬Talk 요약만 있고, 팬Talk 탭 전용 목록 API, 페이징, 답글 표시, 신고/삭제 액션이 없다.
|
||||
- 기존 프로필 FanTalk 구현은 구 API와 구 UI 구조를 사용하므로 V2 크리에이터 채널 탭에 그대로 붙이기 어렵다.
|
||||
|
||||
## 3. Goals
|
||||
- `CreatorChannelTab.fanTalk` 선택 시 팬Talk 탭 API를 호출하고 응답 데이터로 화면을 구성한다.
|
||||
- 기본 query는 `page=0`, `size=20`이다.
|
||||
- sort-bar에는 정렬 UI 없이 `전체 {fanTalkCount}`만 표시한다.
|
||||
- 팬Talk item은 Figma `290:9145`, `290:9147` 기준으로 작성자 프로필, 닉네임, 상대 시간, 본문, 답글 목록을 표시한다.
|
||||
- 답글이 있으면 Figma처럼 좌측 라인과 답글 카드 영역을 표시한다.
|
||||
- 다른 유저의 글에는 `신고` 버튼만 표시하고, 기존 `UserProfileView`의 응원글 신고 기능과 동일하게 `CheersReportDialogView` 및 `ReportType.CHEERS` 흐름을 사용한다.
|
||||
- 내가 쓴 글 또는 내 채널의 타인 글에는 우측 액션으로 `ic_new_more` 더보기 버튼을 표시한다.
|
||||
- 내가 쓴 글의 더보기 메뉴에는 `수정하기`, `삭제하기`를 표시한다.
|
||||
- 내가 쓴 글이 아니지만 내 채널인 경우 더보기 메뉴에는 `삭제하기`만 표시한다.
|
||||
- `삭제하기`는 기존 FanTalk에서 사용하던 삭제 API와 동일하게 `ExplorerRepository.modifyCheers(..., isActive: false)`를 사용한다.
|
||||
- `fanTalkCount == 0`이면 Figma empty view(`290:9001`)를 표시한다.
|
||||
- 내 채널인 경우 우측하단 글쓰기 floating button과 empty view 내부 `응원 남기기` 버튼을 모두 표시하지 않는다.
|
||||
|
||||
## 4. Non-Goals
|
||||
- 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다.
|
||||
- 신규 글쓰기/수정 페이지를 이번 작업에서 구현하지 않는다.
|
||||
- 팬Talk 작성/수정 mutation API를 새로 만들지 않는다.
|
||||
- 신고 API나 신고 dialog를 새로 만들지 않는다.
|
||||
- 삭제 API를 새로 만들지 않고 기존 FanTalk 삭제 흐름을 재사용한다.
|
||||
- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
|
||||
- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다.
|
||||
|
||||
## 5. Core Requirements
|
||||
|
||||
### 5.1 API
|
||||
- Method: `GET`
|
||||
- Path: `/api/v2/creator-channels/{creatorId}/fan-talks`
|
||||
- Path parameter:
|
||||
- `creatorId`
|
||||
- Query parameters:
|
||||
- `page`: 기본값 `0`
|
||||
- `size`: 기본값 `20`
|
||||
- 응답 래퍼는 기존 패턴대로 `ApiResponse<CreatorChannelFanTalkTabResponse>`로 디코딩한다.
|
||||
|
||||
```kotlin
|
||||
data class CreatorChannelFanTalkTabResponse(
|
||||
val fanTalkCount: Int,
|
||||
val fanTalks: List<CreatorChannelFanTalkResponse>,
|
||||
val page: Int,
|
||||
val size: Int,
|
||||
@JsonProperty("hasNext")
|
||||
val hasNext: Boolean
|
||||
)
|
||||
|
||||
data class CreatorChannelFanTalkResponse(
|
||||
val fanTalkId: Long,
|
||||
val writerId: Long,
|
||||
val writerNickname: String,
|
||||
val writerProfileImageUrl: String,
|
||||
val content: String,
|
||||
val createdAtUtc: String,
|
||||
val creatorReplies: List<CreatorChannelFanTalkReplyResponse>
|
||||
)
|
||||
|
||||
data class CreatorChannelFanTalkReplyResponse(
|
||||
val fanTalkId: Long,
|
||||
val writerId: Long,
|
||||
val writerNickname: String,
|
||||
val writerProfileImageUrl: String,
|
||||
val content: String,
|
||||
val createdAtUtc: String
|
||||
)
|
||||
```
|
||||
|
||||
- Kotlin `Long`은 기존 V2 모델 관례대로 Swift `Int`로 선언한다.
|
||||
- `CreatorChannelFanTalkResponse`는 홈 탭 모델과 이름이 충돌하므로 홈 탭 모델명을 변경하거나 팬Talk 탭 모델명을 분리해 컴파일 충돌을 피한다.
|
||||
|
||||
### 5.2 Count bar
|
||||
- 팬Talk 탭 sort-bar는 정렬이 없고 전체 개수만 포함한다.
|
||||
- 좌측에 `전체`과 `fanTalkCount`를 표시한다.
|
||||
- 우측 정렬 버튼, 정렬 아이콘, 정렬 context popup, BottomSheet는 표시하지 않는다.
|
||||
- 후원 탭의 `CreatorChannelDonationCountBar`와 같은 시각 규칙을 사용하되 FanTalk 전용 이름으로 분리하거나 공용화한다.
|
||||
|
||||
### 5.3 FanTalk item
|
||||
- item 전체 배경은 `Color.black`, 하단 separator는 `Color.gray800` 기준을 사용한다.
|
||||
- item padding은 Figma 기준 14pt를 사용한다.
|
||||
- 작성자 프로필 이미지는 42pt 원형으로 표시한다.
|
||||
- 작성자 닉네임은 14pt medium, 작성 시간은 14pt regular와 `Color.gray500`으로 표시한다.
|
||||
- 본문은 16pt regular, `Color.white`, 좌측 50pt inset으로 표시한다.
|
||||
- 본문과 답글은 서버 응답의 문자열을 그대로 표시한다.
|
||||
- 작성 시간은 기존 프로젝트의 상대 시간 표시 helper를 우선 재사용한다.
|
||||
- 이미지 로딩은 기존 V2 관례대로 `DownsampledKFImage` 또는 Kingfisher 기반 컴포넌트를 사용한다.
|
||||
|
||||
### 5.4 Creator replies
|
||||
- `creatorReplies`가 비어 있으면 답글 영역을 표시하지 않는다.
|
||||
- `creatorReplies`가 있으면 각 답글을 Figma `290:9147` 기준으로 표시한다.
|
||||
- 답글 카드 배경은 `Color.gray900`, radius는 14pt, padding은 14pt를 사용한다.
|
||||
- 답글 작성자 프로필은 20pt 원형으로 표시하고 닉네임과 상대 시간을 한 줄에 표시한다.
|
||||
- 답글 본문은 16pt regular, `Color.white`로 표시한다.
|
||||
- 답글 영역 왼쪽에는 Figma처럼 세로 라인을 표시한다.
|
||||
|
||||
### 5.5 Item action rules
|
||||
- 현재 로그인 사용자의 id는 기존 관례대로 `UserDefaults.int(forKey: .userId)`를 사용한다.
|
||||
- `isMine = fanTalk.writerId == currentUserId`.
|
||||
- `isOwnCreatorChannel`은 `CreatorChannelView`에서 계산한 값을 주입한다.
|
||||
- `isMine || isOwnCreatorChannel`이면 우측에 `ic_new_more` 더보기 버튼을 표시한다.
|
||||
- `!isMine && !isOwnCreatorChannel`이면 우측에 `신고` 버튼만 표시한다.
|
||||
- 더보기 버튼을 터치하면 Figma `666:36114` 기준 anchored context popup을 표시한다.
|
||||
- 내가 쓴 글이면 `수정하기`, `삭제하기`를 표시한다.
|
||||
- 내가 쓴 글이 아니지만 내 채널이면 `삭제하기`만 표시한다.
|
||||
- `수정하기`와 글쓰기 floating button은 후속 신규 글쓰기 페이지 구현 전까지 비어 있는 placeholder action으로 두되, 코드상 TODO가 드러나게 유지한다.
|
||||
- `삭제하기`는 확인 dialog 후 기존 삭제 API를 호출한다.
|
||||
|
||||
### 5.6 Report and delete
|
||||
- 신고 버튼을 터치하면 기존 크리에이터 페이지의 응원글 신고와 동일하게 `CheersReportDialogView`를 표시한다.
|
||||
- 신고 confirm 시 `ReportRequest(type: .CHEERS, cheersId: fanTalkId, reason: reason)` 흐름을 사용한다.
|
||||
- 삭제 confirm dialog는 기존 `I18n.MemberChannel.cheersDeleteTitle`, `I18n.Common.confirmDeleteQuestion`, `I18n.Common.delete`, `I18n.Common.cancel`을 재사용한다.
|
||||
- 삭제 API는 기존 FanTalk 삭제 흐름과 동일하게 `ExplorerRepository.modifyCheers(cheersId: fanTalkId, content: nil, isActive: false)`를 호출한다.
|
||||
- 삭제 성공 후 팬Talk 탭 첫 페이지를 다시 조회한다.
|
||||
|
||||
### 5.7 Empty and writing button
|
||||
- `fanTalkCount == 0`이면 Figma `290:9001` 기준 empty view를 표시한다.
|
||||
- Empty 문구는 `아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!`를 표시한다.
|
||||
- Empty view에는 `+` 아이콘과 `응원 남기기` 텍스트가 있는 capsule button을 표시한다.
|
||||
- 단, 내 채널인 경우 empty view 내부 `응원 남기기` 버튼을 표시하지 않는다.
|
||||
- `fanTalkCount > 0`이면 홈 탭 plus button 위치에 글쓰기 floating button을 표시한다.
|
||||
- 단, 내 채널인 경우 글쓰기 floating button을 표시하지 않는다.
|
||||
- 글쓰기 floating button은 `CreatorChannelFloatingActionMenu` 내부 `floatingButton`과 동일한 66pt 원형 버튼, 38pt 아이콘, `Color.soda400` 배경을 사용한다.
|
||||
- 동일 UI를 중복 구현하지 않기 위해 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 공용 icon floating button을 분리하고, `CreatorChannelFloatingActionMenu`와 팬Talk 글쓰기 floating button이 이를 재사용한다.
|
||||
- 글쓰기 floating button에는 plus 아이콘만 표시하고 텍스트는 표시하지 않는다.
|
||||
- 글쓰기 버튼 tap action은 신규 글쓰기 페이지 구현 후 연결할 수 있도록 부모 closure로 분리한다.
|
||||
|
||||
### 5.8 Pagination
|
||||
- 첫 진입 시 `page=0`, `size=20`으로 조회한다.
|
||||
- 다음 페이지 로딩 중 중복 호출을 막는다.
|
||||
- 마지막 item이 화면에 나타났고 `hasNext == true`이면 `page + 1`을 조회한다.
|
||||
- 다음 페이지 성공 시 기존 `fanTalks` 뒤에 append한다.
|
||||
- 첫 페이지 재조회 시 기존 목록을 교체한다.
|
||||
|
||||
## 6. Success Criteria
|
||||
- 팬Talk 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/fan-talks?page=0&size=20`가 호출된다.
|
||||
- count bar에는 `전체 {fanTalkCount}`만 표시되고 정렬 버튼/아이콘/popup은 표시되지 않는다.
|
||||
- 답글이 없는 item은 Figma `290:9145`와 같은 정보 구조로 표시된다.
|
||||
- 답글이 있는 item은 Figma `290:9147`처럼 좌측 라인과 답글 카드가 표시된다.
|
||||
- 타인의 글이면서 내 채널이 아니면 `신고` 버튼만 표시되고 신고 confirm 시 기존 응원글 신고 API가 호출된다.
|
||||
- 내가 쓴 글은 `ic_new_more` 버튼과 `수정하기`, `삭제하기` 메뉴를 표시한다.
|
||||
- 내가 쓴 글이 아니지만 내 채널이면 `ic_new_more` 버튼과 `삭제하기` 메뉴만 표시한다.
|
||||
- 삭제 confirm 시 기존 FanTalk 삭제 API가 호출되고 성공 후 첫 페이지가 갱신된다.
|
||||
- `fanTalkCount == 0`이면 Figma `290:9001` empty view가 표시된다.
|
||||
- 내 채널인 경우 empty view 내부 `응원 남기기` 버튼과 우측하단 글쓰기 floating button이 모두 표시되지 않는다.
|
||||
- `fanTalkCount > 0`이고 내 채널이 아니면 우측하단 글쓰기 floating button이 표시된다.
|
||||
- 글쓰기/수정하기 tap은 신규 글쓰기 페이지 구현 전까지 앱 crash 없이 no-op placeholder로 동작한다.
|
||||
|
||||
## 7. Technical Constraints
|
||||
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
|
||||
- 팬Talk 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/FanTalk/**` 아래에 둔다.
|
||||
- 우측하단 icon floating button은 팬Talk 전용 컴포넌트로 중복 생성하지 않고 `CreatorChannelFloatingActionMenu`의 private `floatingButton` 패턴을 공용 컴포넌트로 추출해 재사용한다.
|
||||
- 삭제 API 재사용을 위해 기존 `ExplorerRepository.modifyCheers`를 호출할 수 있다.
|
||||
- 신고 기능 재사용을 위해 기존 `ReportRepository`, `ReportRequest`, `CheersReportDialogView`를 사용한다.
|
||||
- 신규 문구가 필요하면 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
|
||||
- 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다.
|
||||
|
||||
## 8. Assumptions
|
||||
- 신규 글쓰기/수정 페이지는 후속 작업에서 구현하며, 이번 작업에서는 버튼 표시와 action closure만 준비한다.
|
||||
- 신규 API의 `fanTalkId`는 기존 신고/삭제 API가 요구하는 `cheersId`와 동일한 식별자로 사용할 수 있다.
|
||||
|
||||
## 9. Open Questions
|
||||
- 해당 없음
|
||||
|
||||
## 10. Verification Notes
|
||||
- 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 `290:9139`, `290:9145`, `290:9147`, `666:36114`, `290:9001`을 확인했다.
|
||||
- 2026-07-04: `CreatorChannelView`, 후원 탭 구현, 기존 `UserProfileFanTalkViewModel`, `UserProfileFanTalkCheersItemView`, `UserProfileView`의 신고/삭제 흐름을 확인했다.
|
||||
Reference in New Issue
Block a user