docs(creator): 채널 커뮤니티 탭 문서를 추가한다

This commit is contained in:
2026-06-21 19:28:30 +09:00
parent 1dc6aa5283
commit 6bff74cd1e
2 changed files with 795 additions and 0 deletions

View File

@@ -0,0 +1,471 @@
# 크리에이터 채널 커뮤니티 탭 구현 계획/TASK
> **For agentic workers:** 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.
**Goal:** `GET /api/v2/creator-channels/{creatorId}/community` 응답을 기반으로 크리에이터 채널의 `커뮤니티` 탭에 리스트/썸네일 보기 전환, 커뮤니티 게시글 목록, 유료/구매/본인 채널 표시 분기, 오디오 재생, empty 상태, 본인 채널 하단 `커뮤니티 글 올리기` CTA와 pagination을 표시한다.
**Architecture:** 기존 `CreatorChannelActivity``ViewPager2`/`CreatorChannelPagerAdapter` 구조를 유지하고, `CreatorChannelTab.Community`의 placeholder를 신규 `CreatorChannelCommunityFragment`로 교체한다. 커뮤니티 탭 전용 Fragment/ViewModel/DTO/mapper/adapter는 `kr.co.vividnext.sodalive.v2.creator.channel.community` 하위에 두되, API/Repository는 기존 채널 공통 `CreatorChannelApi`/`CreatorChannelRepository`에 endpoint만 추가한다. 리스트형은 v2 `FeedCommunityView` 재사용 가능성을 우선 검토하되 본인 채널 우측 액션, 댓글 불가 숨김, 중앙 재생 버튼이 기존 홈 피드에 영향을 주면 커뮤니티 탭 전용 item layout으로 제한한다.
**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test.
---
## 전제와 성공 기준
- PRD: `docs/20260621_크리에이터_채널_커뮤니티_탭/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`
- 기존 커뮤니티/피드 참조:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt`
- `app/src/main/res/layout/view_feed_community.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllGridAdapter.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityPostMenuBottomSheetDialog.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/player/CreatorCommunityMediaPlayerManager.kt`
- Figma:
- 전체 리스트형: `290:9061`
- 전체 썸네일형: `290:9073`
- 유료이고 구매하지 않은 게시글: `290:9066`
- 전체 리스트형 + 본인 채널: `665:19021`
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/community`이다.
- 첫 페이지 `page``0`, 기본 `size``20`이다.
- 보기 방식은 클라이언트 UI 상태이며 API query parameter로 보내지 않는다.
- 기본 보기 방식은 리스트형이며 label/icon은 `리스트형`/`ic_new_list`이다.
- 토글 후 썸네일형 label/icon은 `썸네일형`/`ic_new_grid`이다.
- 게시글 item 자체를 터치해도 아무 동작을 수행하지 않는다.
- 본인 또는 구매한 사용자의 오디오 게시글 재생은 기존 `CreatorCommunityMediaPlayerManager`를 재사용한다.
- 본인 채널에 본인이 쓴 리스트형 게시글에서만 우측 상단 더보기와 유료 가격을 표시한다.
- 댓글 불가 게시글은 댓글 icon과 댓글 수를 모두 숨긴다.
- 본인 채널이면 하단 고정 `커뮤니티 글 올리기` CTA를 표시하고 기존 `CreatorCommunityWriteActivity` 진입을 우선 재사용한다.
- 구현 완료 후 최소 다음 명령을 실행한다.
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"`
- `./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: 제한 참조
- 기존 코드 경계, 위젯 재사용 가능성, 기존 커뮤니티 정책 확인이 중심이며 Figma는 PRD 기준만 확인한다.
- Phase 2: Figma 참조 불필요
- API/DTO/Repository/ViewModel 상태 모델은 서버 계약과 기존 오디오/시리즈 탭 패턴을 따른다.
- Phase 3: 제한 참조
- mapper와 item state는 PRD, 기존 `FeedCommunityView`, 기존 `CreatorCommunityAllGridAdapter` 정책을 함께 확인한다.
- Phase 4: 필수 참조
- 리스트형 item, 유료 미구매 잠금 영역, 썸네일형 grid item, Sort-bar는 Figma `290:9061`, `290:9073`, `290:9066`, `665:19021` 기준으로 구현한다.
- Phase 5: 제한 참조
- 탭 연결, pagination, owner CTA, media player 연결은 기존 코드 패턴 중심으로 검증한다.
- Phase 6: 필수 참조
- 최종 수동 화면 검증은 PRD의 모든 Figma 노드와 실제 화면을 대조한다.
---
## 파일 구조
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelPagerAdapter.kt`
- `CreatorChannelTab.Community`를 신규 `CreatorChannelCommunityFragment`로 연결한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- `CreatorChannelCommunityFragment.Host` 구현, 커뮤니티 탭 선택 시 최초 로드, pagination trigger, ViewPager 높이 갱신, owner CTA 표시/클릭, media player 생명주기 연결을 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt`
- 커뮤니티 탭 endpoint를 추가한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelRepository.kt`
- 커뮤니티 탭 repository method를 추가한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/data/CreatorChannelCommunityTabResponse.kt`
- `CreatorChannelCommunityTabResponse`, `CreatorChannelCommunityPostResponse`를 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityViewModel.kt`
- 최초 조회, retry, pagination, 보기 방식 상태, loading/error/empty/content 상태를 관리한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt`
- 보기 방식, 게시글 item, 잠금/오디오/owner action 상태, 화면 상태 UI model을 정의한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt`
- DTO를 UI model로 변환하고 유료/구매/본인/댓글 가능/썸네일 preview 정책을 결정한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt`
- 커뮤니티 탭 UI, 보기 방식 토글, adapter 전환, pagination error toast, media player callback, host callback 연결을 담당한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt`
- 리스트형 RecyclerView adapter를 담당한다.
- 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt`
- 썸네일형 3열 RecyclerView adapter를 담당한다.
- 생성: `app/src/main/res/layout/fragment_creator_channel_community.xml`
- Sort-bar, RecyclerView, empty/error/retry 영역을 포함한다.
- 생성: `app/src/main/res/layout/item_creator_channel_community_list.xml`
- 리스트형 커뮤니티 item을 구현한다. 필요 시 `FeedCommunityView` 대신 전용 layout으로 만든다.
- 생성: `app/src/main/res/layout/item_creator_channel_community_grid.xml`
- 썸네일형 정사각형 grid item을 구현한다.
- 수정 가능: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt`, `app/src/main/res/layout/view_feed_community.xml`
- 공통 위젯을 재사용하는 편이 더 작다고 판단되는 경우에만 optional bind flag를 추가한다. 기존 홈/추천 피드 동작이 변하면 안 된다.
- 수정: `app/src/main/res/values/strings.xml`, `app/src/main/res/values-en/strings.xml`, `app/src/main/res/values-ja/strings.xml`
- 보기 방식 label, empty/error/retry/notice/CTA 문구를 추가 또는 기존 문자열 재사용으로 정리한다.
- 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
- `CreatorChannelCommunityViewModel` binding을 추가한다.
- 테스트 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityViewModelTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityPaginationTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragmentLayoutTest.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: Community 탭 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`
- 작업:
- `CreatorChannelTab.Community`가 현재 `CreatorChannelPlaceholderFragment`로 연결되는지 확인한다.
- 신규 `CreatorChannelCommunityFragment.newInstance(creatorId)`로 교체할 위치를 고정한다.
- 검증:
- `rg -n "CreatorChannelTab\\.Community|CreatorChannelPlaceholderFragment|createFragment" app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel`
- 기대 결과: Community 탭 placeholder와 관련 테스트 갱신 지점이 확인된다.
- [ ] **Task 1.2: v2 `FeedCommunityView` 재사용 가능성 확인**
- 확인:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt`
- `app/src/main/res/layout/view_feed_community.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelHomeSectionAdapter.kt`
- 작업:
- `FeedItem.Community``creatorProfileUrl`, `imageUrl`, `audioUrl`, `price`, `existOrdered` 매핑에 충분한지 확인한다.
- `FeedCommunityView`의 유료 미구매 잠금 overlay는 재사용 후보로 둔다.
- 댓글 불가 숨김, 본인 채널 우측 상단 더보기/가격, 이미지 중앙 재생 버튼이 기존 홈/추천 피드에 영향을 줄 수 있으면 전용 item layout으로 구현한다.
- 검증:
- `rg -n "FeedCommunityView|FeedItem\\.Community|existOrdered|ll_feed_community_paid_overlay|tv_feed_community_comment_count" app/src/main/java app/src/main/res/layout/view_feed_community.xml`
- 기대 결과: 공통 위젯 수정 여부와 전용 layout 필요성이 기록된다.
- [ ] **Task 1.3: 기존 커뮤니티 그리드/더보기/재생 정책 확인**
- 확인:
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityAllGridAdapter.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/CreatorCommunityPostMenuBottomSheetDialog.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/player/CreatorCommunityMediaPlayerManager.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/all/player/CreatorCommunityContentItem.kt`
- 작업:
- 썸네일형 text preview는 기존 `CreatorCommunityAllGridAdapter.CONTENT_PREVIEW_MAX_LENGTH = 24` 정책을 우선 따른다.
- 우측 더보기는 기존 `CreatorCommunityPostMenuBottomSheetDialog`의 수정/삭제/고정/고정 해제 액션 정책을 따른다.
- 재생은 `CreatorCommunityMediaPlayerManager.toggleContent(CreatorCommunityContentItem(postId, audioUrl))`를 사용한다.
- 검증:
- `rg -n "CONTENT_PREVIEW_MAX_LENGTH|CreatorCommunityPostMenuBottomSheetDialog|CreatorCommunityMediaPlayerManager|CreatorCommunityContentItem|toggleContent" app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community`
- 기대 결과: 기존 그리드 preview, 더보기 메뉴, 재생 manager 사용 방식이 확인된다.
- [ ] **Task 1.4: Owner CTA 진입점과 리소스 확인**
- 확인:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- `app/src/main/res/layout/activity_creator_channel.xml`
- `app/src/main/res/values/strings.xml`
- `app/src/main/res/values-en/strings.xml`
- `app/src/main/res/values-ja/strings.xml`
- 작업:
- 기존 `onOwnerFabCommunityClicked()``CreatorCommunityWriteActivity`를 여는지 확인한다.
- 하단 고정 owner CTA도 Community 탭에서 같은 method를 호출하도록 연결 계획을 고정한다.
- `ic_new_upload_community_post`, `creator_channel_owner_fab_community` 리소스 존재를 확인한다.
- 검증:
- `rg -n "onOwnerFabCommunityClicked|CreatorCommunityWriteActivity|ic_new_upload_community_post|creator_channel_owner_fab_community" app/src/main/java app/src/main/res`
- 기대 결과: 기존 커뮤니티 작성 진입점과 icon/string 리소스가 확인된다.
---
### Phase 2: API/DTO/Repository/ViewModel 계약 추가
- [ ] **Task 2.1: 커뮤니티 탭 DTO 추가**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/data/CreatorChannelCommunityTabResponse.kt`
- 작업:
- `@Keep`, `@SerializedName` 기반으로 `CreatorChannelCommunityTabResponse`, `CreatorChannelCommunityPostResponse`를 추가한다.
- `CreatorChannelCommunityPostResponse`에는 `postId`, `creatorId`, `creatorNickname`, `creatorProfileUrl`, `createdAtUtc`, `content`, `imageUrl`, `audioUrl`, `price`, `existOrdered`, `isCommentAvailable`, `likeCount`, `commentCount`, `isPinned`를 포함한다.
- `@JsonProperty`가 아닌 프로젝트 기존 Gson 패턴인 `@SerializedName`을 사용한다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- 신규 DTO 추가 후 컴파일이 PASS한다.
- [ ] **Task 2.2: 커뮤니티 탭 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}/community")` endpoint를 추가한다.
- query parameter `page`, `size`만 전달한다.
- Repository method는 `getCommunity(creatorId, page, size, token)` 형태로 둔다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- API/Repository 추가 후 기존 Koin graph와 충돌 없이 컴파일된다.
- [ ] **Task 2.3: ViewModel RED 테스트 작성**
- 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityViewModelTest.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityPaginationTest.kt`
- 테스트 케이스:
- 최초 로딩이 `page=0`, `size=20`으로 호출된다.
- 기본 보기 방식은 `List`이다.
- 보기 방식 toggle은 `List -> Grid -> List` 순서이며 API를 재호출하지 않는다.
- `communityPostCount == 0`이면 `Empty` 상태가 된다.
- 표시 가능한 `communityPosts`가 없으면 `Empty` 상태가 된다.
- `hasNext == true`일 때 다음 페이지는 마지막 응답의 `page + 1`로 요청한다.
- load-more 요청에는 `size=20`을 유지한다.
- loading 중 중복 load-more 요청은 무시된다.
- 다음 페이지 성공 시 기존 게시글 뒤에 append한다.
- 다음 페이지 실패 시 기존 목록은 유지하고 pagination error message만 설정한다.
- `consumePaginationErrorMessage()` 호출 후 pagination error message가 null이 된다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityViewModelTest"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityPaginationTest"`
- 기대 결과:
- production 구현 전 `CreatorChannelCommunityViewModel` 미구현으로 RED 실패한다.
- [ ] **Task 2.4: `CreatorChannelCommunityViewModel` 구현**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityViewModel.kt`
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`
- 작업:
- `DEFAULT_PAGE_SIZE = 20`, `FIRST_PAGE = 0`, 기본 `viewMode = CreatorChannelCommunityViewMode.List`로 둔다.
- `loadCommunity(creatorId: Long, isOwner: Boolean)`는 같은 `creatorId`와 기존 state가 있으면 중복 최초 조회를 막는다.
- `toggleViewMode()`는 현재 로드된 데이터를 유지하고 API를 재호출하지 않는다.
- `retryCommunity()`는 첫 페이지를 다시 조회한다.
- `loadMore()`는 content 상태, `hasNext`, `isLoadingMore`, `creatorId`를 확인해 중복 요청을 막는다.
- `requestGeneration`으로 오래된 응답이 최신 상태를 덮어쓰지 않게 한다.
- 첫 페이지 성공 후 `communityPostCount == 0` 또는 표시 가능한 item이 0개이면 `Empty` 상태로 전환한다.
- pagination 실패는 기존 content를 유지하고 `paginationErrorMessage`에만 반영한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`
- 기대 결과:
- ViewModel 테스트가 GREEN이다.
---
### Phase 3: Mapper/UI model 정책 추가
- [ ] **Task 3.1: Mapper RED 테스트 작성**
- 생성:
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityMapperTest.kt`
- 테스트 케이스:
- `createdAtUtc`는 기존 상대 시간 포맷 helper를 사용해 UI text로 변환된다.
- `creatorProfileUrl`은 profile image URL로 매핑된다.
- `isPinned == true`이면 notice/pin 표시 상태가 true다.
- `isCommentAvailable == false`이면 댓글 icon/count 표시 상태가 false다.
- `price > 0 && !existOrdered && !isOwner`이면 유료 미구매 잠금 상태다.
- 유료 미구매 상태에서는 image placeholder mode가 `LockedGray`이고 play button 표시 상태가 false다.
- `isOwner == true` 또는 `existOrdered == true`이면 `audioUrl != null && imageUrl != null`에서 play button 표시 상태가 true다.
- 본인 채널에 본인이 쓴 게시글에서만 owner more button 표시 상태가 true다.
- 본인 채널에 본인이 쓴 유료 게시글에서만 top price 표시 상태가 true다.
- 타인 채널에서는 top more/price 표시 상태가 false다.
- grid text-only preview는 줄바꿈을 공백으로 바꾸고 24자까지만 사용한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityMapperTest"`
- 기대 결과:
- mapper 미구현 상태에서 RED 실패한다.
- [ ] **Task 3.2: UI model과 mapper 구현**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityUiModels.kt`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/model/CreatorChannelCommunityMappers.kt`
- 작업:
- `CreatorChannelCommunityViewMode``List`, `Grid`로 정의하고 label/icon resource를 가진다.
- `CreatorChannelCommunityPostUiModel`에는 `postId`, `creatorId`, `creatorNickname`, `creatorProfileUrl`, `createdAtText`, `content`, `imageUrl`, `audioUrl`, `price`, `existOrdered`, `likeCount`, `commentCount`, `showComment`, `showNotice`, `isLocked`, `showOwnerMore`, `showOwnerTopPrice`, `showPlayButton`, `gridPreviewText`를 둔다.
- `showComment = isCommentAvailable`로 매핑한다.
- `isLocked = price > 0 && !existOrdered && !isOwner`로 매핑한다.
- `showPlayButton = !isLocked && !audioUrl.isNullOrBlank() && !imageUrl.isNullOrBlank()`로 매핑한다.
- `showOwnerMore``showOwnerTopPrice``isOwner == true && creatorId == currentUserId` 조건으로 제한한다.
- `gridPreviewText`는 기존 `CreatorCommunityAllGridAdapter`처럼 줄바꿈 제거 후 trim하고 24자까지 사용한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityMapperTest"`
- 기대 결과:
- Mapper 테스트가 GREEN이다.
---
### Phase 4: Fragment, Adapter, XML UI 구현
- [ ] **Task 4.1: Fragment layout 추가**
- 생성:
- `app/src/main/res/layout/fragment_creator_channel_community.xml`
- 작업:
- Sort-bar에는 좌측 `전체` + `communityPostCount`, 우측 보기 방식 label + icon을 배치한다.
- RecyclerView는 리스트형과 썸네일형을 같은 `RecyclerView`에서 `LayoutManager`/adapter 교체로 처리한다.
- empty, error message, retry button 영역은 오디오/시리즈 탭 패턴을 따른다.
- 검증 명령:
- `./gradlew :app:mergeDebugResources`
- 기대 결과:
- 신규 layout binding 생성이 PASS한다.
- [ ] **Task 4.2: 리스트형 item layout/adapter 추가**
- 생성:
- `app/src/main/res/layout/item_creator_channel_community_list.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityListAdapter.kt`
- 작업:
- Figma `290:9061`, `290:9066`, `665:19021` 기준의 feed card를 구현한다.
- `creatorProfileUrl`, nickname, 상대 시간, notice, 본문, 이미지/잠금 영역, reaction 영역을 표시한다.
- `showComment == false`이면 댓글 icon/count view를 `GONE` 처리한다.
- `isLocked == true`이면 이미지 대신 회색 RoundedRectangle과 lock/가격 캡슐을 표시하고 play button을 숨긴다.
- `showPlayButton == true`이면 이미지 가운데 play/pause button을 표시한다.
- `showOwnerMore == true`이면 우측 상단 더보기 버튼을 표시하고 기존 `CreatorCommunityPostMenuBottomSheetDialog` 호출 callback을 연결한다.
- root item click listener는 설정하지 않거나 no-op으로 둔다.
- 검증 명령:
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"`
- 기대 결과:
- layout resource와 adapter bind source 검증이 PASS한다.
- [ ] **Task 4.3: 썸네일형 grid item layout/adapter 추가**
- 생성:
- `app/src/main/res/layout/item_creator_channel_community_grid.xml`
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/ui/CreatorChannelCommunityGridAdapter.kt`
- 작업:
- 3열 정사각형 item을 유지하도록 layout params를 adapter에서 계산하거나 `GridLayoutManager` span 폭에 맞춘다.
- `imageUrl != null && !isLocked`이면 이미지 전체 표시.
- `imageUrl == null && !isLocked`이면 `gridPreviewText`를 중앙 정렬로 표시.
- `isLocked == true`이면 잠금/가격 표시를 노출한다.
- `showNotice == true`이면 pin/notice icon을 상단에 표시한다.
- root item click listener는 설정하지 않거나 no-op으로 둔다.
- 검증 명령:
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.CreatorChannelCommunityFragmentLayoutTest"`
- 기대 결과:
- grid layout/resource 검증이 PASS한다.
- [ ] **Task 4.4: `CreatorChannelCommunityFragment` 구현**
- 생성:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/community/CreatorChannelCommunityFragment.kt`
- 작업:
- `CreatorChannelCommunityViewModel`을 observe하고 Loading/Empty/Error/Content를 bind한다.
- Sort-bar 우측 toggle click에서 `viewModel.toggleViewMode()`를 호출한다.
- view mode가 List이면 `LinearLayoutManager`, Grid이면 `GridLayoutManager(spanCount = 3)`를 적용한다.
- pagination error message는 Toast로 표시하고 consume한다.
- `onCreatorChannelCommunityTabSelected()`, `onCreatorChannelCommunityScrolledToBottom()`, `onCreatorChannelCommunityOwnerCtaVisibilityChanged()` entry를 제공한다.
- media player update callback에서 adapter의 play/pause 상태를 갱신한다.
- Fragment `onDestroyView()` 또는 `onDestroy()`에서 `CreatorCommunityMediaPlayerManager.stopContent()`를 호출해 재생 리소스를 정리한다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`
- 기대 결과:
- Fragment와 adapter 테스트가 GREEN이다.
- [ ] **Task 4.5: 문자열/리소스 정리**
- 수정:
- `app/src/main/res/values/strings.xml`
- `app/src/main/res/values-en/strings.xml`
- `app/src/main/res/values-ja/strings.xml`
- 작업:
- `리스트형`, `썸네일형`, empty/error/retry/notice 문구를 추가한다.
- 기존 `creator_channel_owner_fab_community`, `screen_creator_community_purchase_with_can`는 재사용 가능하면 중복 추가하지 않는다.
- `ic_new_list`, `ic_new_grid`, `ic_new_upload_community_post`가 없으면 기존 drawable 정책에 맞게 추가 여부를 별도 확인 후 진행한다.
- 검증 명령:
- `./gradlew :app:mergeDebugResources`
- 기대 결과:
- 한국어/영어/일본어 string 참조가 모두 해소된다.
---
### Phase 5: Activity/Pager/Owner CTA/Pagination 연결
- [ ] **Task 5.1: PagerAdapter에서 Community Fragment 연결**
- 수정:
- `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.Community -> CreatorChannelCommunityFragment.newInstance(creatorId)` 분기를 추가한다.
- 기존 placeholder 기대 테스트를 Community 실제 Fragment 기대값으로 갱신한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelPagerAdapterTest"`
- 기대 결과:
- Community 탭이 신규 Fragment로 연결됨이 검증된다.
- [ ] **Task 5.2: `CreatorChannelActivity` Host 연결**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt`
- 작업:
- `CreatorChannelCommunityFragment.Host`를 구현한다.
- `findCommunityFragment()`를 추가한다.
- `onPageSelected`와 header 변경 시 현재 탭이 Community이면 `onCreatorChannelCommunityTabSelected()`를 호출한다.
- `notifyCurrentCreatorChannelTabScrolledToBottom()`에 Community 분기를 추가한다.
- `isCreatorChannelLoadMoreTab()`에 Community를 포함한다.
- `onCreatorChannelCommunityContentChanged()`에서 ViewPager 높이 갱신과 추가 load-more 필요 여부 확인을 호출한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- 기대 결과:
- Community 탭 선택/스크롤/높이 갱신 source 검증이 PASS한다.
- [ ] **Task 5.3: Owner CTA Community 연결**
- 수정:
- `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt`
- 작업:
- `currentOwnerCtaTab()`에 Community 탭을 포함한다.
- `updateOwnerCtaVisibility()`에서 Community일 때 `ic_new_upload_community_post`, `creator_channel_owner_fab_community`를 bind한다.
- `onOwnerCtaClicked()`에서 Community일 때 기존 `onOwnerFabCommunityClicked()`를 호출한다.
- `findCommunityFragment()?.onCreatorChannelCommunityOwnerCtaVisibilityChanged(ownerCtaTab == CreatorChannelTab.Community)`를 호출해 하단 padding/inset을 반영하게 한다.
- 검증 명령:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"`
- `./gradlew :app:mergeDebugResources`
- 기대 결과:
- 본인 채널 Community 탭 하단 CTA 연결이 source/resource 검증으로 확인된다.
- [ ] **Task 5.4: media player 생명주기와 adapter 갱신 연결**
- 수정:
- `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/ui/CreatorChannelCommunityListAdapter.kt`
- 작업:
- `CreatorCommunityMediaPlayerManager(requireContext()) { listAdapter.notifyDataSetChanged() }` 형태로 생성한다.
- play/pause button click에서 `CreatorCommunityContentItem(postId, audioUrl)`를 전달해 `toggleContent()`를 호출한다.
- bind 시 `mediaPlayerManager.isPlayingContent(postId)` 값으로 play/pause icon을 결정한다.
- Fragment 정리 시 `stopContent()`를 호출한다.
- 검증 명령:
- `./gradlew :app:compileDebugKotlin`
- 기대 결과:
- 기존 media player manager import와 호출이 컴파일된다.
---
### Phase 6: 통합 검증과 수동 확인
- [ ] **Task 6.1: 단위 테스트 실행**
- 실행:
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Community*"`
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`
- 기대 결과:
- 커뮤니티 탭 mapper/ViewModel/pagination/layout/source 테스트가 모두 PASS한다.
- 검증 기록:
- 구현 후 기록한다.
- [ ] **Task 6.2: 리소스/컴파일/린트 검증**
- 실행:
- `./gradlew :app:mergeDebugResources`
- `./gradlew :app:compileDebugKotlin`
- `./gradlew :app:ktlintCheck`
- `git diff --check`
- 기대 결과:
- resource merge, Kotlin compile, ktlint, whitespace 검증이 모두 PASS한다.
- 검증 기록:
- 구현 후 기록한다.
- [ ] **Task 6.3: 수동 화면 확인**
- 확인 항목:
- 타인 채널 리스트형 기본값에서 Sort-bar 우측이 `리스트형`/`ic_new_list`로 표시된다.
- 토글 시 썸네일형 3열 grid로 바뀌고 API 재호출 없이 현재 데이터를 표시한다.
- 다시 토글 시 리스트형으로 복귀한다.
- 유료 미구매 게시글은 회색 RoundedRectangle, lock, 가격 캡슐을 표시하고 재생 버튼을 숨긴다.
- 본인 또는 구매한 사용자의 오디오 게시글은 이미지 중앙에 재생/일시정지 버튼을 표시하고 기존 media player manager로 재생된다.
- 댓글 불가 게시글은 댓글 icon과 댓글 수가 보이지 않는다.
- 본인 채널 리스트형에서 본인이 쓴 게시글만 우측 상단 더보기와 유료 가격이 보인다.
- 게시글 item 터치 시 아무 동작도 하지 않는다.
- 본인 채널 Community 탭 하단 `커뮤니티 글 올리기` CTA가 고정 표시되고 기존 작성 화면으로 진입한다.
- CTA가 목록 마지막 item 또는 empty 문구를 가리지 않는다.
- `hasNext == true`일 때 스크롤 하단에서 다음 page가 append된다.
- empty 상태에서 Sort-bar와 목록/grid가 숨겨지고 empty 문구만 표시된다.
- 검증 기록:
- 구현 후 기록한다.
---
## Verification Log
- 계획 문서 생성 단계에서는 코드 변경을 수행하지 않았다. 구현 후 통합 검증, 회귀 검증, 최종 수동 확인 기록을 이 섹션에 누적한다.

View File

@@ -0,0 +1,324 @@
# PRD: 크리에이터 채널 커뮤니티 탭
## 1. Overview
크리에이터 채널의 `커뮤니티` 탭에서 커뮤니티 게시글 수, 리스트/썸네일 보기 전환, 게시글 목록, 본인 채널 글쓰기 CTA와 스크롤 pagination을 제공한다.
---
## 2. Problem
- 크리에이터 채널 컨테이너와 `홈`, `라이브`, `오디오`, `시리즈` 탭은 별도 문서에서 정의되었지만, `커뮤니티` 탭의 API 계약과 Figma 기반 UI 요구사항은 별도 정의가 필요하다.
- 사용자는 크리에이터 채널에서 커뮤니티 게시글을 일반 피드 형태 또는 썸네일 그리드 형태로 전환해 탐색할 수 있어야 한다.
- 커뮤니티 탭의 Sort-bar 우측 액션은 오디오/시리즈 탭의 정렬 선택이 아니라 리스트/썸네일 보기 토글이어야 한다.
- 커뮤니티 게시글은 공지 고정 여부, 유료 글 가격, 이미지/오디오 포함 여부, 댓글 가능 여부에 따라 표시 상태가 달라질 수 있다.
- 크리에이터 본인의 채널에서는 하단 고정 `커뮤니티 글 올리기` CTA를 제공해야 한다.
- 목록이 길어질 수 있으므로 `CreatorChannelCommunityTabResponse.hasNext == true`일 때 다음 페이지를 자동 로딩해야 한다.
---
## 3. Goals
- Figma 전체 리스트형 `290:9061` 기준으로 크리에이터 채널 `커뮤니티` 탭의 리스트형 UI 요구사항을 정의한다.
- Figma 전체 썸네일형 `290:9073` 기준으로 썸네일형 UI 요구사항을 정의한다.
- Figma 전체 리스트형 + 본인 채널 `665:19021` 기준으로 본인 채널 UI 요구사항을 정의한다.
- Figma 유료이고 구매하지 않은 게시글 `290:9066` 기준으로 유료 미구매 게시글 표시 요구사항을 정의한다.
- 본인 채널 + 썸네일형은 게시글 표시 UI만 썸네일형으로 변경하고 하단 고정 `커뮤니티 글 올리기` CTA는 동일하게 유지한다.
- 본인 채널에 본인이 쓴 커뮤니티 게시글의 리스트형 item은 우측 상단에 더보기 버튼을 표시하고, 유료 게시글이면 가격을 함께 표시한다.
- API 응답의 `creatorProfileUrl`, `existOrdered`를 사용해 작성자 프로필 이미지와 유료 게시글 구매 여부를 표시한다.
- 이번 탭에서 게시글 item 터치 시 별도 동작을 수행하지 않는다.
- 오디오 게시글 재생은 기존 `CreatorCommunityMediaPlayerManager`를 재사용한다.
- API endpoint `GET /api/v2/creator-channels/{creatorId}/community`를 기준으로 최초 조회와 pagination 요구사항을 정의한다.
- 최초 조회 query parameter 기본값은 `page=0`, `size=20`으로 둔다.
- Sort-bar 좌측에는 전체 커뮤니티 게시글 수를 표시한다.
- Sort-bar 우측에는 현재 보기 방식 label과 icon을 표시하고, 터치할 때마다 `리스트형``썸네일형`을 토글한다.
- 보기 방식 기본값은 `리스트형`이며 icon은 `ic_new_list`를 사용한다.
- 썸네일형으로 토글되면 label은 `썸네일형`, icon은 `ic_new_grid`를 사용한다.
- `리스트형`, `썸네일형`, `커뮤니티 글 올리기` 등 사용자 표시 문구는 다국어 문자열 리소스로 관리한다.
- 응답의 `hasNext``true`이면 현재 `page + 1` 페이지를 추가 로딩한다.
---
## 4. Non-Goals
- 크리에이터 채널 상단 header, title bar, 공통 main tab-bar 구조 자체를 재설계하지 않는다.
- `홈`, `라이브`, `오디오`, `시리즈`, `화보`, `팬Talk`, `후원` 탭의 상세 구현은 이번 범위에서 제외한다.
- 커뮤니티 게시글 상세 화면은 다음 범위에서 추가하며, 이번 범위에서 신규 상세 화면을 구현하지 않는다.
- 댓글 화면, 좋아요 API, 댓글 작성 API, 결제/구매 플로우 내부 동작 변경은 이번 범위에서 제외한다.
- 커뮤니티 글 작성 화면 내부 구현은 이번 범위에서 제외하고 CTA 진입점만 정의한다.
- API schema를 임의 변경하거나 서버 응답 필드명을 클라이언트에서 새로 정의하지 않는다.
- 서버 정렬, 필터, 검색, pull-to-refresh, skeleton/shimmer는 이번 범위에서 제외한다.
- Figma asset을 localhost URL 그대로 앱 코드에 직접 의존하지 않는다.
---
## 5. Target Users
- 크리에이터 채널에서 커뮤니티 게시글을 탐색하는 앱 사용자.
- 게시글의 이미지, 오디오, 유료 여부, 좋아요/댓글 수를 목록에서 확인하려는 앱 사용자.
- 피드형 목록과 썸네일 그리드를 상황에 맞게 전환해 보고 싶은 앱 사용자.
- 본인 채널에서 커뮤니티 글 작성 화면으로 진입하려는 크리에이터.
- `kr.co.vividnext.sodalive.v2` 하위 크리에이터 채널 탭을 구현/유지보수하는 Android 개발자.
---
## 6. User Stories
- 사용자는 크리에이터 채널의 `커뮤니티` 탭에서 전체 커뮤니티 게시글 수를 확인하고 싶다.
- 사용자는 커뮤니티 게시글을 기본 리스트형으로 보고 싶다.
- 사용자는 Sort-bar 우측 토글을 눌러 커뮤니티 게시글을 썸네일형으로 전환하고 싶다.
- 사용자는 다시 토글을 눌러 썸네일형에서 리스트형으로 돌아오고 싶다.
- 사용자는 공지로 고정된 게시글을 목록에서 구분하고 싶다.
- 사용자는 유료 커뮤니티 글의 가격을 목록 또는 썸네일에서 확인하고 싶다.
- 사용자는 게시글 본문, 이미지, 좋아요 수, 댓글 수, 댓글 가능 여부를 목록에서 확인하고 싶다.
- 사용자는 목록 하단까지 스크롤하면 다음 페이지가 자연스럽게 이어서 로딩되길 기대한다.
- 사용자는 크리에이터가 아직 커뮤니티 글을 작성하지 않은 경우 불필요한 목록 UI 없이 empty 문구만 보고 싶다.
- 크리에이터 본인은 본인 채널의 `커뮤니티` 탭에서 하단 고정 버튼으로 커뮤니티 글 작성 화면에 진입하고 싶다.
---
## 7. Core Features
### Creator Channel Community Tab API
`커뮤니티` 탭 진입과 추가 로딩 시 크리에이터별 커뮤니티 탭 데이터를 조회한다.
#### Requirements
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/community`이다.
- `creatorId`는 path variable로 전달한다.
- Query parameters는 `page`, `size`를 사용한다.
- 최초 조회 기본값은 `page=0`, `size=20`이다.
- 정렬 또는 보기 방식 query parameter는 사용하지 않는다.
- 보기 방식이 변경되어도 API를 재호출하지 않고 현재 로드된 데이터를 다른 UI로 표시한다.
- `hasNext == true`일 때 다음 페이지 요청은 현재 응답의 `page + 1` 값을 사용한다.
- 중복 pagination 요청이 발생하지 않도록 loading 중 추가 요청을 막아야 한다.
#### Response Contract
```kotlin
data class CreatorChannelCommunityTabResponse(
val communityPostCount: Int,
val communityPosts: List<CreatorChannelCommunityPostResponse>,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
)
data class CreatorChannelCommunityPostResponse(
val postId: Long,
val creatorId: Long,
val creatorNickname: String,
val creatorProfileUrl: String,
val createdAtUtc: String,
val content: String,
val imageUrl: String?,
val audioUrl: String?,
val price: Int,
val existOrdered: Boolean,
@JsonProperty("isCommentAvailable")
val isCommentAvailable: Boolean,
val likeCount: Int,
val commentCount: Int,
@JsonProperty("isPinned")
val isPinned: Boolean
)
```
#### Edge Cases
- 최초 조회 실패 시 기존 크리에이터 채널 탭의 error/retry 패턴을 따른다.
- 다음 페이지 로딩 실패 시 기존 목록은 유지하고 기존 pagination 실패 표시 정책을 따른다.
- 다음 페이지 응답의 `communityPosts`가 비어 있어도 `hasNext` 값 기준으로 이후 로딩 가능 여부를 갱신한다.
- 서버 응답의 `page`, `size`가 요청 상태와 다를 경우 구현 계획 단계에서 기존 ViewModel 상태 동기화 패턴을 확인해 따른다.
- `communityPostCount > 0`이지만 첫 페이지 `communityPosts`가 비어 있는 경우 기존 목록 탭의 empty/error 정책을 구현 계획 단계에서 확인해 따른다.
- `creatorProfileUrl`이 비어 있거나 이미지 로딩에 실패하면 기존 프로필 placeholder 정책을 따른다.
### Sort Bar and View Mode Toggle
Sort-bar는 전체 커뮤니티 게시글 수와 현재 보기 방식을 표시하고, 우측 액션 터치 시 리스트형/썸네일형을 전환한다.
#### Requirements
- Figma 전체 리스트형 기준 Sort-bar는 `290:9061``sort-bar`이다.
- Figma 전체 썸네일형 기준 Sort-bar는 `290:9073``sort-bar`이다.
- Figma 본인 채널 리스트형 기준 Sort-bar는 `665:19021``sort-bar`이다.
- 좌측에는 `전체``communityPostCount`를 표시한다.
- 우측에는 현재 보기 방식 label과 icon을 표시한다.
- 기본 보기 방식은 리스트형이다.
- 리스트형 label은 `리스트형`이며 icon은 `ic_new_list`를 사용한다.
- 썸네일형 label은 `썸네일형`이며 icon은 `ic_new_grid`를 사용한다.
- 우측 보기 방식 영역을 터치하면 `리스트형 -> 썸네일형 -> 리스트형` 순서로 토글한다.
- 보기 방식 label은 한국어/영어/일본어 다국어 문자열 리소스로 관리한다.
- 보기 방식 변경 시 스크롤 위치 유지 여부는 구현 계획 단계에서 기존 탭 목록 상태 정책을 확인해 결정한다.
#### Edge Cases
- 다국어 label 길이가 길어져도 Sort-bar 좌측 카운트와 우측 토글이 겹치지 않아야 한다.
- 빠르게 반복 터치해도 현재 보기 방식 상태와 Adapter item type이 불일치하지 않아야 한다.
- 화면 회전 또는 Fragment/View 재생성 후 보기 방식 상태는 ViewModel 또는 saved state 정책에 따라 유지되어야 한다.
### Community Post List Mode
리스트형은 커뮤니티 게시글을 피드 카드 형태로 세로 표시한다.
#### Requirements
- Figma 전체 리스트형 기준 콘텐츠 영역은 `290:9061`이다.
- Figma 본인 채널 리스트형 기준 콘텐츠 영역은 `665:19021`이다.
- `communityPosts`를 세로 목록으로 표시한다.
- 각 item은 Figma `feed` 구조처럼 `gray/900 #202020` 배경, 14dp radius, 14dp padding을 기준으로 한다.
- 게시글 작성자 영역에는 `creatorProfileUrl` 기반 프로필 이미지, `creatorNickname`, `createdAtUtc`의 상대 시간 표시를 제공한다.
- `isPinned == true`인 게시글은 item 상단에 공지 표시를 노출한다.
- 공지 label은 Figma 기준 `Notice`로 보이지만 사용자 표시 문구는 다국어 문자열 리소스로 관리한다.
- 게시글 본문에는 `content`를 표시한다.
- 무료 게시글 또는 `existOrdered == true`인 구매 완료 게시글은 `imageUrl != null`이면 본문 아래에 이미지를 표시한다.
- 본인 채널에 본인이 쓴 게시글은 구매 여부와 관계없이 작성자 권한으로 본문 아래에 이미지를 표시한다.
- 본인 또는 구매한 사용자에게 표시되는 게시글에서 `audioUrl != null`이고 `imageUrl != null`이면 이미지 가운데에 기존 커뮤니티 페이지와 동일한 재생/일시정지 버튼을 표시한다.
- 재생/일시정지 버튼 터치 시 기존 `CreatorCommunityMediaPlayerManager`로 오디오 재생 상태를 제어한다.
- `audioUrl == null`이면 재생 버튼을 표시하지 않는다.
- `price > 0 && existOrdered == false`이고 본인 채널 작성자 권한도 아닌 유료 미구매 게시글은 Figma `290:9066`처럼 본문 일부와 잠금 이미지 영역을 표시한다.
- 유료 미구매 게시글은 이번 API에서 `imageUrl == null`로 내려오는 계약이므로, 이미지 영역에는 실제 이미지 대신 회색 RoundedRectangle을 표시한다.
- 유료 미구매 게시글의 회색 RoundedRectangle 중앙에는 lock icon과 가격 캡슐을 표시한다.
- 유료 미구매 게시글에서는 오디오 재생 버튼을 표시하지 않는다.
- `price > 0`이면 유료 게시글로 취급한다.
- `price == 0`이면 무료 게시글로 취급한다.
- 좋아요 영역에는 `likeCount`를 표시한다.
- `isCommentAvailable == true`이면 댓글 icon과 `commentCount`를 표시한다.
- `isCommentAvailable == false`이면 댓글 icon과 댓글 수를 모두 숨기고 댓글 진입도 제공하지 않는다.
- 본인 채널에 본인이 쓴 게시글의 리스트형 item에서만 우측 상단에 더보기 버튼을 표시한다.
- 본인 채널에 본인이 쓴 게시글의 리스트형 item에서만 `price > 0`인 유료 게시글 가격을 우측 상단 영역에 표시한다.
- 본인 채널에 본인이 쓴 유료 게시글에서 가격 표시와 더보기 버튼이 같은 우측 상단 영역에 함께 표시되는 경우 서로 겹치지 않게 배치한다.
- 본인 채널이 아니거나 본인이 쓴 게시글이 아닌 경우 리스트형 item 우측 상단 더보기 버튼과 상단 가격 표시는 표시하지 않는다.
- 우측 더보기 액션은 기존 `CreatorCommunityPostMenuBottomSheetDialog`와 동일한 수정/삭제/고정/고정 해제 액션 정책을 따른다.
- 리스트형 item 터치 시 아무 동작도 수행하지 않는다. 상세 화면은 다음 범위에서 추가한다.
#### Edge Cases
- `content`가 긴 경우 Figma처럼 본문 영역 안에서 줄바꿈하고 목록 item 간 겹침이 없어야 한다.
- `content`가 blank string이면 본문 텍스트 영역을 숨길지 빈 영역을 유지할지 구현 계획 단계에서 기존 커뮤니티 item 정책을 확인해 결정한다.
- `imageUrl` 이미지 로딩 실패 시 기존 이미지 placeholder 정책을 따른다.
- 유료 미구매 게시글에서 서버가 예외적으로 `imageUrl`을 내려줘도 이번 요구사항은 회색 RoundedRectangle + 잠금/가격 표시를 우선한다.
- `likeCount`, `commentCount`가 큰 숫자여도 reaction 영역의 icon과 text가 겹치지 않아야 한다.
- `isCommentAvailable == false`이고 `likeCount`만 표시되는 경우 reaction 영역의 간격이 어색하게 남지 않아야 한다.
### Community Post Thumbnail Mode
썸네일형은 커뮤니티 게시글을 정사각형 그리드 item으로 표시한다.
#### Requirements
- Figma 전체 썸네일형 기준 콘텐츠 영역은 `290:9073`이다.
- 본인 채널 + 썸네일형은 Figma 전체 썸네일형의 게시글 그리드 표시 방식을 따르되, 하단 고정 CTA는 본인 채널 정책을 유지한다.
- `communityPosts`를 3열 그리드로 표시한다.
- 각 grid item은 정사각형 비율을 유지한다.
- `imageUrl != null`이면 이미지를 grid item 전체에 표시한다.
- `imageUrl == null`이고 유료 미구매 게시글이 아니면 기존 커뮤니티 그리드 페이지처럼 `gray/900 #202020` 배경 위에 `content` 일부를 중앙 정렬로 표시한다.
- text-only grid item의 본문 preview 길이와 줄 수는 기존 `CreatorCommunityAllGridAdapter` 정책을 우선 참고한다.
- `isPinned == true`이면 Figma처럼 pin/notice icon을 grid item 상단에 표시한다.
- `price > 0 && existOrdered == false`이고 본인 채널 작성자 권한도 아닌 유료 미구매 게시글은 grid item에 잠금/가격 표시를 노출한다.
- 본인 또는 구매한 사용자는 grid item에 실제 이미지 또는 text preview를 표시하고 잠금 overlay를 표시하지 않는다.
- grid item 터치 시 아무 동작도 수행하지 않는다. 상세 화면은 다음 범위에서 추가한다.
#### Edge Cases
- `content`가 긴 text-only 게시글은 grid item 내부에서 정해진 줄 수까지만 표시하고 이후 말줄임 처리한다.
- `imageUrl` 이미지 로딩 실패 시 text-only fallback이 아니라 기존 이미지 placeholder 정책을 따른다.
- 유료 글 잠금 overlay와 text가 겹치지 않아야 한다.
- 3열 그리드에서 기기 폭이 달라져도 item은 정사각형 비율을 유지해야 한다.
- 본인 채널에서 CTA가 표시되는 경우 마지막 grid row가 CTA에 가려지지 않도록 하단 padding 또는 inset을 추가한다.
### V2 Widget Reuse
커뮤니티 탭 구현 시 v2 패키지 하위 기존 위젯을 우선 검토하고, 부족한 상태만 최소 확장한다.
#### Requirements
- 리스트형 기본 피드 카드는 `kr.co.vividnext.sodalive.v2.widget.feed.FeedCommunityView``view_feed_community.xml` 재사용을 우선 검토한다.
- `FeedItem.Community`에는 이미 `creatorImageUrl`, `imageUrl`, `audioUrl`, `price`, `existOrdered`가 있으므로 새 응답 모델을 매핑할 수 있다.
- `FeedCommunityView`는 현재 `price > 0 && !existOrdered` 조건으로 잠금 overlay와 가격을 표시하므로 유료 미구매 기본 표현에 재사용 가능하다.
- 기존 `FeedCommunityView`만으로 부족한 댓글 불가 숨김, 본인 채널 우측 상단 더보기/가격, 이미지 중앙 재생 버튼은 구현 계획 단계에서 위젯 확장 또는 커뮤니티 탭 전용 item wrapper 중 더 작은 변경을 선택한다.
- 이미지 중앙 재생 버튼의 실제 재생 제어는 기존 `CreatorCommunityMediaPlayerManager`를 재사용한다.
- 썸네일형은 기존 `CreatorCommunityAllGridAdapter`의 image/text/locked 분기 정책을 참고하되, v2 하위에 별도 grid item view가 없으면 `kr.co.vividnext.sodalive.v2.creator.channel` 하위 전용 view holder로 최소 구현한다.
- 이미지 blur 처리는 기존 v2 홈 커뮤니티 섹션의 `BlurTransformation` 사용 패턴을 참고하되, 이번 리스트형 유료 미구매 상태는 서버가 `imageUrl == null`로 내려주는 계약이므로 회색 RoundedRectangle 표시를 우선한다.
#### Edge Cases
- 공통 `FeedCommunityView`를 수정할 경우 v2 홈 추천 커뮤니티와 크리에이터 채널 홈 커뮤니티 섹션의 표시가 의도치 않게 바뀌지 않아야 한다.
- 공통 위젯 변경이 기존 홈 화면에 영향을 준다면 커뮤니티 탭 전용 wrapper 또는 optional bind flag로 범위를 제한한다.
- 레거시 `explorer.profile.creator_community` 코드를 v2 패키지로 무리하게 이동하지 않고, 필요한 정책만 참고한다.
### Pagination
커뮤니티 게시글 목록은 스크롤 하단 접근 시 다음 페이지를 로딩한다.
#### Requirements
- `CreatorChannelCommunityTabResponse.hasNext == true`일 때만 다음 페이지를 요청한다.
- 다음 페이지는 마지막 성공 응답의 `page + 1`로 요청한다.
- 다음 페이지 요청에는 `size=20`을 유지한다.
- 다음 페이지 로딩 중에는 추가 page 요청을 중복으로 보내지 않는다.
- 다음 페이지 성공 시 기존 `communityPosts` 뒤에 append한다.
- 보기 방식 변경 시 pagination 상태와 로드된 데이터는 초기화하지 않는다.
#### Edge Cases
- 빠른 스크롤로 load-more trigger가 반복 발생해도 page가 중복 append되지 않아야 한다.
- 리스트형과 썸네일형을 오가도 이미 로드된 page 데이터가 중복 추가되지 않아야 한다.
- 마지막 페이지 응답 이후 `hasNext == false`이면 이후 load-more trigger를 무시한다.
### Empty State
커뮤니티 게시글이 없으면 게시글이 있을 때 표시하는 UI를 숨기고 empty 문구만 표시한다.
#### Requirements
- `communityPostCount == 0` 또는 표시 가능한 `communityPosts`가 없는 전체 empty 상태이면 empty 상태를 표시한다.
- empty 상태에서는 Sort-bar의 보기 방식 토글과 게시글 목록/그리드를 표시하지 않는다.
- empty 문구는 `크리에이터가 커뮤니티 글을 준비 중입니다.\n기대해 주세요!`이다.
- empty 문구는 한국어/영어/일본어 다국어 문자열 리소스로 관리한다.
- empty 상태 표시 방식은 라이브/오디오/시리즈 탭 empty 상태와 동일하게 적용한다.
- 내 채널 empty 상태에서도 empty 문구는 동일하게 표시한다.
- 내 채널 empty 상태에서 하단 CTA는 내 채널 CTA 정책을 따른다.
#### Edge Cases
- API 최초 조회 실패 상태는 empty 상태로 취급하지 않고 기존 error/retry 패턴을 따른다.
- 첫 페이지에서 표시 가능한 게시글이 모두 필터링되거나 숨김 처리되는 경우 empty 상태 정책을 적용한다.
### Owner Community Write CTA
로그인 사용자가 해당 크리에이터 본인인 경우 커뮤니티 탭 하단에 고정된 글쓰기 버튼을 표시한다.
#### Requirements
- Figma 본인 채널 리스트형 기준 CTA는 `665:19021``CTA`이다.
- 본인 채널 + 썸네일형에서도 동일한 하단 고정 CTA를 표시한다.
- 로그인 사용자가 현재 크리에이터 채널의 본인이면 하단 고정 CTA 영역을 표시한다.
- 로그인 사용자가 현재 크리에이터 채널의 본인이 아니면 하단 고정 CTA 영역을 표시하지 않는다.
- CTA 영역은 화면 하단에 고정하고, 목록 스크롤과 함께 움직이지 않는다.
- CTA 버튼 label은 `커뮤니티 글 올리기`이며 다국어 문자열 리소스로 관리한다.
- CTA 버튼 icon은 Figma 기준 글쓰기 icon이며, 구현 시 기존 drawable 또는 신규 drawable 명칭을 확인해 사용한다.
- CTA가 표시되는 경우 목록 마지막 item 또는 empty 문구가 CTA에 가려지지 않도록 하단 padding 또는 inset을 추가한다.
- Android gesture navigation, soft navigation bar, display cutout 환경에서 CTA가 system navigation 영역과 겹치지 않도록 bottom inset을 반영한다.
- 버튼 터치 시 커뮤니티 글 작성 화면으로 진입한다.
#### Edge Cases
- CTA가 표시된 상태에서 리스트형/썸네일형을 토글해도 CTA는 계속 고정 표시되어야 한다.
- 키보드가 열리는 작성 화면 진입 이후 복귀해도 CTA와 목록 inset이 깨지지 않아야 한다.
---
## 8. UX / UI Expectations
- 전체 화면은 기존 크리에이터 채널 컨테이너의 black background, sticky tab-bar, title-bar 동작을 유지한다.
- `커뮤니티` main tab은 선택 상태로 표시하고, 선택 underline과 텍스트 색상은 기존 tab-bar 정책을 따른다.
- Sort-bar 높이와 배치는 Figma `290:9061`, `290:9073`, `665:19021`을 기준으로 하되 기존 탭 구현과 가능한 한 동일한 공통 UI를 사용한다.
- 리스트형 item 간 간격은 Figma 기준 8dp 수준을 따른다.
- 썸네일형은 3열 정사각형 그리드로 표시하고 item 간 별도 gap 없이 배치한다.
- 공지 게시글은 Figma처럼 pin/notice 표시로 일반 게시글과 구분한다.
- 유료 게시글은 가격 또는 잠금 표현으로 무료 게시글과 구분한다.
- 모든 사용자 표시 문구는 문자열 리소스로 관리한다.
- Figma localhost asset URL은 앱 코드에 직접 사용하지 않는다.
---
## 9. Technical Constraints
- Android Gradle 단일 모듈 `:app` 안에서 구현한다.
- 신규 `Fragment`, `ViewModel` 및 그와 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
- 기존 `CreatorChannelActivity``ViewPager2` 기반 탭 구조를 유지한다.
- 기존 크리에이터 채널 API/Repository 패턴을 따른다.
- 서버 DTO 필드명과 타입은 PRD의 Response Contract를 따른다.
- `CreatorChannelCommunityPostResponse`에는 `creatorProfileUrl: String`, `existOrdered: Boolean`을 포함한다.
- API 기본값은 `page=0`, `size=20`이다.
- 보기 방식은 클라이언트 UI 상태이며 API query parameter로 보내지 않는다.
- 리스트형은 v2 `FeedCommunityView` 재사용 가능성을 우선 검토하되, 기존 홈/추천 피드에 영향이 생기는 변경은 커뮤니티 탭 전용 확장으로 제한한다.
- 썸네일형은 레거시 `CreatorCommunityAllGridAdapter`의 표시 정책을 참고하되 신규 코드는 v2 패키지 하위에 작성한다.
- 오디오 게시글 재생은 기존 `kr.co.vividnext.sodalive.explorer.profile.creator_community.all.player.CreatorCommunityMediaPlayerManager`를 재사용한다.
- 네트워크, 이미지 로딩, error/retry, pagination 중복 방지 방식은 기존 라이브/오디오/시리즈 탭 패턴을 우선 따른다.
- 비밀값, `BuildConfig` 값, 로컬 Figma asset URL을 로그/Toast/크래시 메시지에 노출하지 않는다.
---
## 10. Metrics
- 커뮤니티 탭 최초 조회 성공률.
- 커뮤니티 탭 API 실패율과 retry 성공률.
- 리스트형/썸네일형 토글 사용 비율.
- 커뮤니티 게시글 item 클릭률.
- 본인 채널 `커뮤니티 글 올리기` CTA 클릭률.
- pagination 추가 로딩 성공률과 중복 요청 발생 여부.
---
## 11. Open Questions
- 커뮤니티 게시글 작성 CTA는 기존 `CreatorCommunityWriteActivity` 진입을 재사용할 수 있어 보이나, 구현 계획 단계에서 v2 채널 컨테이너와 activity result 처리 범위를 확인한다.