Files
sodalive-android/docs/20260622_FanTalk_탭/plan-task.md

667 lines
57 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.

# 크리에이터 채널 FanTalk 탭 구현 계획/TASK
> **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.
**Goal:** `GET /api/v2/creator-channels/{creatorId}/fan-talks` 응답을 기반으로 크리에이터 채널의 `FanTalk` 탭에 전체 개수, FanTalk 목록, 크리에이터 답글 1개, 권한별 신고/더보기/삭제, empty 상태, 표시 전용 글쓰기 버튼과 pagination을 표시한다.
**Architecture:** 기존 `CreatorChannelActivity``ViewPager2`/`CreatorChannelPagerAdapter` 구조를 유지하고, `CreatorChannelTab.FanTalk`의 placeholder를 신규 `CreatorChannelFanTalkFragment`로 교체한다. FanTalk 탭 전용 Fragment/ViewModel/DTO/mapper/adapter/popup은 `kr.co.vividnext.sodalive.v2.creator.channel.fantalk` 하위에 두되, API/Repository는 기존 채널 공통 `CreatorChannelApi`/`CreatorChannelRepository`에 endpoint와 FanTalk 삭제/신고 위임만 추가한다. 날짜 표시는 v2 공통 `UtcRelativeTimeTextFormatter`를 사용하고, 신고/삭제는 기존 레거시 `CheersReportDialog`, `ReportRepository`, `ExplorerRepository.modifyCheers()` 계약을 재사용한다.
**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test.
---
## 전제와 성공 기준
- PRD: `docs/20260622_FanTalk_탭/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`
- 기존 v2 날짜 포맷:
- `app/src/main/java/kr/co/vividnext/sodalive/common/RelativeTimeFormatter.kt`
- `UtcRelativeTimeTextFormatter`
- `AndroidUtcRelativeTimeTextFormatter`
- 기존 FanTalk 홈 카드 참조:
- `app/src/main/res/layout/item_creator_channel_home_fantalk.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelFanTalkCardView.kt`
- 기존 신고/삭제 흐름 참조:
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewModel.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/report/CheersReportDialog.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/report/ReportRequest.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerRepository.kt`
- Figma:
- 전체 목록: `290:9139`
- empty 상태: `290:9000`
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/fan-talks`이다.
- 첫 페이지 `page``0`, 기본 `size``20`이다.
- query parameter는 `page`, `size`만 전달한다.
- 정렬 query parameter와 sort popup은 FanTalk 탭에서 사용하지 않는다.
- `creatorReplies`는 응답 순서 기준 첫 번째 1개만 표시한다.
- 내가 쓴 글이면 더보기 popup에 `수정하기`, `삭제하기`를 표시한다.
- 내 채널에서 타인이 작성한 글이면 더보기 popup에 `삭제하기`만 표시한다.
- `수정하기`는 표시만 하고 화면/API를 연결하지 않는다.
- `삭제하기``PutModifyCheersRequest(cheersId = fanTalkId, isActive = false)``ExplorerRepository.modifyCheers()`로 요청한다.
- 신고는 `ReportRequest(type = ReportType.CHEERS, reason = reason, cheersId = fanTalkId)``ReportRepository.report()`로 요청한다.
- 목록 content 상태에서는 우측 하단 floating 글쓰기 버튼을 표시하되 클릭 시 아무 화면/API도 연결하지 않는다.
- empty 상태에서는 Sort-bar와 우측 하단 floating 버튼을 숨기고 중앙 empty 문구와 `응원 남기기` capsule button을 표시한다.
- empty 상태의 `응원 남기기` button도 클릭 시 아무 화면/API도 연결하지 않는다.
- empty 문구는 다음 다국어 문자열 리소스로 제공한다.
- 한국어: `아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!`
- 영어: `No cheers yet.\nBe the first to cheer for the creator!`
- 일본어: `まだ応援がありません。\n最初にクリエイターを応援してみましょう`
- 구현 완료 후 최소 다음 명령을 실행한다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"`
- `./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, 신고/삭제 재사용 경계 확인이 중심이며 Figma는 PRD 기준만 확인한다.
- Phase 2: Figma 참조 불필요
- API/DTO/Repository/ViewModel 상태 모델은 서버 계약과 기존 Community/Series 탭 패턴을 따른다.
- Phase 3: 제한 참조
- mapper와 action state는 PRD, 기존 FanTalk adapter, v2 날짜 formatter 정책을 함께 확인한다.
- Phase 4: 필수 참조
- Sort-bar, list item, 답글 card, 권한별 우측 action, popup, floating button은 Figma `290:9139` 기준으로 구현한다.
- Phase 5: 필수 참조
- empty 상태는 Figma `290:9000` 기준으로 구현한다.
- Phase 6: 제한 참조
- 탭 연결, pagination, 신고/삭제 dialog/API 연결은 기존 코드 패턴 중심으로 검증한다.
- Phase 7: 필수 참조
- 최종 수동 화면 검증은 PRD의 Figma 노드와 실제 화면을 대조한다.
---
## 파일 구조
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
- `CreatorChannelTab.FanTalk`을 신규 `CreatorChannelFanTalkFragment`로 연결한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- `CreatorChannelFanTalkFragment.Host` 구현, FanTalk 탭 선택 시 최초 로드, pagination trigger, ViewPager 높이 갱신, 신고/삭제 확인 dialog 표시, 삭제 성공 후 refresh 연결을 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt`
- FanTalk 탭 endpoint를 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
- FanTalk 목록 조회, 신고, 삭제 repository method를 추가한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/data/CreatorChannelFanTalkTabResponse.kt`
- `CreatorChannelFanTalkTabResponse`, `CreatorChannelFanTalkResponse`, `CreatorChannelFanTalkReplyResponse`를 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModel.kt`
- 최초 조회, retry, refresh, pagination, 신고, 삭제, loading/error/empty/content 상태를 관리한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkUiModels.kt`
- FanTalk item, reply, 권한별 action, 화면 상태 UI model을 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkMappers.kt`
- DTO를 UI model로 변환하고 `creatorReplies.firstOrNull()`, 날짜 포맷, 권한별 action 표시를 결정한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt`
- FanTalk 탭 UI, adapter, popup, retry, pagination error toast, report/delete callback, host callback 연결을 담당한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkAdapter.kt`
- FanTalk 목록 RecyclerView adapter를 담당한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkMorePopup.kt`
- `수정하기`, `삭제하기` popup 표시와 click callback을 담당한다.
- 생성: `app/src/main/res/layout/fragment_creator_channel_fantalk.xml`
- Sort-bar 없는 count bar, RecyclerView, empty, error/retry, floating write button 영역을 포함한다.
- 생성: `app/src/main/res/layout/item_creator_channel_fantalk.xml`
- 원글, 권한별 우측 action, 크리에이터 답글 card 1개를 구현한다.
- 생성: `app/src/main/res/layout/view_creator_channel_fantalk_more_popup.xml`
- 권한별 `수정하기`/`삭제하기` popup menu layout을 구현한다.
- 생성 가능: `app/src/main/res/drawable/bg_creator_channel_fantalk_reply.xml`
- 답글 card 배경이 기존 drawable로 대응되지 않을 때만 추가한다.
- 생성 가능: `app/src/main/res/drawable/bg_creator_channel_fantalk_empty_button.xml`
- empty capsule button 배경이 기존 home FanTalk/donation drawable로 대응되지 않을 때만 추가한다.
- 수정: `app/src/main/res/values/strings.xml`
- FanTalk 탭 empty/error/retry/action/report/delete/count 문구를 추가하거나 기존 문자열을 재사용한다.
- 수정: `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`
- `CreatorChannelFanTalkViewModel` binding을 추가한다.
- 테스트 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkMapperTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModelTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkPaginationTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkActionTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.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: 기존 구조 확인과 작업 경계 고정
- [x] **Task 1.1: FanTalk 탭 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.FanTalk`이 현재 `CreatorChannelPlaceholderFragment`로 연결되는지 확인한다.
- 신규 `CreatorChannelFanTalkFragment.newInstance(creatorId)`로 교체할 위치를 고정한다.
- 검증:
- `rg -n "CreatorChannelTab\\.FanTalk|CreatorChannelPlaceholderFragment|createFragment" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel`
- 기대 결과: FanTalk placeholder와 관련 테스트 갱신 지점이 확인된다.
- 검증 기록:
- 2026-06-22: `rg -n "CreatorChannelTab\\.FanTalk|CreatorChannelPlaceholderFragment|createFragment" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel` 실행.
- `CreatorChannelPagerAdapter.kt:20-29`에서 Home/Live/Audio/Series/Community만 실제 Fragment로 분기하고 `FanTalk`은 현재 `else -> CreatorChannelPlaceholderFragment.newInstance(tab)` 경로에 포함됨을 확인했다.
- `CreatorChannelPagerAdapterTest.kt:23-43`은 FanTalk을 포함한 나머지 탭 placeholder 기대값을 갖고 있으며, `CreatorChannelActivitySourceTest.kt:419`는 FanTalk 분기 부재를 검증하므로 Phase 5에서 함께 갱신해야 한다.
- [x] **Task 1.2: 기존 v2 목록 탭 패턴 확인**
- 확인:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.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/series/CreatorChannelSeriesViewModel.kt`
- 작업:
- FanTalk ViewModel도 `FIRST_PAGE = 0`, `DEFAULT_PAGE_SIZE = 20`, `requestGeneration`, `paginationErrorMessage`, `consumePaginationErrorMessage()` 패턴을 따른다.
- Fragment도 `onCreatorChannelFanTalkTabSelected()`, `onCreatorChannelFanTalkScrolledToBottom()`, `onCreatorChannelFanTalkRefreshRequested()` entry를 제공한다.
- 검증:
- `rg -n "requestGeneration|paginationErrorMessage|consumePaginationErrorMessage|ScrolledToBottom|RefreshRequested" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel`
- 기대 결과: Community/Series의 pagination과 refresh 패턴이 확인된다.
- 검증 기록:
- 2026-06-22: `rg -n "requestGeneration|paginationErrorMessage|consumePaginationErrorMessage|ScrolledToBottom|RefreshRequested" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel` 실행.
- `CreatorChannelCommunityViewModel.kt:30-38`, `CreatorChannelCommunityViewModel.kt:61-92`, `CreatorChannelCommunityViewModel.kt:94-168`에서 중복 최초 조회 방지, `FIRST_PAGE = 0`, `DEFAULT_PAGE_SIZE = 20`, `requestGeneration`, pagination error 유지/consume 패턴을 확인했다.
- `CreatorChannelSeriesViewModel.kt:30-38`, `CreatorChannelSeriesViewModel.kt:54-85`, `CreatorChannelSeriesViewModel.kt:87-157`에서도 동일한 pagination/requestGeneration 패턴을 확인했다.
- `CreatorChannelCommunityFragment.kt:91-103`, `CreatorChannelCommunityFragment.kt:154-156`에서 탭 선택/하단 스크롤/refresh entry와 pagination toast consume 패턴을 확인했고, `CreatorChannelActivity.kt:820-825`의 현재 탭 하단 스크롤 dispatcher에 FanTalk 분기 추가 지점이 있음을 확인했다.
- [x] **Task 1.3: 기존 신고/삭제 재사용 경계 확인**
- 확인:
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewActivity.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/fantalk/UserProfileFantalkAllViewModel.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/report/CheersReportDialog.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/cheers/PutModifyCheersRequest.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerRepository.kt`
- 작업:
- 신고 dialog는 `CheersReportDialog(requireActivity(), layoutInflater)`를 사용한다.
- 신고 사유가 비어 있으면 `screen_user_profile_fantalk_report_reason_required` toast를 표시한다.
- 신고 요청은 `ReportType.CHEERS`, `cheersId = fanTalkId`로 고정한다.
- 삭제 요청은 `PutModifyCheersRequest(cheersId = fanTalkId, isActive = false)`로 고정한다.
- 레거시 파일은 직접 수정하지 않는다.
- 검증:
- `rg -n "CheersReportDialog|ReportType\\.CHEERS|PutModifyCheersRequest|isActive = false|modifyCheers" app/src/main/java/kr/co/vividnext/sodalive/explorer app/src/main/java/kr/co/vividnext/sodalive/report`
- 기대 결과: 기존 신고/삭제 계약이 확인된다.
- 검증 기록:
- 2026-06-22: `rg -n "CheersReportDialog|ReportType\\.CHEERS|PutModifyCheersRequest|isActive = false|modifyCheers" app/src/main/java/kr/co/vividnext/sodalive/explorer app/src/main/java/kr/co/vividnext/sodalive/report` 실행.
- `UserProfileFantalkAllViewActivity.kt:111-128`에서 삭제 확인 후 `viewModel.modifyCheers(..., isActive = false)` 호출 패턴을 확인했다.
- `UserProfileFantalkAllViewActivity.kt:200-214`에서 `CheersReportDialog(this, layoutInflater)` 사용, blank reason 시 `screen_user_profile_fantalk_report_reason_required` toast, reason 존재 시 신고 요청 위임 패턴을 확인했다.
- `UserProfileFantalkAllViewModel.kt:85-119`에서 `ReportRequest(ReportType.CHEERS, reason, cheersId = cheersId)``ReportRepository.report()`로 위임하는 계약을 확인했다.
- `UserProfileFantalkAllViewModel.kt:167-232`, `ExplorerRepository.kt:64-67`에서 `PutModifyCheersRequest(cheersId)``isActive`를 설정해 `ExplorerRepository.modifyCheers()`로 위임하는 계약을 확인했다. 레거시 파일은 수정하지 않고 신규 v2 경계에서 호출만 재사용한다.
- [x] **Task 1.4: FanTalk 문자열과 리소스 재사용 경계 확인**
- 확인:
- `app/src/main/res/values/strings.xml`
- `app/src/main/res/values-en/strings.xml`
- `app/src/main/res/values-ja/strings.xml`
- `app/src/main/res/drawable`
- `app/src/main/res/layout/item_creator_channel_home_fantalk.xml`
- 작업:
- `ic_new_more`, `ic_new_fantalk_plus`, `ic_placeholder_profile` 재사용 가능 여부를 확인한다.
- empty 문구는 신규 `creator_channel_fantalk_empty_message`로 추가한다.
- button label은 기존 `creator_channel_fantalk_support_action`을 재사용한다.
- `수정하기`, `삭제하기`, `신고`, `전체` 문구는 기존 문자열이 PRD 문구와 다르면 FanTalk 탭 전용 문자열을 추가한다.
- 검증:
- `rg -n "ic_new_more|ic_new_fantalk_plus|creator_channel_fantalk_support_action|screen_user_profile_cheer_edit|confirm_delete_title|report|creator_channel_.*all" app/src/main/res`
- 기대 결과: 재사용 가능한 리소스와 신규 문자열 추가 대상이 구분된다.
- 검증 기록:
- 2026-06-22: `rg -n "ic_new_more|ic_new_fantalk_plus|creator_channel_fantalk_support_action|screen_user_profile_cheer_edit|confirm_delete_title|report|creator_channel_.*all" app/src/main/res` 실행.
- `item_creator_channel_home_fantalk.xml:58-64`, `item_creator_channel_home_fantalk.xml:117-129`에서 `ic_placeholder_profile`, `ic_new_fantalk_plus`, `creator_channel_fantalk_support_action` 재사용 가능성을 확인했다.
- `activity_creator_channel.xml:235`, `item_creator_channel_community_list.xml:131`에서 `ic_new_more` 재사용 사례를 확인했다.
- `strings.xml:99-101`, `strings.xml:331`, `strings.xml:950`, `strings.xml:953` 및 en/ja 대응 문자열에서 `report_button`, `confirm_delete_title`, `creator_channel_fantalk_support_action`, `screen_user_profile_cheer_edit`, `screen_user_profile_fantalk_report_reason_required` 재사용 가능성을 확인했다.
- PRD의 탭 전용 empty/error/retry/count/action 문구 중 기존 문구와 다른 값은 Phase 4에서 `creator_channel_fantalk_empty_message`, `creator_channel_fantalk_error_message`, `creator_channel_fantalk_retry`, `creator_channel_fantalk_all_label`, `creator_channel_fantalk_report`, `creator_channel_fantalk_edit`, `creator_channel_fantalk_delete`로 신규 추가한다.
---
### Phase 2: API/DTO/Repository/ViewModel 계약 추가
- [x] **Task 2.1: FanTalk 탭 DTO 추가**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/data/CreatorChannelFanTalkTabResponse.kt`
- 작업:
- `@Keep`, `@SerializedName` 기반으로 `CreatorChannelFanTalkTabResponse`, `CreatorChannelFanTalkResponse`, `CreatorChannelFanTalkReplyResponse`를 추가한다.
- PRD의 서버 필드명과 동일하게 `fanTalkCount`, `fanTalks`, `page`, `size`, `hasNext`, `fanTalkId`, `writerId`, `writerNickname`, `writerProfileImageUrl`, `content`, `createdAtUtc`, `creatorReplies`를 정의한다.
- `@JsonProperty("hasNext")` 대신 프로젝트 기존 Gson 패턴인 `@SerializedName("hasNext")`를 사용한다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- 신규 DTO 추가 후 컴파일이 PASS한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelFanTalkTabResponse.kt``@Keep`/`@SerializedName` 기반 `CreatorChannelFanTalkTabResponse`, `CreatorChannelFanTalkResponse`, `CreatorChannelFanTalkReplyResponse`를 추가했다.
- `fanTalkCount`, `fanTalks`, `page`, `size`, `hasNext`, `fanTalkId`, `writerId`, `writerNickname`, `writerProfileImageUrl`, `content`, `createdAtUtc`, `creatorReplies` 필드가 PRD/계획 문서의 서버 필드명과 일치함을 확인했다.
- `./gradlew :app:compileDebugKotlin` 실행 결과 PASS.
- [x] **Task 2.2: FanTalk 탭 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}/fan-talks")` endpoint를 추가한다.
- query parameter `page`, `size`만 전달한다.
- Repository method는 `getFanTalks(creatorId, page, size, token)` 형태로 둔다.
- `CreatorChannelRepository.reportFanTalk(fanTalkId, reason, token)``ReportRequest(ReportType.CHEERS, reason, cheersId = fanTalkId)``reportRepository.report()`로 위임한다.
- `CreatorChannelRepository.deleteFanTalk(fanTalkId, token)``PutModifyCheersRequest(cheersId = fanTalkId, isActive = false)``explorerRepository.modifyCheers()`로 위임한다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- API/Repository 추가 후 기존 Koin graph와 충돌 없이 컴파일된다.
- 검증 기록:
- 2026-06-22: `CreatorChannelApi.getFanTalks()``@GET("/api/v2/creator-channels/{creatorId}/fan-talks")` endpoint를 추가하고 query parameter가 `page`, `size`만 전달되도록 구현했다.
- `CreatorChannelRepository.getFanTalks(creatorId, page, size, token)`, `reportFanTalk(fanTalkId, reason, token)`, `deleteFanTalk(fanTalkId, token)`를 추가했다. 신고는 `ReportRequest(ReportType.CHEERS, reason, cheersId = fanTalkId)`, 삭제는 `PutModifyCheersRequest(cheersId = fanTalkId, isActive = false)`로 기존 repository에 위임함을 확인했다.
- `./gradlew :app:compileDebugKotlin` 실행 결과 PASS.
- [x] **Task 2.3: ViewModel RED 테스트 작성**
- 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModelTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkPaginationTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkActionTest.kt`
- 테스트 케이스:
- 최초 로딩이 `page=0`, `size=20`으로 호출된다.
- `fanTalkCount == 0`이면 `Empty` 상태가 된다.
- 표시 가능한 `fanTalks`가 없으면 `Empty` 상태가 된다.
- `fanTalkCount > 0`이지만 첫 페이지 `fanTalks`가 비어 있으면 `Empty` 상태가 되고 count 값은 유지된다.
- `hasNext == true`일 때 다음 페이지는 마지막 응답의 `page + 1`로 요청한다.
- load-more 요청에는 `size=20`을 유지한다.
- loading 중 중복 load-more 요청은 무시된다.
- 다음 페이지 성공 시 기존 FanTalk 뒤에 append한다.
- 다음 페이지 실패 시 기존 목록은 유지하고 pagination error message만 설정한다.
- `consumePaginationErrorMessage()` 호출 후 pagination error message가 null이 된다.
- 신고 성공 시 toast message가 설정된다.
- 신고 실패 시 기존 content 상태는 유지하고 toast/error message가 설정된다.
- 삭제 성공 시 첫 페이지 refresh가 호출된다.
- 삭제 실패 시 기존 content 상태는 유지하고 toast/error message가 설정된다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkViewModelTest"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkPaginationTest"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkActionTest"`
- 기대 결과:
- production 구현 전 `CreatorChannelFanTalkViewModel` 미구현으로 RED 실패한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelFanTalkViewModelTest.kt`, `CreatorChannelFanTalkPaginationTest.kt`, `CreatorChannelFanTalkActionTest.kt`를 추가했다.
- production 구현 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkMapperTest"`를 실행했을 때 unit test compile 단계에서 `CreatorChannelFanTalkViewModel`, `CreatorChannelFanTalkTabResponse`, `getFanTalks`, `reportFanTalk`, `deleteFanTalk` 등 미구현 참조로 실패해 RED 상태를 확인했다.
- 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"` 실행 결과 PASS.
- [x] **Task 2.4: `CreatorChannelFanTalkViewModel` 구현**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModel.kt`
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
- 작업:
- `DEFAULT_PAGE_SIZE = 20`, `FIRST_PAGE = 0`으로 둔다.
- `loadFanTalks(creatorId: Long, isOwner: Boolean)`는 같은 `creatorId``isOwner`로 기존 state가 있으면 중복 최초 조회를 막는다.
- `retryFanTalks()``refreshFanTalks()`는 첫 페이지를 다시 조회한다.
- `loadMore()`는 content 상태, `hasNext`, `isLoadingMore`, `creatorId`를 확인해 중복 요청을 막는다.
- `requestGeneration`으로 오래된 응답이 최신 상태를 덮어쓰지 않게 한다.
- 첫 페이지 성공 후 `fanTalkCount == 0` 또는 표시 가능한 item이 0개이면 `Empty(fanTalkCount)` 상태로 전환한다.
- pagination 실패는 기존 content를 유지하고 `paginationErrorMessage`에만 반영한다.
- `reportFanTalk(fanTalkId, reason)`는 blank reason을 ViewModel에서 호출하지 않는 전제로 두고 repository 결과 message를 toast state로 전달한다.
- `deleteFanTalk(fanTalkId)`는 성공 시 `refreshFanTalks()`를 호출한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`
- 기대 결과:
- ViewModel 관련 테스트가 GREEN이다.
- 검증 기록:
- 2026-06-22: `CreatorChannelFanTalkViewModel.kt`를 추가하고 `AppDI.kt``viewModel { CreatorChannelFanTalkViewModel(get(), get()) }` binding을 추가했다.
- `DEFAULT_PAGE_SIZE = 20`, `FIRST_PAGE = 0`, 동일 조건 최초 조회 중복 방지, `requestGeneration`, load-more guard, pagination error 유지/consume, 신고/삭제 action message, 삭제 성공 후 refresh 동작을 테스트로 확인했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"` 실행 결과 PASS.
- 2026-06-22 리뷰 수정: 신고 성공 응답의 `message`가 null이면 `character_comment_report_submitted` fallback toast message를 사용하도록 수정했다.
- RED: `CreatorChannelFanTalkActionTest``ApiResponse(true, Any(), null)` 케이스를 추가한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkActionTest"` 실행 시 `expected:<Your report has been submitted.> but was:<null>`로 실패함을 확인했다.
- GREEN: `SodaLiveApplicationHolder` 초기화가 포함된 action 테스트와 ViewModel fallback 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkActionTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`, `./gradlew :app:compileDebugKotlin`, `git diff --check` 실행 결과 PASS.
---
### Phase 3: Mapper와 권한별 UI model 추가
- [x] **Task 3.1: FanTalk UI model 추가**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkUiModels.kt`
- 작업:
- `CreatorChannelFanTalkUiModel`에는 `fanTalkId`, `writerId`, `writerNickname`, `writerProfileImageUrl`, `content`, `createdAtText`, `reply`, `rightAction`을 포함한다.
- `CreatorChannelFanTalkReplyUiModel`에는 `fanTalkId`, `writerId`, `writerNickname`, `writerProfileImageUrl`, `content`, `createdAtText`를 포함한다.
- `CreatorChannelFanTalkRightAction``Report`, `OwnerMore(showEdit: Boolean, showDelete: Boolean)`로 정의한다.
- 내가 쓴 글이면 `OwnerMore(showEdit = true, showDelete = true)`이다.
- 내 채널의 타인 글이면 `OwnerMore(showEdit = false, showDelete = true)`이다.
- 내가 쓴 글도 아니고 내 채널도 아니면 `Report`이다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- UI model 추가 후 컴파일이 PASS한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelFanTalkUiModels.kt`에 원글/답글 UI model과 `CreatorChannelFanTalkRightAction.Report`, `OwnerMore(showEdit, showDelete)`를 추가했다.
- mapper/action 테스트에서 내가 쓴 글은 `OwnerMore(showEdit = true, showDelete = true)`, 내 채널의 타인 글은 `OwnerMore(showEdit = false, showDelete = true)`, 타인 채널의 타인 글은 `Report`로 매핑됨을 확인했다.
- `./gradlew :app:compileDebugKotlin` 실행 결과 PASS.
- [x] **Task 3.2: Mapper RED/GREEN 테스트 작성**
- 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkMapperTest.kt`
- 테스트 케이스:
- `createdAtUtc``UtcRelativeTimeTextFormatter.format()` 결과로 매핑된다.
- `creatorReplies`가 비어 있으면 `reply == null`이다.
- `creatorReplies`가 2개 이상이면 첫 번째 reply만 매핑된다.
- 내가 쓴 글이면 `OwnerMore(showEdit = true, showDelete = true)`이다.
- 내 채널의 타인 글이면 `OwnerMore(showEdit = false, showDelete = true)`이다.
- 내가 쓴 글도 아니고 내 채널도 아니면 `Report`이다.
- 원글/답글 content가 빈 문자열이어도 item은 유지된다.
- 구현:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkMappers.kt`
- `List<CreatorChannelFanTalkResponse>.toFanTalkUiModels(relativeTimeTextFormatter, isOwner, currentUserId)`를 추가한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkMapperTest"`
- 기대 결과:
- Mapper 테스트가 GREEN이다.
- 검증 기록:
- 2026-06-22: `CreatorChannelFanTalkMapperTest.kt`를 추가하고 production 구현 전 미구현 참조로 RED 실패함을 확인했다.
- `CreatorChannelFanTalkMappers.kt``List<CreatorChannelFanTalkResponse>.toFanTalkUiModels(relativeTimeTextFormatter, isOwner, currentUserId)`를 추가했다.
- `createdAtUtc``UtcRelativeTimeTextFormatter.format()` 매핑, `creatorReplies.firstOrNull()` 1개 reply 매핑, 권한별 right action, 빈 content 유지 케이스를 테스트로 확인했다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"``./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"` 실행 결과 PASS.
---
### Phase 4: FanTalk 목록 UI와 popup 구현
- [x] **Task 4.1: Fragment layout과 문자열 추가**
- 생성:
- `app/src/main/res/layout/fragment_creator_channel_fantalk.xml`
- 수정:
- `app/src/main/res/values/strings.xml`
- `app/src/main/res/values-en/strings.xml`
- `app/src/main/res/values-ja/strings.xml`
- 작업:
- content 상태에서는 `전체` label과 `fanTalkCount`만 있는 count bar를 배치한다.
- sort label/icon/popup 진입 영역은 layout에 넣지 않는다.
- RecyclerView, error message, retry button, empty container, floating write button을 포함한다.
- empty container에는 PRD의 empty 문구와 `creator_channel_fantalk_support_action` button을 배치한다.
- empty 상태에서는 count bar, RecyclerView, floating write button을 숨기는 구조로 둔다.
- 신규 문자열은 `creator_channel_fantalk_empty_message`, `creator_channel_fantalk_error_message`, `creator_channel_fantalk_retry`, `creator_channel_fantalk_all_label`, `creator_channel_fantalk_report`, `creator_channel_fantalk_edit`, `creator_channel_fantalk_delete` 이름으로 추가한다.
- 검증 명령:
- `./gradlew :app:mergeDebugResources`
- 기대 결과:
- 신규 layout/string resource merge가 PASS한다.
- 검증 기록:
- 2026-06-22: `fragment_creator_channel_fantalk.xml`을 추가하고 content 상태 count bar, RecyclerView, error/retry, empty container, floating write button을 배치했다. sort label/icon/popup 진입 영역은 추가하지 않았다.
- 2026-06-22: `creator_channel_fantalk_empty_message`, `creator_channel_fantalk_error_message`, `creator_channel_fantalk_retry`, `creator_channel_fantalk_all_label`, `creator_channel_fantalk_report`, `creator_channel_fantalk_edit`, `creator_channel_fantalk_delete` 문자열을 한국어/영어/일본어 리소스에 추가했다.
- 2026-06-22: `./gradlew :app:mergeDebugResources` 실행 결과 PASS.
- [x] **Task 4.2: FanTalk item layout과 adapter 추가**
- 생성:
- `app/src/main/res/layout/item_creator_channel_fantalk.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkAdapter.kt`
- 작업:
- 원글 profile image는 42dp 원형 수준으로 표시한다.
- 원글 writer nickname, createdAtText, content를 표시한다.
- 우측 action은 `OwnerMore`이면 `ic_new_more` ImageView, `Report`이면 `신고` TextView만 표시한다.
- reply가 있으면 원글 아래 회색 배경 답글 card를 표시한다.
- reply profile image는 20dp 원형 수준으로 표시한다.
- reply writer nickname, createdAtText, content를 표시한다.
- reply가 없으면 답글 card와 연결선을 숨긴다.
- profile image url이 비어 있거나 실패하면 `ic_placeholder_profile`을 사용한다.
- item click 자체는 동작하지 않는다.
- 검증 명령:
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- item layout과 adapter 추가 후 resource merge와 Kotlin compile이 PASS한다.
- 검증 기록:
- 2026-06-22: `item_creator_channel_fantalk.xml``CreatorChannelFanTalkAdapter.kt`를 추가했다. 원글/답글 프로필, 닉네임, 시간, 본문, 권한별 `신고`/`ic_new_more`, reply card와 connector visibility를 UI model 기준으로 바인딩한다.
- 2026-06-22: profile image는 `loadUrl``CircleCropTransformation`, `ic_placeholder_profile` placeholder/error를 사용하도록 구현했다.
- 2026-06-22: `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin` 실행 결과 PASS.
- [x] **Task 4.3: 더보기 popup 추가**
- 생성:
- `app/src/main/res/layout/view_creator_channel_fantalk_more_popup.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkMorePopup.kt`
- 작업:
- `showEdit == true`이면 `수정하기`를 표시한다.
- `showDelete == true`이면 `삭제하기`를 표시한다.
- `수정하기` 터치 시 popup만 닫고 API/화면 이동은 호출하지 않는다.
- `삭제하기` 터치 시 popup을 닫고 adapter callback으로 `fanTalkId`를 전달한다.
- popup은 anchor view 기준 우측 영역에 표시하되 화면 밖으로 벗어나지 않도록 `PopupWindow` 기본 clipping을 유지한다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- popup class 추가 후 컴파일이 PASS한다.
- 검증 기록:
- 2026-06-22: `view_creator_channel_fantalk_more_popup.xml``CreatorChannelFanTalkMorePopup.kt`를 추가했다. `showEdit`, `showDelete`에 따라 `수정하기`, `삭제하기`를 표시하고 수정은 dismiss만, 삭제는 dismiss 후 callback으로 `fanTalkId`를 전달한다.
- 2026-06-22: `./gradlew :app:compileDebugKotlin` 실행 결과 PASS.
- [x] **Task 4.4: Fragment layout source 테스트 추가**
- 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt`
- 테스트 케이스:
- fragment layout에 sort icon/정렬 label이 없다.
- fragment layout에 `전체` count TextView가 있다.
- fragment layout에 empty 문구와 `응원 남기기` button이 있다.
- fragment layout에 content floating write button이 있다.
- item layout에 `ic_new_more``신고` TextView가 있다.
- item layout에 reply container가 있다.
- popup layout에 `수정하기`, `삭제하기` TextView가 있다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"`
- 기대 결과:
- Layout source 테스트가 GREEN이다.
- 검증 기록:
- 2026-06-22: `CreatorChannelFanTalkFragmentLayoutTest.kt`를 추가했다. fragment layout의 sort UI 부재, count/empty/floating write button, item action/reply container, popup 수정/삭제 TextView, adapter/popup source 계약을 검증한다.
- 2026-06-22: RED 확인으로 신규 layout/resource 구현 전 테스트가 layout/id 부재로 실패함을 확인했고, 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"` 실행 결과 PASS.
---
### Phase 5: Fragment 동작, 신고/삭제, empty 상태 연결
- [x] **Task 5.1: `CreatorChannelFanTalkFragment` 구현**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt`
- 작업:
- `CreatorChannelFanTalkViewModel`을 Koin `by viewModel()`로 주입한다.
- `RecyclerView``LinearLayoutManager``CreatorChannelFanTalkAdapter`를 사용한다.
- `onCreatorChannelFanTalkTabSelected()`에서 `viewModel.loadFanTalks(creatorId, isOwner = host.isCreatorChannelOwner())`를 호출한다.
- `onCreatorChannelFanTalkScrolledToBottom()`에서 `viewModel.loadMore()`를 호출한다.
- `onCreatorChannelFanTalkRefreshRequested()`에서 `viewModel.refreshFanTalks()`를 호출한다.
- Loading/Error/Empty/Content 상태별 visibility를 명확히 분기한다.
- Content 상태에서는 count bar, RecyclerView, floating write button을 표시한다.
- Empty 상태에서는 empty container만 표시하고 count bar, RecyclerView, floating write button을 숨긴다.
- floating write button과 empty `응원 남기기` button click listener는 비워 두거나 no-op으로 둔다.
- 신고 action click 시 `CheersReportDialog`를 열고 blank reason이면 기존 reason required toast를 표시한다.
- 삭제 action click 시 host에 `fanTalkId`를 전달해 삭제 확인 dialog를 열도록 한다.
- pagination error와 action toast message는 Toast로 표시 후 consume한다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- Fragment 추가 후 컴파일이 PASS한다.
- 검증 기록:
- 2026-06-22: TDD RED로 `CreatorChannelPagerAdapterTest`, `CreatorChannelActivitySourceTest`, `CreatorChannelFanTalkFragmentLayoutTest`에 FanTalk Fragment/Host/source 계약 기대값을 먼저 추가했다. production 변경 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 시 `CreatorChannelFanTalkFragment` unresolved reference로 실패해 신규 Fragment와 wiring 누락을 확인했다.
- 2026-06-22: `CreatorChannelFanTalkFragment.kt`를 추가하고 Koin `by viewModel()`, `LinearLayoutManager`, `CreatorChannelFanTalkAdapter`, Loading/Error/Empty/Content visibility, no-op write buttons, report dialog, owner more popup, delete confirmed entry, pagination/action toast consume, Host callback을 구현했다.
- 2026-06-22: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"` 실행 결과 PASS.
- [x] **Task 5.2: `CreatorChannelActivity`에 FanTalk Host 연결**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- 작업:
- Activity가 `CreatorChannelFanTalkFragment.Host`를 구현한다.
- `isCreatorChannelOwner()` 기존 method를 FanTalk Host에서도 재사용한다.
- FanTalk 탭 선택 시 `findFanTalkFragment()?.onCreatorChannelFanTalkTabSelected()`를 호출한다.
- 스크롤 하단 도달 시 `findFanTalkFragment()?.onCreatorChannelFanTalkScrolledToBottom()`를 호출한다.
- 새로고침이 필요한 공통 경로에서 `findFanTalkFragment()?.onCreatorChannelFanTalkRefreshRequested()`를 호출한다.
- `onCreatorChannelFanTalkContentChanged()`에서 ViewPager 높이 갱신을 호출한다.
- `onCreatorChannelFanTalkDeleteClicked(fanTalkId)`에서 `SodaDialog`를 표시한다.
- 삭제 확인 dialog의 confirm에서 Fragment 또는 ViewModel으로 삭제 요청을 다시 전달한다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- Activity Host 연결 후 컴파일이 PASS한다.
- 검증 기록:
- 2026-06-22: `CreatorChannelActivity``CreatorChannelFanTalkFragment.Host`를 구현하도록 추가하고 `findFanTalkFragment()`, 탭 선택/header 변경 dispatch, bottom scroll dispatch, load-more tab 포함, content changed 높이 갱신, 삭제 확인 `SodaDialog` 및 confirm 시 Fragment 삭제 entry 호출을 연결했다.
- 2026-06-22: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 PASS.
- [x] **Task 5.3: `CreatorChannelPagerAdapter`에 FanTalk 탭 연결**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapterTest.kt`
- 작업:
- `CreatorChannelTab.FanTalk -> CreatorChannelFanTalkFragment.newInstance(creatorId)` 분기를 추가한다.
- 기존 테스트를 Home/Live/Audio/Series/Community/FanTalk 실제 Fragment 생성 검증으로 갱신한다.
- FanTalk 외 placeholder 유지 기대값은 유지한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"`
- 기대 결과:
- PagerAdapter 테스트가 GREEN이다.
- 검증 기록:
- 2026-06-22: `CreatorChannelTab.FanTalk -> CreatorChannelFanTalkFragment.newInstance(creatorId)` 분기를 추가하고 `CreatorChannelPagerAdapterTest`에서 FanTalk 실제 Fragment 생성 및 placeholder 제외 기대값을 갱신했다.
- 2026-06-22: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"` 실행 결과 PASS.
- [x] **Task 5.4: Activity source 테스트 갱신**
- 수정:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- 작업:
- 기존 `assertFalse(source.contains("CreatorChannelTab.FanTalk ->"))` 기대값을 제거하거나 FanTalk 연결 검증으로 변경한다.
- `CreatorChannelFanTalkFragment.Host` 구현을 검증한다.
- `findFanTalkFragment()?.onCreatorChannelFanTalkTabSelected()` 호출을 검증한다.
- `findFanTalkFragment()?.onCreatorChannelFanTalkScrolledToBottom()` 호출을 검증한다.
- `onCreatorChannelFanTalkContentChanged()` 구현을 검증한다.
- 신고/삭제 dialog 관련 source 검증은 과도한 문자열 고정 대신 주요 class/method 호출만 검증한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- 기대 결과:
- Activity source 테스트가 GREEN이다.
- 검증 기록:
- 2026-06-22: `CreatorChannelActivitySourceTest`의 FanTalk 분기 부재 기대값을 제거하고 Pager/Activity/Fragment source 계약 검증으로 교체했다. Host 구현, `findFanTalkFragment()`, 탭 선택/header selected dispatch, scrolled bottom, load more tab 포함, content changed callback, delete clicked dialog, delete confirmed callback을 검증한다.
- 2026-06-22: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 PASS.
---
### Phase 6: 통합 검증과 회귀 확인
- [x] **Task 6.1: FanTalk 단위 테스트 실행**
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`
- 기대 결과:
- FanTalk mapper/ViewModel/pagination/action/layout 테스트가 모두 PASS한다.
- 검증 기록:
- 2026-06-22: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"` 실행 결과 PASS.
- [x] **Task 6.2: Creator Channel 관련 회귀 테스트 실행**
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`
- 기대 결과:
- FanTalk 연결로 인해 Home/Live/Audio/Series/Community 기존 테스트가 깨지지 않는다.
- 검증 기록:
- 2026-06-22: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"` 실행 결과 PASS.
- 2026-06-22: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` 최초 실행 시 `CreatorChannelHomeViewModelTest > 채널 후원 성공은 기존 후원 API를 호출하고 홈을 다시 로드한다` 1건이 `CreatorChannelHomeViewModelTest.kt:285`에서 실패했다.
- 2026-06-22: 동일 테스트 단독 재실행 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelHomeViewModelTest.채널 후원 성공은 기존 후원 API를 호출하고 홈을 다시 로드한다"` 결과 PASS.
- 2026-06-22: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` 재실행 결과 PASS.
- [x] **Task 6.3: Resource/Kotlin/Lint 검증 실행**
- 실행:
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:compileDebugKotlin`
- `./gradlew :app:ktlintCheck`
- `git diff --check`
- 기대 결과:
- resource merge, Kotlin compile, ktlint, whitespace check가 모두 PASS한다.
- 검증 기록:
- 2026-06-22: `./gradlew :app:mergeDebugResources` 실행 결과 PASS.
- 2026-06-22: `./gradlew :app:compileDebugKotlin` 실행 결과 PASS.
- 2026-06-22: `./gradlew :app:ktlintCheck` 실행 결과 PASS.
- 2026-06-22: `git diff --check` 실행 결과 출력 없이 PASS.
- [x] **Task 6.4: 수동 화면 검증**
- 확인:
- content 상태에서 Sort-bar에는 `전체`와 count만 보이고 정렬 UI가 없다.
- 내가 쓴 글에는 `ic_new_more`가 보이고 popup에 `수정하기`, `삭제하기`가 보인다.
- 내 채널의 타인 글에는 `ic_new_more`가 보이고 popup에 `삭제하기`만 보인다.
- 타인 채널의 타인 글에는 `신고`만 보인다.
- `수정하기`를 눌러도 화면 이동/API 호출이 없다.
- 삭제 confirm을 취소하면 API 호출이 없다.
- 삭제 confirm을 확인하면 삭제 API 호출 후 목록이 갱신된다.
- 신고 reason 미선택 시 `screen_user_profile_fantalk_report_reason_required` toast가 표시된다.
- 신고 reason 선택 후 report API가 호출된다.
- `creatorReplies`가 여러 개 내려와도 첫 번째 1개만 표시된다.
- empty 상태에서 Sort-bar와 우측 하단 floating 버튼이 숨겨지고 중앙 empty 문구와 `응원 남기기` button이 보인다.
- content 상태에서 우측 하단 floating 버튼이 보이지만 클릭해도 화면 이동/API 호출이 없다.
- 목록 하단 스크롤 시 `hasNext == true`일 때만 다음 page가 로딩된다.
- 검증 기록:
- 2026-06-22: 현재 실행 환경에서는 Android 앱 화면을 기기/에뮬레이터로 띄운 실기 수동 검증을 수행하지 못했다.
- 2026-06-22: Phase 4, 5에서 Figma `290:9139`, `290:9000` 기준 화면 구조와 source/layout 테스트를 대조했고, Phase 6에서는 관련 unit/source/layout/resource/Kotlin/ktlint/whitespace 검증이 PASS함을 확인했다.
- 2026-06-22: 실기/에뮬레이터 수동 QA가 가능해지면 위 체크리스트의 권한별 action, 신고/삭제 dialog/API, empty/content/pagination 동작을 실제 화면에서 추가 확인해야 한다.
---
### Phase 7: FanTalk Empty View 위치와 Figma 색상 보정
- [x] **Task 7.1: Figma empty 기준과 기존 탭 배치 비교**
- 확인:
- Figma node `290:9001`
- `app/src/main/res/layout/fragment_creator_channel_fantalk.xml`
- `app/src/main/res/layout/fragment_creator_channel_audio.xml`
- `app/src/main/res/layout/fragment_creator_channel_series.xml`
- `app/src/main/res/layout/fragment_creator_channel_community.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt`
- 작업:
- FanTalk empty container의 `minHeight`/부모 전체 center 배치 여부를 확인한다.
- 오디오/시리즈/커뮤니티 탭의 empty container 위치 패턴을 확인한다.
- Figma node `290:9001`의 empty 문구 색상, button 배경, button text 색상, spacing을 확인한다.
- 검증 기준:
- Figma 기준: 문구 `gray/500`(`#959595`), button 배경 `gray/800`(`#343434`), button text `white`, 문구-button gap 14dp, icon-text gap 6dp.
- 기존 탭 기준: empty container는 `wrap_content`, `paddingTop=@dimen/spacing_48`, `paddingBottom=@dimen/spacing_32`, `Top_toTopOf=parent`, bottom constraint 없음.
- 검증 기록:
- 2026-06-22: Figma `290:9001`을 확인했다. empty 문구는 Pretendard regular 16sp, line-height 1.45, `gray/500 #959595`이고, button은 `gray/800 #343434` 배경, radius 100, 20dp icon, 16sp medium white text, 문구-button gap 14dp, icon-text gap 6dp임을 확인했다.
- 2026-06-22: 오디오/시리즈/커뮤니티 탭 empty container는 `FrameLayout`, `layout_height=wrap_content`, `paddingTop=@dimen/spacing_48`, `paddingBottom=@dimen/spacing_32`, `Top_toTopOf=parent` 패턴이고 bottom constraint를 사용하지 않음을 확인했다.
- 2026-06-22: FanTalk empty container는 `layout_height=0dp`, parent bottom constraint, center gravity를 사용하고, Fragment가 `binding.root.minimumHeight = minHeight`를 적용하고 있어 다른 탭과 다르게 추가 높이를 만든다는 점을 확인했다.
- [x] **Task 7.2: FanTalk layout/source 테스트 RED 갱신**
- 수정:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragmentLayoutTest.kt`
- 작업:
- FanTalk empty container가 오디오/시리즈/커뮤니티와 같은 상단 가시 영역 배치를 따르도록 테스트 기대값을 변경한다.
- `binding.root.minimumHeight = minHeight`를 사용하지 않도록 source 테스트 기대값을 변경한다.
- Figma 기준 색상/spacing 리소스가 적용되는지 테스트에 포함한다.
- 검증 명령:
- `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"`
- 기대 결과:
- 구현 수정 전에는 layout/source 기대값 차이로 RED 실패한다.
- 검증 기록:
- 2026-06-22: 테스트를 먼저 갱신한 뒤 위 명령을 실행했고 `팬톡 fragment layout은 count list empty error retry floating write를 제공하고 sort UI는 제외한다`, `팬톡 fragment source는 Phase 5 상태 신고 삭제 Host 계약을 포함한다` 2건이 예상대로 실패했다.
- [x] **Task 7.3: FanTalk Empty View 최소 수정**
- 수정:
- `app/src/main/res/layout/fragment_creator_channel_fantalk.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt`
- 작업:
- empty container를 `FrameLayout`/`wrap_content` 기반으로 바꾸고 `paddingTop=@dimen/spacing_48`, `paddingBottom=@dimen/spacing_32`, top constraint만 남긴다.
- empty 문구/버튼 내부 vertical layout을 Figma 기준으로 유지하되 문구-button gap을 14dp로 맞춘다.
- button icon-text gap을 6dp로 맞춘다.
- `onCreatorChannelFanTalkViewportHeightChanged(minHeight: Int)`는 다른 탭처럼 no-op으로 변경해 root `minimumHeight` 설정을 제거한다.
- 검증 명령:
- `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"`
- `./gradlew --no-daemon :app:mergeDebugResources`
- `./gradlew --no-daemon :app:compileDebugKotlin`
- `git diff --check`
- 기대 결과:
- FanTalk layout 테스트, resource merge, Kotlin compile, whitespace check가 PASS한다.
- 검증 기록:
- 2026-06-22: `fragment_creator_channel_fantalk.xml`에서 empty container를 `FrameLayout`/`wrap_content`로 변경하고 `paddingTop=@dimen/spacing_48`, `paddingBottom=@dimen/spacing_32`, top constraint만 남겼다.
- 2026-06-22: empty content 내부 button spacing을 Figma 기준에 맞춰 문구-button gap 14dp, icon-text gap 6dp로 조정했다. empty 문구 `gray_500`, button 배경 `gray_800`, button text `white`는 테스트로 확인했다.
- 2026-06-22: `CreatorChannelFanTalkFragment.onCreatorChannelFanTalkViewportHeightChanged(minHeight: Int)`를 다른 탭처럼 no-op으로 변경해 root `minimumHeight` 설정을 제거했다.
- 2026-06-22: `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"` 최초 GREEN 시도에서 floating write button의 bottom constraint까지 전체 XML 기준으로 검사해 1건이 실패했다. Empty container 블록만 검사하도록 테스트를 좁힌 뒤 재실행 결과 PASS.
- 2026-06-22: `./gradlew --no-daemon :app:mergeDebugResources` 실행 결과 PASS.
- 2026-06-22: `./gradlew --no-daemon :app:compileDebugKotlin` 실행 결과 PASS.
- 2026-06-22: `git diff --check` 실행 결과 출력 없이 PASS.
---
## Verification Log
- 문서 작성 시점에는 구현을 수행하지 않았으므로 빌드/테스트 검증 기록이 없다.
- 2026-06-22: Phase 2, 3 코드 리뷰를 수행했다. API/DTO/Repository/ViewModel/mapper/action model/test 변경이 PRD와 계획 문서의 계약 범위 안에 있으며, 레거시 파일 직접 수정 없이 기존 repository/dialog 계약을 호출하는 구조임을 확인했다. blocking finding은 발견하지 못했다.
- 2026-06-22: Phase 2, 3 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`, `git diff --cached --check`를 실행했고 모두 PASS했다.
- 2026-06-22: Phase 4, 5 코드 리뷰를 수행했다. Figma `290:9139`, `290:9000` 기준으로 count bar, sort UI 부재, 원글/답글 item, 권한별 action, more popup, floating/empty write button, empty 문구를 구현과 대조했고 blocking finding은 발견하지 못했다.
- 2026-06-22: Phase 4, 5 재검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`, `git diff --check`를 실행했고 모두 PASS했다. 최초 `mergeDebugResources`는 Gradle wrapper lock 파일 샌드박스 접근 제한으로 실패했으며, 동일 명령을 승인 실행해 PASS를 확인했다.
- 2026-06-22: Phase 6 통합 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행했다. 전체 Creator Channel 테스트 최초 실행에서 `CreatorChannelHomeViewModelTest > 채널 후원 성공은 기존 후원 API를 호출하고 홈을 다시 로드한다` 1건이 일시 실패했으나, 해당 테스트 단독 재실행과 전체 Creator Channel 테스트 재실행은 모두 PASS했다.
- 2026-06-22: Phase 7 FanTalk Empty View 보정 검증으로 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `git diff --check`를 실행했고 모두 PASS했다.