528 lines
34 KiB
Markdown
528 lines
34 KiB
Markdown
# 크리에이터 채널 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: 기존 구조 확인과 작업 경계 고정
|
||
|
||
- [ ] **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와 관련 테스트 갱신 지점이 확인된다.
|
||
- 검증 기록:
|
||
|
||
- [ ] **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 패턴이 확인된다.
|
||
- 검증 기록:
|
||
|
||
- [ ] **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`
|
||
- 기대 결과: 기존 신고/삭제 계약이 확인된다.
|
||
- 검증 기록:
|
||
|
||
- [ ] **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`
|
||
- 기대 결과: 재사용 가능한 리소스와 신규 문자열 추가 대상이 구분된다.
|
||
- 검증 기록:
|
||
|
||
---
|
||
|
||
### 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<CreatorChannelFanTalkResponse>.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
|
||
|
||
- 문서 작성 시점에는 구현을 수행하지 않았으므로 빌드/테스트 검증 기록이 없다.
|