feat(creator): 커뮤니티 탭을 추가한다

This commit is contained in:
Yu Sung
2026-07-05 22:25:07 +09:00
parent 130b8eee40
commit f3b7d01a3f
20 changed files with 1706 additions and 2 deletions

View File

@@ -0,0 +1,192 @@
# PRD: 크리에이터 채널 커뮤니티 탭
## 1. Overview
크리에이터 채널 공통 shell의 `커뮤니티` 탭에서 크리에이터의 커뮤니티 게시글을 리스트형 또는 썸네일형으로 제공한다. 상단 title bar, header, sticky tab-bar는 기존 크리에이터 채널 홈/라이브/오디오/시리즈/팬Talk/후원 구현을 재사용하고, tab-bar 아래 콘텐츠만 커뮤니티 탭 전용 API 응답으로 구성한다.
API는 `GET /api/v2/creator-channels/{creatorId}/community`를 사용한다. `creatorId`는 path variable이며, query parameter는 `page`, `size`를 사용한다. 기본값은 `page=0`, `size=20`이다.
Figma 참조:
- 전체 리스트형: `290:9061`, `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=290-9061&m=dev`
- 전체 썸네일형: `290:9073`, `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=290-9073&m=dev`
- 전체 리스트형 + 본인 채널: `665:19021`, `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=665-19021&m=dev`
- 전체 썸네일형 + 본인 채널: Figma 설명 기준, 썸네일 UI는 `290:9073`과 동일하고 하단 고정 `커뮤니티 글 올리기` 버튼을 표시한다.
- 유료이고 구매하지 않은 게시글: `290:9066`, `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=290-9066&m=dev`
- 유료 금액 우측 상단 표시: `665:19024`, `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=665-19024&m=dev`
- Empty: `290:8994`, `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=290-8994&m=dev`
## 2. Problem
- 현재 `CreatorChannelTab.community`는 임시 화면으로 표시되어 V2 크리에이터 채널 안에서 커뮤니티 게시글 목록을 확인할 수 없다.
- 기존 `CreatorCommunityAllView`는 구 프로필 커뮤니티 전체보기 화면이고, V2 채널 탭의 sticky header, tab-bar, sort-bar 구조와 직접 맞지 않는다.
- 커뮤니티 탭은 오디오 탭의 정렬 선택과 달리 리스트형/썸네일형 보기 전환 버튼이 필요하다.
- 유료 미구매 게시글, 댓글 비활성 게시글, 본인 채널 게시글의 표시 조건이 기존 전체보기와 일부 달라 V2 탭 기준으로 명확히 분리해야 한다.
## 3. Goals
- `CreatorChannelTab.community` 선택 시 커뮤니티 탭 API를 호출하고 응답 데이터로 화면을 구성한다.
- 기본 query는 `page=0`, `size=20`이다.
- sort-bar 좌측에는 `전체 {communityPostCount}`를 표시한다.
- sort-bar 우측은 정렬이 아니라 리스트형/썸네일형 토글 버튼으로 제공한다.
- 기본 보기 방식은 리스트형이고, 기본 아이콘은 `ic_new_list`를 사용한다.
- 토글 후 썸네일형에서는 `ic_new_grid`를 사용한다.
- 리스트형/썸네일형 표시 문구는 `I18n`에 ko/en/ja 다국어로 추가한다.
- 리스트형은 기존 커뮤니티 리스트 카드의 정보 구조를 따르되 V2 채널 탭 디자인에 맞춰 작성한다.
- 썸네일형은 3열 grid로 표시하고, 이미지가 있으면 이미지 또는 GIF를 재생 가능하게 표시한다.
- 썸네일형에서 `imageUrl == nil`이면 유/무료 관계없이 이미지를 표시하지 않고 기존 크리에이터 커뮤니티 페이지의 fallback 규칙을 참고해 글 일부를 표시한다.
- 유료 미구매 게시글은 이미지 영역에 회색 `RoundedRectangle`을 표시하고, 오디오 재생 버튼은 표시하지 않는다.
- 본인 또는 구매한 유저는 이미지와 중앙 오디오 재생 버튼을 기존 커뮤니티 페이지와 동일한 방식으로 표시한다.
- 댓글을 사용할 수 없는 게시글은 댓글 아이콘과 댓글 개수를 숨긴다.
- 본인 채널에 본인이 쓴 커뮤니티 게시글에서만 상단 더보기와 가격 표시를 표시한다.
- 본인 채널이면 하단 고정 `커뮤니티 글 올리기` 버튼을 표시한다.
- 게시글 우측 더보기 액션은 기존 커뮤니티 전체보기의 수정/삭제/신고/고정 액션 흐름을 재사용한다.
## 4. Non-Goals
- 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다.
- 커뮤니티 게시글 상세 화면은 이번 범위에서 구현하지 않는다.
- 커뮤니티 글쓰기/수정 화면 자체를 새로 구현하지 않고 기존 `creatorCommunityWrite` / `creatorCommunityModify` navigation step을 재사용한다.
- 커뮤니티 댓글 리스트, 댓글 작성, 신고, 삭제, 구매 mutation API를 새로 만들지 않고 기존 커뮤니티 구현의 흐름을 재사용한다.
- 오디오 탭의 정렬 popup을 커뮤니티 탭에 표시하지 않는다.
- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다.
- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다.
## 5. Core Requirements
### 5.1 API
- Method: `GET`
- Path: `/api/v2/creator-channels/{creatorId}/community`
- Path parameter:
- `creatorId`
- Query parameters:
- `page`: 기본값 `0`
- `size`: 기본값 `20`
- 응답 래퍼는 기존 패턴대로 `ApiResponse<CreatorChannelCommunityTabResponse>`로 디코딩한다.
```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,
@JsonProperty("isCommentAvailable")
val isCommentAvailable: Boolean,
val likeCount: Int,
val commentCount: Int,
@JsonProperty("isPinned")
val isPinned: Boolean,
val existOrdered: Boolean
)
```
- Kotlin `Long`은 기존 V2 모델 관례대로 Swift `Int`로 선언한다.
- `creatorProfileUrl`, `existOrdered`는 서버 응답에 추가되는 필드로 포함한다.
- `createdAtUtc`는 기존 `DateParser.relativeTimeText(fromUTC:fallback:now:)` 계열 helper를 사용해 상대 시간으로 표시한다.
### 5.2 View mode toggle
- 기본 보기 방식은 리스트형이다.
- sort-bar 우측 버튼을 터치할 때마다 리스트형과 썸네일형을 토글한다.
- 리스트형 상태에서는 버튼에 `ic_new_list`와 리스트형 문구를 표시한다.
- 썸네일형 상태에서는 버튼에 `ic_new_grid`와 썸네일형 문구를 표시한다.
- 리스트형/썸네일형 문구는 `I18n.CreatorChannelCommunity` 또는 같은 범위의 신규 enum에 ko/en/ja로 추가한다.
- 보기 방식 전환은 현재까지 로드된 `communityPosts`를 그대로 사용하고 API를 다시 호출하지 않는다.
### 5.3 List layout
- 리스트형은 Figma `290:9061``665:19021`을 기준으로 표시한다.
- 작성자 프로필 이미지, 닉네임, 상대 시간, 본문, 이미지/오디오 영역, 좋아요 수, 댓글 수를 표시한다.
- `isPinned == true`이면 게시글에 고정 표시를 제공한다.
- `price > 0 && existOrdered == false && isOwnPost == false`이면 유료 미구매 상태로 표시한다.
- 유료 미구매 상태에서는 이미지 URL도 `nil`로 내려올 수 있으므로 이미지 영역에 회색 `RoundedRectangle`을 표시한다.
- 유료 미구매 상태에서는 오디오 재생 버튼을 표시하지 않는다.
- 본인 또는 구매한 유저는 이미지가 있으면 이미지 또는 GIF를 표시하고, `audioUrl != nil`이면 이미지 중앙에 재생 버튼을 표시한다.
- GIF URL은 정지 이미지로 변환하지 않고 재생 가능한 이미지 컴포넌트를 사용한다.
- `imageUrl == nil`이면 유/무료 관계없이 이미지 영역을 표시하지 않는다.
- `isCommentAvailable == false`이면 댓글 아이콘과 댓글 개수를 숨긴다.
### 5.4 Thumbnail layout
- 썸네일형은 Figma `290:9073` 기준 3열 grid로 표시한다.
- 이미지 또는 GIF가 있으면 정방형 썸네일에 표시한다.
- 이미지가 없으면 유/무료 관계없이 기존 `CreatorCommunityAllGridItemView`의 fallback 규칙을 참고해 글 일부를 표시한다.
- 유료 미구매 게시글은 회색 배경과 중앙형 가격 UI를 표시한다.
- `isPinned == true`이면 썸네일 우측 상단에 고정 표시를 제공한다.
- grid item tap은 후속 상세 화면 구현 전까지 상세 이동을 연결하지 않는다.
### 5.5 Paid post display
- 유료 미구매 판정은 `price > 0 && existOrdered == false && isOwnPost == false`이다.
- 리스트형 중앙 가격 표시는 Figma `290:9066`을 따른다.
- 우측 상단 가격 표시는 Figma `665:19024`를 따른다.
- 썸네일형 유료 미구매 가격 UI는 중앙형을 사용한다.
- 상단 더보기와 가격 표시는 본인 채널에 본인이 쓴 커뮤니티 게시글에서만 표시한다.
- 유료 미구매 게시글의 구매 dialog는 기존 `CommunityPostPurchaseDialog`를 재사용한다.
- 구매 성공 후 현재 페이지 범위를 첫 페이지부터 다시 조회하거나 해당 item을 갱신한다.
### 5.6 Actions
- 좋아요는 기존 `CreatorCommunityRepository.communityPostLike(postId:)` 흐름을 재사용한다.
- 댓글 목록은 기존 `CreatorCommunityCommentListView`를 재사용하되, `isCommentAvailable == false`이면 진입 UI를 표시하지 않는다.
- 더보기 메뉴는 기존 `CreatorCommunityMenuView` 또는 V2 anchored popup 패턴 중 구현 시점에 더 작은 변경으로 맞는 방식을 사용한다.
- 본인 채널의 내 게시글에서 더보기 메뉴는 기존과 동일하게 고정/수정/삭제 액션을 제공한다.
- 내 게시글이 아니면 신고 액션을 기존 `CreatorCommunityReportView``ReportType.COMMUNITY_POST` 흐름으로 처리한다.
- 수정 성공, 삭제 성공, 고정 상태 변경 성공 후 커뮤니티 탭 첫 페이지를 다시 조회한다.
### 5.7 Own channel CTA
- 본인 채널이고 `selectedTab == .community`이면 하단 고정 `커뮤니티 글 올리기` 버튼을 표시한다.
- 버튼 문구는 기존 `I18n.CreatorChannelHome.uploadCommunityPost` 또는 동일 의미의 기존 다국어 문구를 우선 재사용한다.
- 버튼 tap은 기존 `showCommunityWrite()` 흐름을 재사용한다.
- 글 작성 성공 후 홈 정보와 커뮤니티 탭 목록을 갱신한다.
### 5.8 Pagination and state
- 첫 진입 시 `page=0`, `size=20`으로 조회한다.
- 다음 페이지 로딩 중 중복 호출을 막는다.
- 마지막 item이 화면에 나타났고 `hasNext == true`이면 `page + 1`을 조회한다.
- 다음 페이지 성공 시 기존 `communityPosts` 뒤에 append한다.
- 첫 페이지 재조회 시 기존 목록을 교체한다.
- Empty 상태는 `communityPostCount == 0` 기준으로 판정하고 Figma `290:8994`를 따른다.
## 6. Success Criteria
- 커뮤니티 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/community?page=0&size=20`가 호출된다.
- count bar에는 `전체 {communityPostCount}`가 표시된다.
- 우측 버튼은 정렬이 아니라 리스트형/썸네일형 토글로 동작한다.
- 기본값은 리스트형이며 `ic_new_list` 상태로 표시된다.
- 토글 시 썸네일형으로 바뀌고 `ic_new_grid` 상태로 표시된다.
- 리스트형/썸네일형 문구는 ko/en/ja 다국어로 제공된다.
- 유료 미구매 게시글은 회색 `RoundedRectangle` 이미지 영역과 가격 UI를 표시하고 오디오 재생 버튼은 표시하지 않는다.
- 본인 또는 구매한 유저의 게시글은 이미지/GIF와 중앙 오디오 재생 버튼을 표시한다.
- `isCommentAvailable == false`이면 댓글 아이콘과 댓글 개수가 표시되지 않는다.
- 본인 채널의 내 게시글에서만 상단 더보기와 가격 표시가 노출된다.
- 본인 채널이면 리스트형/썸네일형 모두에서 하단 고정 `커뮤니티 글 올리기` 버튼이 표시된다.
- 게시글 더보기 액션은 기존 커뮤니티 페이지와 동일한 수정/삭제/신고/고정 흐름을 사용한다.
- `communityPostCount == 0`이면 커뮤니티 empty 상태가 표시된다.
- 목록 하단 도달 시 `hasNext == true`이면 다음 페이지가 append된다.
## 7. Technical Constraints
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
- 커뮤니티 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/Community/**` 아래에 둔다.
- 기존 구 커뮤니티 전체보기 파일은 필요한 재사용 지점만 참고하고, V2 탭 구현을 위해 직접 대규모 수정하지 않는다.
- 기존 커뮤니티 mutation, 댓글, 구매, 신고, 오디오 플레이어 흐름은 가능한 한 재사용한다.
- 신규 문구는 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
- 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다.
## 8. Assumptions
- `creatorProfileUrl``existOrdered`는 커뮤니티 탭 API 응답에 포함된다.
- 현재 로그인 사용자의 id는 기존 관례대로 `UserDefaults.int(forKey: .userId)`로 확인한다.
- `isOwnPost``post.creatorId == UserDefaults.int(forKey: .userId)`로 판정한다.
- 커뮤니티 게시글 상세 화면은 후속 범위이므로 grid/list item tap 상세 이동은 이번 작업에서 연결하지 않는다.
- 본인 채널 여부는 `CreatorChannelView``isOwnCreatorChannel` 값을 커뮤니티 탭에 주입한다.
## 9. Open Questions
- 해당 없음
## 10. Verification Notes
- 2026-07-05: 사용자 제공 API, 응답 모델, Figma URL, 본인 채널/유료/댓글/토글 요구사항을 기준으로 PRD를 작성했다.
- 2026-07-05: `docs/agent-guides/documentation-policy.md`, 기존 크리에이터 채널 오디오/팬Talk 문서, `CreatorChannelView`, 기존 `CreatorCommunityAllView`, `CreatorCommunityAllItemView`, `CreatorCommunityAllViewModel`을 확인해 문서 범위를 정했다.
- 2026-07-05: 사용자 추가 확인으로 Empty Figma `290:8994`, 썸네일형 유료 미구매 가격 UI 중앙형, `imageUrl == nil`일 때 유/무료 관계없이 이미지 미표시 규칙을 반영했다.