# PRD: 크리에이터 채널 FanTalk 탭 ## 1. Overview 크리에이터 채널의 `FanTalk` 탭에서 팬이 남긴 응원글과 크리에이터 답글을 조회하고, 작성자/채널 소유 여부에 따라 신고 또는 더보기 메뉴를 제공한다. --- ## 2. Problem - 크리에이터 채널에는 `FanTalk` 탭이 존재하지만, 탭 전용 API 계약과 Figma 기반 UI 요구사항이 별도 문서로 정의되어야 한다. - 사용자는 크리에이터 채널 안에서 전체 FanTalk 수와 팬 응원글, 크리에이터 답글을 한 화면에서 확인할 수 있어야 한다. - FanTalk 목록은 길어질 수 있으므로 `hasNext == true`일 때 다음 페이지를 이어서 조회해야 한다. - 내가 쓴 글 또는 내 채널의 글에는 수정/삭제 등 소유자용 액션을 열 수 있는 더보기 버튼이 필요하다. - 그 외 유저가 작성한 글에는 더보기 버튼 대신 신고 버튼만 표시되어야 한다. - 신고 기능은 기존 크리에이터 페이지(`UserProfileActivity`)의 FanTalk 신고 흐름과 동일하게 처리되어야 한다. --- ## 3. Goals - Figma 전체 화면 `290:9139` 기준으로 크리에이터 채널 `FanTalk` 탭 UI 요구사항을 정의한다. - API endpoint `GET /api/v2/creator-channels/{creatorId}/fan-talks`를 기준으로 최초 조회와 pagination 요구사항을 정의한다. - 최초 조회 query parameter 기본값은 `page=0`, `size=20`으로 둔다. - Sort-bar에는 정렬 UI를 표시하지 않고 `전체` label과 `fanTalkCount`만 표시한다. - FanTalk item에는 작성자 프로필 이미지, 닉네임, 작성 시간, 본문을 표시한다. - `createdAtUtc`는 크리에이터 채널 v2 공통 날짜/시간 포맷을 따른다. - FanTalk item에 크리에이터 답글이 있으면 원글 아래에 크리에이터 답글을 최대 1개 표시한다. - 내가 쓴 글일 때 원글 우측 액션은 `ic_new_more` 더보기 버튼으로 표시하고, popup에는 `수정하기`, `삭제하기`를 표시한다. - 내 채널에서 타인이 작성한 글일 때 원글 우측 액션은 `ic_new_more` 더보기 버튼으로 표시하고, popup에는 `삭제하기`만 표시한다. - 내가 쓴 글도 아니고 내 채널도 아닌 경우 원글 우측 액션은 `신고` 버튼으로 표시한다. - 더보기 popup의 `수정하기`는 이번 범위에서 표시만 하고 실제 수정 화면/API는 연결하지 않는다. - 더보기 popup의 `삭제하기`는 기존 FanTalk 삭제 API 흐름에 연결한다. - 신고 버튼 터치 시 기존 `UserProfileFantalkAllViewActivity`의 FanTalk 신고 기능과 동일하게 `CheersReportDialog`를 열고 `ReportType.CHEERS` 신고를 처리한다. - 우측 하단 floating 글쓰기 버튼은 Figma처럼 표시하되, 터치 액션은 후속 작업 범위로 남긴다. - FanTalk가 없는 경우 Figma empty 화면 `290:9000` 기준으로 empty 문구와 `응원 남기기` button을 표시하되, button 터치 액션은 후속 작업 범위로 남긴다. - 응답의 `hasNext`가 `true`이면 현재 `page + 1` 페이지를 추가 로딩한다. --- ## 4. Non-Goals - 크리에이터 채널 상단 header, title bar, 공통 main tab-bar 구조 자체를 재설계하지 않는다. - `홈`, `라이브`, `오디오`, `시리즈`, `화보`, `커뮤니티`, `후원` 탭의 상세 구현은 이번 범위에서 제외한다. - FanTalk 글쓰기 페이지 구현과 floating 글쓰기 버튼의 실제 화면 연결은 이번 범위에서 제외한다. - empty 상태의 `응원 남기기` button 실제 화면 연결은 이번 범위에서 제외한다. - FanTalk 상세 화면이 별도로 필요하더라도 이번 범위에서는 정의하지 않는다. - API schema를 임의 변경하거나 서버 응답 필드명을 클라이언트에서 새로 정의하지 않는다. - Sort-bar에 정렬 label, 정렬 icon, 정렬 popup을 추가하지 않는다. - 더보기 메뉴의 `수정하기` 실제 수정 화면/API 연결은 이번 범위에서 제외한다. - 크리에이터 답글 작성, 수정, 삭제 기능은 이번 범위에서 제외한다. - Figma asset을 localhost URL 그대로 앱 코드에 직접 의존하지 않는다. - 레거시 FanTalk 구현 파일을 직접 수정하지 않는다. 필요한 경우 기존 신고 dialog/repository 흐름을 호출하거나 신규 wrapper/adapter를 추가해 사용한다. --- ## 5. Target Users - 크리에이터 채널에서 팬 응원글을 읽는 앱 사용자. - 특정 크리에이터에게 남겨진 팬 반응과 크리에이터 답글을 확인하려는 사용자. - 본인이 작성한 FanTalk 글을 관리하려는 사용자. - 본인 채널의 FanTalk 글을 관리하려는 크리에이터. - 부적절한 FanTalk 글을 신고하려는 사용자. - `kr.co.vividnext.sodalive.v2` 하위 크리에이터 채널 탭을 구현/유지보수하는 Android 개발자. --- ## 6. User Stories - 사용자는 크리에이터 채널의 `FanTalk` 탭에서 전체 FanTalk 수를 확인하고 싶다. - 사용자는 팬이 남긴 응원글의 작성자, 작성 시간, 본문을 확인하고 싶다. - 사용자는 FanTalk 원글에 달린 크리에이터 답글을 원글 아래에서 확인하고 싶다. - 사용자는 내가 작성하지 않은 FanTalk 글을 부적절하다고 판단하면 신고하고 싶다. - 사용자는 내가 작성한 FanTalk 글에서 더보기 메뉴를 열고 수정/삭제 액션을 확인하고 싶다. - 사용자는 내가 작성한 FanTalk 글을 삭제하고 싶다. - 크리에이터는 내 채널에 타인이 작성한 FanTalk 글에서 더보기 메뉴를 열고 삭제 액션을 확인하고 싶다. - 사용자는 목록 하단까지 스크롤하면 다음 페이지가 자연스럽게 이어서 로딩되길 기대한다. - 사용자는 우측 하단 글쓰기 버튼 또는 empty 상태의 `응원 남기기` button을 보고 FanTalk 작성 진입점이 있음을 인지하고 싶다. --- ## 7. Core Features ### Creator Channel FanTalk Tab API `FanTalk` 탭 진입과 추가 로딩 시 크리에이터별 FanTalk 목록 데이터를 조회한다. #### Requirements - API endpoint는 `GET /api/v2/creator-channels/{creatorId}/fan-talks`이다. - `creatorId`는 path variable로 전달한다. - Query parameters는 `page`, `size`를 사용한다. - 최초 조회 기본값은 `page=0`, `size=20`이다. - 정렬 query parameter는 사용하지 않는다. - `hasNext == true`일 때 다음 페이지 요청은 현재 응답의 `page + 1` 값을 사용한다. - 중복 pagination 요청이 발생하지 않도록 loading 중 추가 요청을 막아야 한다. - 다음 페이지 성공 시 기존 `fanTalks` 뒤에 append한다. #### Response Contract ```kotlin data class CreatorChannelFanTalkTabResponse( val fanTalkCount: Int, val fanTalks: List, val page: Int, val size: Int, @JsonProperty("hasNext") val hasNext: Boolean ) data class CreatorChannelFanTalkResponse( val fanTalkId: Long, val writerId: Long, val writerNickname: String, val writerProfileImageUrl: String, val content: String, val createdAtUtc: String, val creatorReplies: List ) data class CreatorChannelFanTalkReplyResponse( val fanTalkId: Long, val writerId: Long, val writerNickname: String, val writerProfileImageUrl: String, val content: String, val createdAtUtc: String ) ``` #### Edge Cases - 최초 조회 실패 시 기존 크리에이터 채널 탭의 error/retry 패턴을 따른다. - 다음 페이지 로딩 실패 시 기존 목록은 유지하고 기존 pagination 실패 표시 정책을 따른다. - 다음 페이지 응답의 `fanTalks`가 비어 있어도 `hasNext` 값 기준으로 이후 로딩 가능 여부를 갱신한다. - 서버 응답의 `page`, `size`가 요청 상태와 다를 경우 서버 응답 값을 기준으로 ViewModel page 상태를 동기화한다. - `writerProfileImageUrl`이 비어 있거나 이미지 로딩에 실패하면 기존 프로필 이미지 placeholder 정책을 따른다. - `creatorReplies`가 비어 있으면 답글 영역과 답글 연결선을 표시하지 않는다. - `creatorReplies`가 2개 이상이어도 응답 순서 기준 첫 번째 1개만 표시한다. ### Sort Bar without Sort Sort-bar는 전체 FanTalk 수만 표시한다. #### Requirements - Figma 전체 화면 기준 Sort-bar는 `290:9139` 내 `sort-bar`이다. - 좌측에는 `전체`와 `fanTalkCount`를 표시한다. - 우측 정렬 label, 정렬 icon, 정렬 popup 진입 영역은 표시하지 않는다. - FanTalk 탭에서는 정렬 상태를 보관하거나 API query로 전달하지 않는다. #### Edge Cases - `fanTalkCount == 0`이고 전체 empty 상태이면 Figma empty 화면 `290:9000`을 우선하며 Sort-bar를 표시하지 않는다. - 다국어 label 길이가 길어져도 전체 개수와 겹치지 않아야 한다. ### FanTalk List Item FanTalk 목록은 Figma의 feed item 구조를 기준으로 원글과 크리에이터 답글을 표시한다. #### Requirements - Figma 전체 화면 기준 FanTalk item은 `290:9139`의 `feed-list` 내 `feed` 구조를 따른다. - 각 원글에는 `writerProfileImageUrl`, `writerNickname`, `createdAtUtc`, `content`를 표시한다. - `createdAtUtc`는 크리에이터 채널 v2 공통 날짜/시간 포맷을 따른다. - 원글 본문은 Figma처럼 16sp regular text로 표시하고 긴 내용은 자연스럽게 여러 줄로 확장한다. - `creatorReplies`가 있으면 원글 아래에 회색 배경의 답글 card를 표시한다. - 답글 card에는 답글 작성자 프로필 이미지, 닉네임, 작성 시간, 본문을 표시한다. - 답글 card의 작성자는 API 응답의 `writerNickname`, `writerProfileImageUrl`을 사용한다. - 크리에이터 답글은 현재 1개만 추가되는 서버 정책을 기준으로 최대 1개만 표시한다. - `creatorReplies`가 여러 개 내려오더라도 응답 순서 기준 첫 번째 1개만 표시하고 나머지는 표시하지 않는다. - 원글과 답글 사이에는 Figma처럼 연결선을 표시한다. #### Edge Cases - 원글 `content`가 비어 있으면 작성자/작성 시간/우측 액션은 유지하고 본문 텍스트 영역은 빈 문자열로 표시한다. - 답글 `content`가 비어 있으면 답글 card는 표시하되 본문 텍스트 영역은 빈 문자열로 표시한다. - 닉네임이 긴 경우 프로필 영역에서 말줄임 처리한다. - `createdAtUtc` 파싱 실패 시 기존 날짜 표시 fallback 정책을 따른다. ### Item Right Action FanTalk 원글 우측 액션은 사용자 권한에 따라 더보기 또는 신고로 분기한다. #### Requirements - 현재 로그인 사용자의 id와 `writerId`가 같으면 내가 쓴 글로 판단한다. - 현재 크리에이터 채널이 내 채널이면 내 채널로 판단한다. - 내가 쓴 글이거나 내 채널에서 타인이 작성한 글인 경우 우측 액션은 더보기 버튼으로 표시한다. - 더보기 버튼 icon은 `ic_new_more`를 사용한다. - 더보기 버튼을 누르면 Figma popup 메뉴처럼 권한별 메뉴를 표시한다. - 내가 쓴 글의 popup 메뉴 label은 `수정하기`, `삭제하기`이다. - 내 채널에서 타인이 작성한 글의 popup 메뉴 label은 `삭제하기`만 표시한다. - 내가 쓴 글이면서 내 채널의 글인 경우 내가 쓴 글 기준으로 `수정하기`, `삭제하기`를 표시한다. - `수정하기`는 표시만 하고 이번 범위에서는 실제 수정 화면/API를 연결하지 않는다. - `삭제하기`는 기존 FanTalk 삭제 API 흐름에 연결한다. - 기존 삭제 API 흐름은 레거시 FanTalk 삭제와 동일하게 `PutModifyCheersRequest(cheersId = fanTalkId, isActive = false)`를 `ExplorerRepository.modifyCheers()` / `PUT /explorer/profile/cheers`로 요청하는 방식이다. - 삭제 전에는 기존 FanTalk 삭제와 동일하게 `SodaDialog` 확인 dialog를 표시한다. - 삭제 성공 후에는 목록을 새로고침하거나 해당 item을 제거해 삭제 결과가 화면에 반영되어야 한다. - 내가 쓴 글도 아니고 내 채널도 아닌 경우 우측 액션은 `신고` 버튼으로 표시한다. - 신고 버튼은 Figma 기준 14sp medium gray text button으로 표시한다. #### Edge Cases - 로그인 사용자 id를 확인할 수 없는 경우 내가 쓴 글로 판단하지 않는다. - 내 채널 여부를 확인할 수 없는 경우 내 채널로 판단하지 않는다. - `수정하기` 터치 시에는 API를 호출하지 않고 수정 화면으로 이동하지 않는다. - 삭제 확인 dialog에서 취소하면 삭제 API를 호출하지 않는다. - 삭제 API 실패 시 기존 FanTalk 삭제 흐름과 동일한 toast/error 정책을 따른다. - 더보기 popup은 화면 우측 또는 하단을 벗어나지 않아야 한다. - 더보기 popup이 열린 상태에서 다른 item의 액션을 누르면 기존 popup을 닫고 새 액션을 처리한다. ### FanTalk Report 신고 버튼 터치 시 기존 FanTalk 신고 기능과 동일한 신고 dialog와 신고 API 계약을 사용한다. #### Requirements - 신고 버튼 터치 시 기존 `UserProfileFantalkAllViewActivity.showCheersReportPopup()` 흐름과 동일하게 `CheersReportDialog`를 표시한다. - 신고 사유를 선택하지 않고 신고하면 `screen_user_profile_fantalk_report_reason_required` 문구를 사용한다. - 신고 요청은 기존 FanTalk 신고와 동일하게 `ReportRequest(type = ReportType.CHEERS, reason = reason, cheersId = fanTalkId)` 계약을 사용한다. - 신고 성공 toast는 기존 신고 흐름의 응답 message 또는 `character_comment_report_submitted` 문구를 따른다. - 신고 API 호출은 기존 `ReportRepository.report()` 흐름을 재사용한다. #### Edge Cases - 신고 dialog를 취소하면 API를 호출하지 않는다. - 신고 API 실패 시 기존 FanTalk 신고 흐름과 동일한 toast/error 정책을 따른다. - 이미 신고한 글에 대한 서버 오류 또는 중복 신고 메시지는 서버 응답 message를 우선 표시한다. ### Floating Write Button 우측 하단 floating 글쓰기 버튼은 표시만 하고 실제 연결은 후속 작업으로 남긴다. #### Requirements - Figma 전체 화면 기준 우측 하단 `button-floating`을 표시한다. - 버튼은 목록 위에 floating 형태로 배치한다. - 버튼 icon과 색상은 Figma와 대응되는 기존 프로젝트 asset/token을 우선 사용한다. - 버튼 터치 이벤트는 후속 FanTalk 글쓰기 페이지 구현 후 연결한다. - 이번 범위에서는 버튼 터치 시 실제 글쓰기 화면을 열지 않고 별도 API도 호출하지 않는다. #### Edge Cases - 목록 하단 item, 시스템 navigation bar, keyboard와 버튼이 겹치지 않아야 한다. - 빈 목록 상태에서는 Figma empty 화면 `290:9000`을 기준으로 우측 하단 floating 글쓰기 버튼 대신 중앙 `응원 남기기` capsule button을 표시한다. ### Pagination FanTalk 목록은 스크롤 하단 접근 시 다음 페이지를 로딩한다. #### Requirements - `CreatorChannelFanTalkTabResponse.hasNext == true`일 때만 다음 페이지를 요청한다. - 다음 페이지는 마지막 성공 응답의 `page + 1`로 요청한다. - 다음 페이지 요청에는 `size=20`을 유지한다. - 다음 페이지 로딩 중에는 추가 page 요청을 중복으로 보내지 않는다. - 다음 페이지 성공 시 기존 `fanTalks` 뒤에 append한다. #### Edge Cases - 빠른 스크롤로 load-more trigger가 반복 발생해도 page가 중복 append되지 않아야 한다. - Fragment/View 재생성 후 현재 목록과 page 상태는 ViewModel 상태 보존 정책에 따라 유지되어야 한다. - 마지막 페이지 응답 이후 `hasNext == false`이면 이후 load-more trigger를 무시한다. ### Empty State FanTalk가 없으면 목록 대신 empty 상태를 표시한다. #### Requirements - `fanTalkCount == 0` 또는 표시 가능한 `fanTalks`가 없는 전체 empty 상태이면 empty 상태를 표시한다. - empty 상태에서는 FanTalk 목록을 표시하지 않는다. - empty 상태에서는 Figma empty 화면 `290:9000` 기준으로 Sort-bar를 표시하지 않는다. - empty 문구는 화면 중앙에 16sp regular, `gray/500` 계열, center 정렬로 표시한다. - empty 문구 한국어는 `아직 응원이 없습니다.\n처음으로 크리에이터를 응원해 보세요!`이다. - empty 문구 영어는 `No cheers yet.\nBe the first to cheer for the creator!`이다. - empty 문구 일본어는 `まだ応援がありません。\n最初にクリエイターを応援してみましょう!`이다. - empty 문구는 한국어/영어/일본어 다국어 문자열 리소스로 관리한다. - empty 상태에서는 Figma `290:9000`처럼 문구 아래에 `응원 남기기` capsule button을 표시한다. - empty 상태의 `응원 남기기` button은 `gray/800` 배경, 100dp 수준 radius, 20dp icon, 16sp medium white text를 기준으로 한다. - empty 상태의 `응원 남기기` button 터치 액션은 후속 FanTalk 글쓰기 페이지 구현 후 연결한다. - 이번 범위에서는 empty 상태의 `응원 남기기` button 터치 시 실제 글쓰기 화면을 열지 않고 별도 API도 호출하지 않는다. #### Edge Cases - API 최초 조회 실패 상태는 empty 상태로 취급하지 않고 기존 error/retry 패턴을 따른다. - `fanTalkCount > 0`이지만 첫 페이지 `fanTalks`가 비어 있고 표시 가능한 item이 없으면 empty 상태를 표시하되, 전체 개수는 서버 응답의 `fanTalkCount` 값을 유지한다. --- ## 8. UX / UI Expectations - 전체 화면은 기존 크리에이터 채널 컨테이너의 black background, sticky tab-bar, title-bar 동작을 유지한다. - `FanTalk` main tab은 선택 상태로 표시하고, 선택 underline과 텍스트 색상은 기존 tab-bar 정책을 따른다. - Sort-bar 높이와 배치는 Figma `290:9139`를 기준으로 하되 정렬 UI는 제거한다. - FanTalk item은 black background 위에 white/gray text hierarchy를 유지한다. - 답글 card는 Figma처럼 `gray/900` 계열 배경과 radius를 적용해 원글과 구분한다. - 원글 작성자 프로필 이미지는 42dp 원형, 답글 작성자 프로필 이미지는 20dp 원형 수준을 기준으로 한다. - item 간 divider 또는 spacing은 Figma 구조를 기준으로 하되 기존 RecyclerView item 간격 정책과 충돌하지 않게 맞춘다. - 우측 액션 영역은 더보기 icon 또는 신고 text 중 하나만 표시한다. - popup 메뉴는 Figma처럼 원글 우측 액션 근처에 표시하고 `수정하기`, `삭제하기` 항목을 세로로 배치한다. - floating 글쓰기 버튼은 우측 하단 14dp margin 수준으로 표시하고 목록 스크롤과 독립적으로 유지한다. - empty 상태는 Figma `290:9000`처럼 tab-bar 아래 black background 중앙에 empty 문구와 `응원 남기기` capsule button을 세로로 배치한다. --- ## 9. Technical Constraints - Android Gradle 프로젝트의 `:app` 모듈에서 구현한다. - 신규 `Fragment`, `ViewModel`, adapter, mapper, DTO 등은 `kr.co.vividnext.sodalive.v2` 하위에 작성한다. - 레거시 파일은 직접 수정하지 않고, 기존 기능이 필요하면 기존 class를 호출하거나 신규 wrapper/adapter를 추가한다. - 기존 `CreatorChannelActivity`의 `ViewPager2`/`CreatorChannelPagerAdapter` 구조를 유지한다. - API/Repository는 기존 크리에이터 채널 공통 data 계층 패턴을 우선 따른다. - 신고 dialog/API는 기존 `CheersReportDialog`, `ReportRepository`, `ReportType.CHEERS` 계약을 재사용한다. - `fanTalkId`는 신고 요청의 `cheersId`로 전달한다. - FanTalk 삭제는 기존 `ExplorerRepository.modifyCheers()`와 `PutModifyCheersRequest(cheersId = fanTalkId, isActive = false)` 계약을 재사용한다. - 정렬 관련 `ContentSort`, `CreatorChannelSortPopup`은 FanTalk 탭에서 사용하지 않는다. - 이미지 로딩, 날짜 포맷, toast/error/retry, pagination trigger는 기존 크리에이터 채널 탭 구현 패턴을 따른다. - 문자열은 다국어 리소스로 관리하고 hardcoded user-facing text를 최소화한다. - Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다. --- ## 10. Metrics - FanTalk 탭 최초 조회 성공률. - FanTalk 탭 최초 조회 API latency. - FanTalk pagination 성공률과 중복 요청 발생 여부. - 신고 버튼 노출 조건 정확도. - 더보기 버튼 노출 조건 정확도. - 삭제 action 성공률과 삭제 후 목록 반영 정확도. - FanTalk 신고 성공률. - FanTalk 탭 empty/error 상태 노출 빈도. - floating 글쓰기 버튼 클릭 수는 후속 글쓰기 페이지 연결 시 측정한다. --- ## 11. Open Questions - 현재 확정 대기 중인 항목 없음. --- ## 12. Resolved Decisions - 더보기 popup에서 `수정하기`는 표시만 하고 실제 수정 화면/API는 연결하지 않는다. - 더보기 popup에서 `삭제하기`는 기존 FanTalk 삭제 API 흐름에 연결한다. - 내 채널에서 타인이 작성한 FanTalk 글의 더보기 popup에는 `삭제하기`만 표시한다. - `createdAtUtc`는 크리에이터 채널 v2 공통 날짜/시간 포맷을 따른다. - empty 문구는 한국어/영어/일본어 문자열 리소스로 관리한다. - floating 글쓰기 버튼과 empty 상태의 `응원 남기기` button은 이번 범위에서 표시만 하고 터치 액션은 후속 범위에서 연결한다. - `creatorReplies`는 최대 1개만 표시한다.