# 크리에이터 채널 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 계약 추가 - [ ] **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한다. - 검증 기록: - [ ] **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와 충돌 없이 컴파일된다. - 검증 기록: - [ ] **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 실패한다. - 검증 기록: - [ ] **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이다. - 검증 기록: --- ### Phase 3: Mapper와 권한별 UI model 추가 - [ ] **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한다. - 검증 기록: - [ ] **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.toFanTalkUiModels(relativeTimeTextFormatter, isOwner, currentUserId)`를 추가한다. - 검증 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkMapperTest"` - 기대 결과: - Mapper 테스트가 GREEN이다. - 검증 기록: --- ### Phase 4: FanTalk 목록 UI와 popup 구현 - [ ] **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한다. - 검증 기록: - [ ] **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한다. - 검증 기록: - [ ] **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한다. - 검증 기록: - [ ] **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이다. - 검증 기록: --- ### Phase 5: Fragment 동작, 신고/삭제, empty 상태 연결 - [ ] **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한다. - 검증 기록: - [ ] **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한다. - 검증 기록: - [ ] **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이다. - 검증 기록: - [ ] **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이다. - 검증 기록: --- ### Phase 6: 통합 검증과 회귀 확인 - [ ] **Task 6.1: FanTalk 단위 테스트 실행** - 실행: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"` - 기대 결과: - FanTalk mapper/ViewModel/pagination/action/layout 테스트가 모두 PASS한다. - 검증 기록: - [ ] **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 기존 테스트가 깨지지 않는다. - 검증 기록: - [ ] **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한다. - 검증 기록: - [ ] **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가 로딩된다. - 검증 기록: --- ## Verification Log - 문서 작성 시점에는 구현을 수행하지 않았으므로 빌드/테스트 검증 기록이 없다.