387 lines
37 KiB
Markdown
387 lines
37 KiB
Markdown
# FanTalk 상세 / 답글쓰기 구현 계획/TASK
|
|
|
|
> **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.
|
|
|
|
**Goal:** 본인 채널의 `FanTalk` item을 터치하면 커뮤니티 답글 화면과 동일한 UI의 FanTalk 상세 화면으로 진입하고, 기존 FanTalk 답글 쓰기 API로 크리에이터 답글을 작성한다.
|
|
|
|
**Architecture:** 상세 화면은 `kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail` 하위에 신규 Activity/ViewModel로 둔다. FanTalk는 별도 상세 조회 API가 없으므로 item 터치 시 목록의 FanTalk data model을 통째로 상세 화면에 전달한다. 답글이 없으면 `CreatorChannelRepository.writeFanTalkReply(...)` wrapper가 기존 `ExplorerRepository.writeCheers(parentCheersId = fanTalkId, ...)`를 호출하고 `ApiResponse<CreatorChannelFanTalkReplyResponse>`의 반환 data를 현재 상세 답글로 즉시 바인딩한다. 답글이 있으면 대체 안내 모달 확인 후 `modifyCheers` wrapper로 기존 답글을 수정한다.
|
|
|
|
**Tech Stack:** Kotlin, Android XML Views, ViewBinding, Activity Result API, RxJava3, Koin, JUnit4/Robolectric.
|
|
|
|
---
|
|
|
|
## 전제와 성공 기준
|
|
|
|
- PRD: `docs/20260709_FanTalk_상세_답글쓰기/prd.md`
|
|
- 기존 FanTalk 탭 문서:
|
|
- `docs/20260622_FanTalk_탭/prd.md`
|
|
- `docs/20260622_FanTalk_탭/plan-task.md`
|
|
- 기존 FanTalk 글 입력 문서:
|
|
- `docs/20260708_FanTalk_글_입력/prd.md`
|
|
- `docs/20260708_FanTalk_글_입력/plan-task.md`
|
|
- UI 참조 구현:
|
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/CreatorChannelCommunityReplyActivity.kt`
|
|
- `app/src/main/res/layout/activity_creator_channel_community_reply.xml`
|
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/CreatorChannelCommunityReplyViewModel.kt`
|
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/detail/reply/ui/CreatorChannelCommunityReplyAdapter.kt`
|
|
- 기존 FanTalk 목록 구현:
|
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt`
|
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkViewModel.kt`
|
|
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkAdapter.kt`
|
|
- `app/src/main/res/layout/item_creator_channel_fantalk.xml`
|
|
- 기존 FanTalk API wrapper:
|
|
- `CreatorChannelRepository.getFanTalks(...)`
|
|
- `CreatorChannelRepository.writeFanTalk(creatorId, content, token)`
|
|
- `CreatorChannelRepository.deleteFanTalk(fanTalkId, token)`
|
|
- 기존 레거시 답글 쓰기 API:
|
|
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/ExplorerRepository.kt`
|
|
- `writeCheers(parentCheersId, creatorId, content, token)`
|
|
- 본인 채널 gate:
|
|
- `CreatorChannelActivity.currentHeader?.isOwner == true`
|
|
- FanTalk 상세 데이터 전달:
|
|
- 별도 상세 조회 API를 호출하지 않는다.
|
|
- FanTalk item data model을 통째로 전달한다.
|
|
- 개별 primitive extra 분해 전달은 사용하지 않는다.
|
|
- FanTalk 상세 답글 정책:
|
|
- 크리에이터 답글은 1개만 가능하다.
|
|
- 별도 답글 pagination/API는 사용하지 않는다.
|
|
- 기존 답글이 있는 상태에서 send하면 안내 모달을 표시하고, 확인 시 수정 API로 기존 답글을 신규 입력값으로 대체한다.
|
|
- 구현 완료 후 최소 다음 명령을 실행한다.
|
|
- `./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:mergeDebugResources`
|
|
- `./gradlew :app:compileDebugKotlin`
|
|
- `./gradlew :app:ktlintCheck`
|
|
- `git diff --check`
|
|
|
|
---
|
|
|
|
## Figma 참조
|
|
|
|
- 답글 없음: `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=669-40462&m=dev`
|
|
- 답글 있음: `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=669-40468&m=dev`
|
|
- Figma MCP 확인 기록:
|
|
- `669:40462`: 뒤로가기만 있는 title-bar, 부모 FanTalk 원문, 하단 input bar, disabled send button, 답글 card 없음.
|
|
- `669:40468`: 부모 FanTalk 원문, 답글 card, 답글 더보기 popup(`수정하기`, `삭제하기`), 하단 input bar.
|
|
|
|
---
|
|
|
|
## V2 재사용 후보 사전 조사
|
|
|
|
- `CreatorChannelCommunityReplyActivity`
|
|
- 후보 이유: 요청 UI와 동일한 화면 구조, input bar, send button 상태, 키보드 제어, 수정 모드, `V2ModalDialog` 삭제 확인 흐름이 이미 구현되어 있다.
|
|
- 사용 방침: Activity 자체를 직접 재사용하지 않고 FanTalk 전용 Activity를 신규 생성하되, 구조와 동작을 기준으로 맞춘다. 커뮤니티 전용 `CreatorChannelCommunityCommentUiModel` 의존을 FanTalk로 끌고 오지 않는다.
|
|
- `activity_creator_channel_community_reply.xml`
|
|
- 후보 이유: Figma `669:40462`/`669:40468`와 동일한 title-bar/input/list 구조다.
|
|
- 사용 방침: FanTalk 전용 layout으로 복제/축소하거나 include 가능한 공통 input bar를 분리한다. 단일 사용처라면 과도한 공통화는 피한다.
|
|
- `CreatorChannelCommunityMorePopup` + `view_creator_channel_community_more_popup.xml`
|
|
- 후보 이유: `수정하기`, `삭제하기`, `@font/regular`, `bg_creator_channel_sort_popup`이 Figma popup과 맞는다.
|
|
- 사용 방침: FanTalk popup이 이미 있으므로 `CreatorChannelFanTalkMorePopup`에 edit callback/font 보강을 우선 검토한다. 커뮤니티 popup 직접 재사용은 model/API 차이 때문에 신중히 판단한다.
|
|
- `CreatorChannelFanTalkAdapter` + `item_creator_channel_fantalk.xml`
|
|
- 후보 이유: 부모 FanTalk와 최신 답글 card의 프로필/닉네임/시간/본문 UI가 이미 있다.
|
|
- 사용 방침: 상세 화면의 부모 item/답글 item binding 로직 참고 또는 adapter callback 확장에 사용한다.
|
|
- `CreatorChannelFanTalkMorePopup`
|
|
- 후보 이유: FanTalk item의 owner menu로 이미 사용 중이다.
|
|
- 현재 한계: edit click이 dismiss만 하고 실제 callback이 없다.
|
|
- 사용 방침: 상세 수정이 범위에 포함되면 `onEditClick`을 추가하고 source/layout 테스트를 보강한다.
|
|
- `V2ModalDialog`
|
|
- 후보 이유: v2 내부 삭제 확인 dialog 표준이다.
|
|
- 사용 방침: FanTalk 답글 삭제 확인에 재사용한다.
|
|
- `UtcRelativeTimeTextFormatter`
|
|
- 후보 이유: FanTalk/커뮤니티 모두 상대시간 표시를 사용한다.
|
|
- 사용 방침: intent로 원본 `createdAtUtc`를 넘길 수 있으면 상세에서도 동일 formatter로 포맷한다.
|
|
- `bg_creator_channel_community_input_field`, `bg_creator_channel_community_send_enabled`, `bg_creator_channel_community_send_disabled`
|
|
- 후보 이유: 커뮤니티 답글 input/send button 상태와 동일하다.
|
|
- 사용 방침: 디자인 토큰이 동일하면 재사용한다. resource naming이 커뮤니티에 묶이는 점이 문제이면 FanTalk 전용 alias drawable을 추가한다.
|
|
- `ic_new_arrow_up_white`, `ic_new_arrow_up_gray`
|
|
- 후보 이유: 현재 저장소에 존재하며 커뮤니티 답글 화면에서 사용 중이다.
|
|
- 사용 방침: FanTalk 상세 send button 활성/비활성 icon으로 사용한다.
|
|
- `v2/widget/*`
|
|
- 검토 결과: `AudioContentCardView`, `SeriesContentCardView`, ranking/feed/live thumbnail 계열은 FanTalk 상세/답글쓰기 UI와 직접 맞지 않는다. 이번 범위에서는 재사용 후보에서 제외한다.
|
|
|
|
---
|
|
|
|
## API / 데이터 계약
|
|
|
|
- 목록 조회:
|
|
- 기존 `GET /api/v2/creator-channels/{creatorId}/fan-talks?page=0&size=20`
|
|
- 응답 모델: `CreatorChannelFanTalkTabResponse`, `CreatorChannelFanTalkResponse`, `CreatorChannelFanTalkReplyResponse`
|
|
- 상세 초기 데이터:
|
|
- 신규 상세 조회 API를 가정하지 않는다.
|
|
- FanTalk item data model을 통째로 전달한다.
|
|
- `CreatorChannelFanTalkUiModel` 또는 FanTalk 상세 전용 payload model을 `Parcelable`로 전달하는 방식을 우선한다.
|
|
- 개별 primitive extra로 필드를 나누어 전달하지 않는다.
|
|
- 답글 작성:
|
|
- 기존 답글 작성: `CreatorChannelRepository.writeFanTalkReply(fanTalkId, creatorId, content, token)`
|
|
- 내부 호출: `ExplorerRepository.writeCheers(parentCheersId = fanTalkId, creatorId = creatorId, content = content, token = token)`
|
|
- 응답: `ApiResponse<CreatorChannelFanTalkReplyResponse>`의 `data`를 상세 화면 답글로 즉시 바인딩한다.
|
|
- `success=true, data=null`도 성공으로 처리하되, 이 경우 임의 답글 UI model을 만들지 않는다.
|
|
- 답글 대체/수정/삭제:
|
|
- 답글은 1개만 가능하므로 별도 pagination/API를 사용하지 않는다.
|
|
- 기존 답글이 없을 때 send: `writeFanTalkReply(...)`.
|
|
- 기존 답글이 있을 때 send: 대체 안내 모달 확인 후 `modifyFanTalkReply(...)`.
|
|
- 수정: `ExplorerRepository.modifyCheers(PutModifyCheersRequest(cheersId = replyFanTalkId, content = ...), token)`
|
|
- 삭제: `ExplorerRepository.modifyCheers(PutModifyCheersRequest(cheersId = replyFanTalkId, isActive = false), token)`
|
|
- 대체 안내 모달:
|
|
- title: `답글 수정`
|
|
- desc: `기존 답글이 신규 답글로 대체됩니다.\n수정하시겠어요?`
|
|
- cancel button: `취소`
|
|
- confirm button: `확인`
|
|
|
|
---
|
|
|
|
## 파일 구조 (예정)
|
|
|
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailActivity.kt`
|
|
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailViewModel.kt`
|
|
- 생성 가능: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailUiModels.kt`
|
|
- 생성: `app/src/main/res/layout/activity_creator_channel_fantalk_detail.xml`
|
|
- 생성 가능: `app/src/main/res/layout/item_creator_channel_fantalk_detail_reply.xml`
|
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkAdapter.kt`
|
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkFragment.kt`
|
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
|
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
|
|
- 수정 가능: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/ui/CreatorChannelFanTalkMorePopup.kt`
|
|
- 수정 가능: `app/src/main/res/layout/view_creator_channel_fantalk_more_popup.xml`
|
|
- 수정: `app/src/main/AndroidManifest.xml`
|
|
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
|
|
- 수정: `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/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailViewModelTest.kt`
|
|
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/detail/CreatorChannelFanTalkDetailUiContractTest.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/CreatorChannelActivitySourceTest.kt`
|
|
|
|
---
|
|
|
|
### Phase 1: 기존 구조 확인과 상세 진입 계약
|
|
|
|
- [x] **Task 1.1: FanTalk item 터치 가능 지점 확인**
|
|
- 확인:
|
|
- `CreatorChannelFanTalkAdapter.kt`
|
|
- `item_creator_channel_fantalk.xml`
|
|
- 작업:
|
|
- item root 또는 본문 영역에 click callback을 추가할 수 있는지 확인한다.
|
|
- report/more button click과 item click이 충돌하지 않게 event 경계를 정한다.
|
|
- 검증:
|
|
- source test로 adapter constructor에 detail click callback이 있는지 확인한다.
|
|
|
|
- [x] **Task 1.2: 본인 채널 gate 정의**
|
|
- 확인:
|
|
- `CreatorChannelActivity.kt`
|
|
- `currentHeader?.isOwner`
|
|
- 작업:
|
|
- `currentHeader?.isOwner == true`일 때만 상세 진입을 허용한다.
|
|
- 본인 채널이 아닌 경우 adapter callback 연결을 막거나 Activity callback에서 return한다.
|
|
- 검증:
|
|
- source test로 owner gate와 `showLoginActivity()` 정책을 확인한다.
|
|
|
|
---
|
|
|
|
### Phase 2: API wrapper와 ViewModel 계약
|
|
|
|
- [x] **Task 2.1: FanTalk 답글 쓰기 wrapper 추가**
|
|
- 수정:
|
|
- `CreatorChannelRepository.kt`
|
|
- 작업:
|
|
- `writeFanTalkReply(fanTalkId: Long, creatorId: Long, content: String, token: String)`를 추가한다.
|
|
- 내부에서 기존 `explorerRepository.writeCheers(parentCheersId = fanTalkId, creatorId = creatorId, content = content, token = token)`를 호출하고 `ApiResponse<CreatorChannelFanTalkReplyResponse>`를 반환한다.
|
|
- `modifyFanTalkReply(replyFanTalkId: Long, content: String, token: String)`를 추가한다.
|
|
- 내부에서 `explorerRepository.modifyCheers(PutModifyCheersRequest(cheersId = replyFanTalkId, content = content), token)`를 호출한다.
|
|
- 검증:
|
|
- repository source test 또는 ViewModel 테스트로 `parentCheersId = fanTalkId`와 `cheersId = replyFanTalkId` 계약을 확인한다.
|
|
|
|
- [x] **Task 2.2: 상세 ViewModel RED 테스트 작성**
|
|
- 생성:
|
|
- `CreatorChannelFanTalkDetailViewModelTest.kt`
|
|
- 테스트 케이스:
|
|
- 통째로 전달받은 FanTalk data model 기반으로 부모 FanTalk와 기존 답글 1개를 Content state로 노출한다.
|
|
- 입력값 없음/공백만 입력 시 send disabled 상태다.
|
|
- 입력값 있음이면 send enabled 상태다.
|
|
- 기존 답글이 없으면 submit 시 `writeFanTalkReply(fanTalkId, creatorId, trimmedContent, token)`를 호출한다.
|
|
- 기존 답글이 있으면 submit 시 바로 API를 호출하지 않고 대체 안내 모달 event를 emit한다.
|
|
- 대체 안내 모달 확인 시 `modifyFanTalkReply(replyFanTalkId, trimmedContent, token)`를 호출한다.
|
|
- 대체 안내 모달 취소 시 API를 호출하지 않고 입력값을 유지한다.
|
|
- submit 중 중복 전송을 막는다.
|
|
- 성공 시 입력값을 비우고 완료 event를 emit한다.
|
|
- 실패 시 입력값을 유지하고 toast event를 emit한다.
|
|
- 검증:
|
|
- 구현 전 실패(RED)를 확인한다.
|
|
|
|
- [x] **Task 2.3: 상세 ViewModel 구현**
|
|
- 생성:
|
|
- `CreatorChannelFanTalkDetailViewModel.kt`
|
|
- `CreatorChannelFanTalkDetailUiModels.kt`
|
|
- 작업:
|
|
- 부모 FanTalk, 답글 1개, input, sending, replacement modal event, toast/event state를 관리한다.
|
|
- 작성/수정 성공 시 목록 refresh가 필요한 event를 emit한다.
|
|
- 검증:
|
|
- `CreatorChannelFanTalkDetailViewModelTest` PASS.
|
|
|
|
---
|
|
|
|
### Phase 3: 상세 Activity/layout 구현
|
|
|
|
- [x] **Task 3.1: FanTalk 상세 layout 추가**
|
|
- 생성:
|
|
- `activity_creator_channel_fantalk_detail.xml`
|
|
- 필요 시 `item_creator_channel_fantalk_detail_reply.xml`
|
|
- 작업:
|
|
- `activity_creator_channel_community_reply.xml`과 동일한 title-bar/input/list 구조를 만든다.
|
|
- 답글 없음/있음 Figma 상태를 모두 표현한다.
|
|
- 답글 card는 최대 1개만 표시한다.
|
|
- input field는 `maxLines=3`, vertical scroll, 하단 고정을 적용한다.
|
|
- disabled send: `gray_900` + `ic_new_arrow_up_gray`.
|
|
- enabled send: `soda_400` + `ic_new_arrow_up_white`.
|
|
- 기존 답글 대체 안내는 `V2ModalDialog`로 표시한다.
|
|
- 검증:
|
|
- `./gradlew :app:mergeDebugResources`
|
|
- layout inflate/source 계약 테스트.
|
|
|
|
- [x] **Task 3.2: FanTalk 상세 Activity 구현**
|
|
- 생성:
|
|
- `CreatorChannelFanTalkDetailActivity.kt`
|
|
- 수정:
|
|
- `AndroidManifest.xml`
|
|
- `AppDI.kt`
|
|
- 작업:
|
|
- `newIntent(context, creatorId, item)` 성격의 factory를 제공하고 FanTalk data model을 통째로 전달한다.
|
|
- 개별 primitive extra 분해 전달은 사용하지 않는다.
|
|
- 유효하지 않은 `creatorId`/`fanTalkId`면 toast 후 종료한다.
|
|
- 뒤로가기, input text change, send click, keyboard show/hide, event observe를 연결한다.
|
|
- 기존 답글이 있는 상태의 send event에서 대체 안내 모달을 표시하고, 확인 시 ViewModel의 수정 confirm action을 호출한다.
|
|
- 작성 성공 시 `setResult(Activity.RESULT_OK)` 후 현재 화면 state를 갱신하거나 목록 refresh를 요청한다.
|
|
- 검증:
|
|
- `./gradlew :app:compileDebugKotlin`
|
|
- source test로 manifest/AppDI/newIntent 계약 확인.
|
|
|
|
---
|
|
|
|
### Phase 4: FanTalk 목록 진입점 연결
|
|
|
|
- [x] **Task 4.1: Adapter/Fragment callback 추가**
|
|
- 수정:
|
|
- `CreatorChannelFanTalkAdapter.kt`
|
|
- `CreatorChannelFanTalkFragment.kt`
|
|
- 작업:
|
|
- item click callback을 adapter에 추가한다.
|
|
- Fragment Host에 `onCreatorChannelFanTalkItemClicked(item)` 성격의 callback을 추가한다.
|
|
- 본인 채널이 아닌 경우 item click callback을 연결하지 않거나 no-op 처리한다.
|
|
- 검증:
|
|
- source test로 report/more click과 item click callback이 분리되어 있는지 확인한다.
|
|
|
|
- [x] **Task 4.2: ActivityResult 기반 상세 화면 연결**
|
|
- 수정:
|
|
- `CreatorChannelActivity.kt`
|
|
- 작업:
|
|
- FanTalk 상세 ActivityResult launcher를 추가한다.
|
|
- 상세에서 답글 작성/수정/삭제 성공 result를 받으면 `refreshCreatorChannelFanTalk()`와 `homeActionDelegate?.refreshHome()`를 호출한다.
|
|
- 로그인 토큰이 없으면 기존 `showLoginActivity()`를 호출하고 상세를 열지 않는다.
|
|
- 검증:
|
|
- `CreatorChannelActivitySourceTest`에 owner gate, launcher, refresh 계약 추가.
|
|
|
|
---
|
|
|
|
### Phase 5: 기존 답글 대체/수정/삭제 흐름
|
|
|
|
- [x] **Task 5.1: 기존 답글 대체 모달 구현**
|
|
- 수정:
|
|
- `CreatorChannelFanTalkDetailActivity.kt`
|
|
- `CreatorChannelFanTalkDetailViewModel.kt`
|
|
- `strings.xml`, `values-en/strings.xml`, `values-ja/strings.xml`
|
|
- 작업:
|
|
- 기존 답글이 있는 상태에서 send하면 `V2ModalDialog`를 표시한다.
|
|
- 모달 desc는 기존 답글이 신규 답글로 대체된다는 내용을 담는다.
|
|
- 확인 시 `modifyFanTalkReply(replyFanTalkId, trimmedContent, token)`를 호출한다.
|
|
- 취소 시 API를 호출하지 않고 입력값을 유지한다.
|
|
- 검증:
|
|
- ViewModel replacement event/confirm/cancel 테스트.
|
|
- Activity source 계약 테스트.
|
|
|
|
- [x] **Task 5.2: 더보기 popup edit/delete 동작 정리**
|
|
- 수정 가능:
|
|
- `CreatorChannelFanTalkMorePopup.kt`
|
|
- `view_creator_channel_fantalk_more_popup.xml`
|
|
- 작업:
|
|
- `수정하기` click callback을 실제로 연결한다.
|
|
- font는 커뮤니티 popup처럼 `@font/regular` 적용을 검토한다.
|
|
- 삭제는 `V2ModalDialog` 확인 후 `modifyCheers(isActive = false)` wrapper로 연결한다.
|
|
- 검증:
|
|
- popup source/layout 계약 테스트.
|
|
|
|
- [x] **Task 5.3: 더보기 수정 모드 구현 여부 확정**
|
|
- 작업:
|
|
- 커뮤니티 답글 화면과 동일하게 하단 input 수정 모드를 사용할지 확정한다.
|
|
- 확정 시 ViewModel edit target, X 취소 button, 변경 없음 toast를 구현 계획에 반영한다.
|
|
- 검증:
|
|
- 확정된 범위에 맞춰 ViewModel 테스트를 추가한다.
|
|
|
|
---
|
|
|
|
### Phase 6: 통합 검증
|
|
|
|
- [x] **Task 6.1: targeted 테스트 실행**
|
|
- 실행:
|
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`
|
|
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"`
|
|
- 기대 결과:
|
|
- FanTalk 상세 ViewModel/source/layout 계약 테스트가 PASS한다.
|
|
|
|
- [x] **Task 6.2: build/resource/lint 검증**
|
|
- 실행:
|
|
- `./gradlew :app:mergeDebugResources`
|
|
- `./gradlew :app:compileDebugKotlin`
|
|
- `./gradlew :app:ktlintCheck`
|
|
- `git diff --check`
|
|
- 기대 결과:
|
|
- resource merge, Kotlin compile, ktlint, whitespace 검증이 모두 통과한다.
|
|
|
|
- [x] **Task 6.3: 수동 확인**
|
|
- 확인:
|
|
- 본인 채널에서 FanTalk item 터치 시 상세 진입.
|
|
- 타인 채널에서 FanTalk item 터치 시 상세 미진입.
|
|
- 답글 없음 UI가 Figma `669:40462`와 주요 구조 일치.
|
|
- 답글 있음 UI가 Figma `669:40468`와 주요 구조 일치.
|
|
- 입력값 없음 send disabled: `gray_900` + `ic_new_arrow_up_gray`.
|
|
- 입력값 있음 send enabled: `soda_400` + `ic_new_arrow_up_white`.
|
|
- 기존 답글이 없으면 send 시 답글 작성 API 호출.
|
|
- 기존 답글이 있으면 send 시 대체 안내 모달 표시 후 확인 시 답글 수정 API 호출.
|
|
- 답글 작성/수정 성공 후 FanTalk 목록과 홈 요약 refresh.
|
|
|
|
---
|
|
|
|
## Verification Log
|
|
|
|
- 2026-07-09: 문서 작성만 수행했다. `sample-prd.md`, `docs/agent-guides/work-plan-docs.md`, 기존 `FanTalk 글 입력` 문서, 기존 `커뮤니티 게시물 상세 / 댓글 / 답글` 문서, `CreatorChannelCommunityReplyActivity`, `activity_creator_channel_community_reply.xml`, FanTalk Fragment/Adapter/ViewModel/API wrapper를 확인했다.
|
|
- 2026-07-09: Figma MCP로 `669:40462`, `669:40468` design context와 screenshot을 확인했다. 답글 없음/있음 구조, 하단 input bar, popup 문구를 문서에 반영했다.
|
|
- 2026-07-09: 이번 요청은 “문서만 작성”이므로 Gradle build/test/ktlint는 실행하지 않았다.
|
|
- 2026-07-09: 사용자 보완 요청을 반영했다. FanTalk 상세 조회 API 없음, item data model 통째 전달, 답글 1개 제한, 기존 답글 대체 안내 모달, 확인 시 답글 수정 API 호출, `ic_new_arrow_up_*` 리소스 사용, 답글 pagination/API 불필요 정책을 PRD와 계획/TASK에 반영했다. 문서 보완만 수행했으므로 Gradle build/test/ktlint는 실행하지 않았다.
|
|
- 2026-07-09: 비-UI 기반 TDD 작업을 수행했다. RED 단계에서 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkFragmentLayoutTest"`를 실행했고, `CreatorChannelFanTalkDetailViewModel`, `CreatorChannelFanTalkDetailPayload`, `writeFanTalkReply`/`modifyFanTalkReply` wrapper 등 누락 계약으로 `compileDebugUnitTestKotlin`이 실패하는 것을 확인했다.
|
|
- 2026-07-09: GREEN 단계에서 `CreatorChannelRepository` FanTalk reply wrapper, `CreatorChannelFanTalkDetailPayload`/reply payload, `CreatorChannelFanTalkDetailViewModel`, FanTalk Adapter item click callback, Fragment Host callback을 구현했다. `@Parcelize` payload의 factory는 Parcelize 대상 class 밖 top-level 함수로 두어 컴파일 안정성을 확인했다.
|
|
- 2026-07-09: 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModelTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*FanTalk*"`를 실행했고 모두 성공했다. `lsp_diagnostics`는 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행할 수 없었다.
|
|
- 2026-07-09: 상세 Activity/layout 및 목록 진입 연결을 구현했다. `CreatorChannelFanTalkDetailActivity`, `activity_creator_channel_fantalk_detail.xml`, Manifest/AppDI 등록, `CreatorChannelActivity` owner gate와 ActivityResult refresh 계약을 추가했다. 당시 `writeCheers` 응답은 `ApiResponse<Any>`라 신규 답글 id를 받을 수 없어, 작성 성공 직후 로컬 임시 답글(`fanTalkId=0`)에는 수정/삭제 더보기 메뉴를 숨기도록 보정했다. 이후 후속 요청으로 `writeCheers`/`modifyCheers` 응답은 `ApiResponse<CreatorChannelFanTalkReplyResponse>`로 변경되었다.
|
|
- 2026-07-09: 최종 검증으로 `./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.CreatorChannelActivitySourceTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check`를 실행했다. 모두 성공했고, 병렬 Gradle 실행 중 Kotlin daemon incremental cache 충돌 로그가 있어 이후 검증은 순차 실행으로 재확인했다. 실제 기기/에뮬레이터 수동 조작은 이 환경에서 수행하지 못해 source/layout/unit/resource/compile 검증으로 대체했다.
|
|
|
|
- 2026-07-09: UI/통합 단계 TDD를 수행했다. RED 단계에서 `CreatorChannelFanTalkDetailUiContractTest`와 `CreatorChannelActivitySourceTest` 계약을 먼저 추가하고 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`를 실행했다. 최초 테스트 문자열 escaping 오류를 수정한 뒤 production 변경 전 재실행에서 신규 상세 Activity 파일 없음, 상세 layout 파일 없음, Manifest/DI/ViewModel 계약 누락으로 3개 테스트 실패를 확인했다.
|
|
- 2026-07-09: GREEN 단계에서 `CreatorChannelFanTalkDetailActivity`, `activity_creator_channel_fantalk_detail.xml`, Manifest activity 등록, Koin ViewModel 등록, `CreatorChannelFanTalkDetailViewModel.consumeReplyChangedEvent()`/`cancelReplyEdit()`, `CreatorChannelActivity`의 owner-gated detail ActivityResult launcher를 구현했다. 상세 화면은 기존 커뮤니티 답글 input/send token과 FanTalk reply background를 재사용했고 별도 FanTalk 상세 API는 추가하지 않았다.
|
|
- 2026-07-09: 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`를 실행했고 모두 성공했다. `lsp_diagnostics`는 `kotlin-lsp` 미설치 및 XML LSP 미구성으로 실행할 수 없었다.
|
|
- 2026-07-09: 리뷰 게이트에서 지적된 차단 이슈 2건을 수정했다. FanTalk 상세 원글 more icon은 원글 수정/삭제를 이번 범위에서 지원하지 않으므로 표시하지 않게 변경했고, 답글 작성 직후 API 응답에 실제 답글 id가 없어 로컬 임시 답글(`fanTalkId=0`) 상태에서 다시 submit할 때 수정 API가 조용히 무시되는 문제는 오류 toast를 표시하고 대체 모달/수정 API로 진행하지 않도록 guard를 추가했다. 회귀 테스트 `작성 직후 임시 답글은 대체 submit 시 수정 API를 호출하지 않고 오류를 표시한다`를 추가했다.
|
|
- 2026-07-09: 차단 이슈 수정 후 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModelTest"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`, `git diff --check`를 재실행했고 모두 성공했다. 첫 ViewModel 테스트 실행에서는 Kotlin daemon incremental cache 경고 후 fallback compile로 성공했으며, 이후 순차 재검증은 성공했다. `lsp_diagnostics`는 `kotlin-lsp` 미설치로 실행할 수 없었다.
|
|
- 2026-07-09: Figma `669:40470` 후속 확인에 따라 FanTalk 상세 답글 card의 small profile row nickname/date 타이포그래피를 `Pretendard Variable Regular` 12px 대응 토큰인 `@style/Typography.Caption3`로 맞춘다. 부모 FanTalk nickname/time은 변경하지 않고, `CreatorChannelFanTalkDetailUiContractTest`에서 `tv_creator_channel_fantalk_detail_reply_nickname`와 `tv_creator_channel_fantalk_detail_reply_time`이 `Typography.Caption3`를 사용하는 계약을 검증한다.
|
|
- 2026-07-09: 답글 card nickname/date 타이포그래피 후속 수정 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailUiContractTest" :app:mergeDebugResources`와 `git diff --check`를 실행했고 모두 성공했다. Gradle 실행 중 Kotlin daemon incremental cache 충돌 로그가 있었지만 fallback compile 후 `BUILD SUCCESSFUL`을 확인했다. `lsp_diagnostics`는 `kotlin-lsp` 미설치, XML/Markdown LSP 미구성으로 실행할 수 없었다.
|
|
- 2026-07-09: 답글 card 시간 위치 후속 수정으로 닉네임 TextView의 `layout_weight`를 제거하고 `wrap_content` 폭을 사용하게 변경했으며, 시간 TextView의 `layout_marginStart`를 `@dimen/spacing_4`로 조정해 닉네임 오른쪽 4dp 간격에 붙도록 했다. `CreatorChannelFanTalkDetailUiContractTest`에 닉네임 폭과 시간 간격 계약을 추가했다.
|
|
- 2026-07-09: 답글 card 시간 위치 후속 수정 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailUiContractTest" :app:mergeDebugResources`와 `git diff --check`를 실행했고 모두 성공했다. Gradle 실행 중 Kotlin daemon incremental cache 충돌 로그가 있었지만 fallback compile 후 `BUILD SUCCESSFUL`을 확인했다.
|
|
- 2026-07-09: 답글 card header에서 닉네임과 시간은 4dp 간격으로 붙이고, 더보기 button은 기존처럼 오른쪽 끝에 유지하기 위해 시간과 더보기 사이에 `0dp`/`layout_weight=1` spacer를 추가했다. `CreatorChannelFanTalkDetailUiContractTest`에 spacer 계약을 추가했다.
|
|
- 2026-07-09: 답글 card 더보기 button 오른쪽 정렬 후속 수정 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailUiContractTest" :app:mergeDebugResources`와 `git diff --check`를 실행했고 모두 성공했다. Gradle 실행 중 Kotlin daemon incremental cache 충돌 로그가 있었지만 fallback compile 후 `BUILD SUCCESSFUL`을 확인했다.
|
|
- 2026-07-09: FanTalk 상세 답글 작성 API 응답이 `ApiResponse<CreatorChannelFanTalkReplyResponse>`로 변경된 후속 요청을 반영한다. 기존 로컬 임시 답글(`fanTalkId=0`) 생성 대신 서버가 내려준 답글 `data`를 현재 상세 화면에 즉시 바인딩해 닉네임, 프로필 이미지, 시간을 실제 응답 기준으로 표시하도록 수정하고 검증한다.
|
|
- 2026-07-09: FanTalk 상세 답글 작성 응답 바인딩 후속 수정 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModelTest"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*FanTalk*"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`, `git diff --check`를 실행했고 모두 성공했다. 첫 ViewModel 테스트 재실행에서는 Kotlin daemon incremental cache 충돌 후 fallback compile로 `BUILD SUCCESSFUL`을 확인했다. `lsp_diagnostics`는 `kotlin-lsp` 미설치로 실행할 수 없었다.
|
|
- 2026-07-09: FanTalk 상세 답글 작성 성공 후 toast만 표시되고 반환 데이터가 바인딩되지 않는 문제를 확인했다. 원인은 `handleWriteReplyResponse`가 `response.data.creatorReplies.firstOrNull()`만 답글로 인정해, write API가 생성된 답글 자체를 `data`로 내려주거나 `creatorReplies` 없이 내려주는 성공 응답을 실패로 처리한 것이다. `creatorReplies`가 없으면 `response.data` 자체를 답글 UI 모델로 바인딩하도록 보정하고, 외부 응답의 `creatorReplies`는 nullable로 받아 mapper에서 `orEmpty()`로 처리한다.
|
|
- 2026-07-09: FanTalk 상세 답글 작성 성공 응답 shape 보정 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModelTest"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.*"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources :app:compileDebugKotlin :app:ktlintCheck`, `git diff --check`를 실행했고 모두 성공했다. 첫 ViewModel 테스트 실행에서는 Kotlin daemon incremental cache 충돌 후 fallback compile로 `BUILD SUCCESSFUL`을 확인했고, `lsp_diagnostics`는 `kotlin-lsp` 미설치로 실행할 수 없었다.
|
|
- 2026-07-09: 코드 리뷰 후속 수정으로 FanTalk 답글 작성 wrapper를 V2 `CreatorChannelApi.writeFanTalkReply`가 아니라 요구사항의 기존 `ExplorerRepository.writeCheers(parentCheersId = fanTalkId, ...)` 경로로 되돌렸다. 이후 사용자 후속 요청에 따라 `writeCheers`/`modifyCheers` 반환 타입을 `ApiResponse<CreatorChannelFanTalkReplyResponse>`로 변경했고, 크리에이터 답글 작성 성공 시 반환된 `data`를 현재 상세 답글에 즉시 바인딩한다. `success=true, data=null`이면 성공 처리는 유지하되 임의 답글 데이터는 만들지 않는다.
|
|
- 2026-07-09: 사용자 후속 요청으로 일반 FanTalk 글 작성은 기존처럼 `success` 기준 처리와 목록 새로고침 흐름을 유지하고, 유저가 쓴 FanTalk에 크리에이터가 답글을 추가하는 상세 화면만 반환 `CreatorChannelFanTalkReplyResponse` data를 즉시 UI에 반영하도록 수정한다. `CreatorChannelFanTalkDetailViewModelTest`의 `답글이 없으면 submit 시 반환된 답글 데이터를 현재 state에 바인딩한다`가 해당 계약을 검증한다.
|
|
- 2026-07-09: 코드 리뷰 후속 문자열 수정으로 `CreatorChannelFanTalkReplacementModalUiModel`의 하드코딩 한국어 문구를 string resource id 기반으로 변경했다. title/cancel/confirm은 기존 `screen_user_profile_reply_edit`, `cancel`, `confirm`을 재사용하고, description은 `creator_channel_fantalk_reply_replace_description`을 `values`, `values-en`, `values-ja`에 추가했다. `CreatorChannelFanTalkDetailUiContractTest`에 Activity의 `getString(modal.*ResId)` 사용과 다국어 resource 존재, UI model 내 하드코딩 문구 부재 계약을 추가했다.
|
|
- 2026-07-09: 후속 수정 검증으로 KSP incremental cache 충돌을 피하기 위해 `app/build/kspCaches/debug`, `app/build/kspCaches/debugUnitTest`를 삭제한 뒤 순차 실행했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModelTest"`와 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailUiContractTest"`가 모두 `BUILD SUCCESSFUL`로 통과했다. 최초 병렬 테스트 실행은 KSP cache 동시 접근으로 실패했으나 코드 실패가 아니라 검증 방식 문제로 확인했다.
|
|
- 2026-07-09: `writeCheers`/`modifyCheers` 반환 타입 변경 후 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailViewModelTest"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.detail.CreatorChannelFanTalkDetailUiContractTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.write.CreatorChannelFanTalkWriteViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.fantalk.CreatorChannelFanTalkActionTest"`, `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources :app:compileDebugKotlin`, `git diff --check`를 실행했고 모두 성공했다. 일반 FanTalk 글 작성/삭제 테스트는 반환 data를 사용하지 않고 기존 success 기준 동작이 유지됨을 확인했다.
|