docs(creator-channel): 커뮤니티 탭 API 계획을 기록한다
This commit is contained in:
239
docs/20260621_크리에이터_채널_커뮤니티_탭_API/prd.md
Normal file
239
docs/20260621_크리에이터_채널_커뮤니티_탭_API/prd.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# PRD: 크리에이터 채널 커뮤니티 탭 API
|
||||
|
||||
## 1. Overview
|
||||
크리에이터 채널의 커뮤니티 탭에서 조회자가 볼 수 있는 커뮤니티 게시글 전체 개수와 게시글 목록을 페이징 조회하는 API를 제공한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. Problem
|
||||
- 크리에이터 채널 홈 API는 커뮤니티 게시글 일부를 홈 화면 요약용으로 조회하지만, 커뮤니티 탭은 전체 개수와 페이징 목록이 필요하다.
|
||||
- 기존 홈 API의 커뮤니티 조회 로직이 `home` 도메인 repository 안에 포함되어 있어, 커뮤니티 탭 API에서 그대로 재사용하려면 홈 도메인에 의존하게 된다.
|
||||
- 커뮤니티 게시글 조회 로직은 홈 화면과 커뮤니티 탭에서 모두 쓰일 수 있으므로, 하나의 커뮤니티 조회 도메인으로 분리되어야 한다.
|
||||
- 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 커뮤니티 게시글은 전체 개수와 목록에서 모두 제외되어야 한다.
|
||||
- legacy `/creator-community` 목록 조회는 구매 내역 조건과 성인 필터 조건이 섞일 수 있으므로, `isAdult=false` 조회에서 구매한 19금 게시글이 개수나 목록에 포함되지 않도록 새 v2 조회 정책에서 명확히 보장해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Goals
|
||||
- 크리에이터 채널 커뮤니티 탭 조회 API를 제공한다.
|
||||
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/community`로 한다.
|
||||
- 클라이언트에서 호출하는 공개 API controller/facade/response DTO는 `kr.co.vividnext.sodalive.v2.api.creator.channel.community` 하위 조립 계층에 둔다.
|
||||
- 커뮤니티 게시글 목록, 전체 개수, 구매 여부, 좋아요 수, 댓글 수, 성인 콘텐츠 노출, 유료 오디오 접근 정책은 API 패키지 밖 커뮤니티 도메인 패키지에 둔다.
|
||||
- 크리에이터 채널 홈 API와 커뮤니티 탭 API는 동일한 커뮤니티 조회 도메인을 사용한다.
|
||||
- 응답에는 조회 가능한 커뮤니티 게시글 전체 개수, 게시글 목록, page, size, hasNext를 포함한다.
|
||||
- 게시글 목록 item에는 게시글 id, 크리에이터 id, 크리에이터 닉네임, 작성 시간 UTC, 게시글 본문, 이미지 URL, 오디오 URL, 가격, 댓글 쓰기 가능 여부, 좋아요 개수, 댓글 개수, pin 여부를 포함한다.
|
||||
- 유료 게시글의 오디오 콘텐츠는 조회자가 구매했거나 게시글 작성자인 경우에만 signed URL로 내려준다.
|
||||
- 유료 게시글을 구매하지 않은 조회자에게는 오디오 콘텐츠 URL을 `null`로 내려준다.
|
||||
- 이미지가 없는 게시글은 `imageUrl`을 `null`로 내려준다.
|
||||
- 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 게시글은 전체 개수와 목록에서 제외한다.
|
||||
- 페이징 요청값은 기존 오디오/시리즈 탭 API와 같은 보정 규칙을 따른다.
|
||||
|
||||
---
|
||||
|
||||
## 4. Non-Goals
|
||||
- 커뮤니티 게시글 작성, 수정, 삭제 API는 포함하지 않는다.
|
||||
- 커뮤니티 게시글 구매 API는 포함하지 않는다.
|
||||
- 커뮤니티 댓글 작성, 수정, 삭제, 목록 조회 API는 포함하지 않는다.
|
||||
- 커뮤니티 좋아요 생성/취소 API는 포함하지 않는다.
|
||||
- legacy `/creator-community` API의 공개 endpoint 변경은 포함하지 않는다.
|
||||
- 크리에이터 채널 홈 API의 공개 응답 스키마 변경은 포함하지 않는다.
|
||||
- 홈 API의 커뮤니티 노출 개수나 홈 화면 구성 정책 변경은 포함하지 않는다.
|
||||
- DB schema, 운영 DDL, 마이그레이션은 포함하지 않는다.
|
||||
- 앱 표시용 상대 시간 문구는 서버에서 새로 조합하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 5. Target Users
|
||||
- 회원: 크리에이터 채널 커뮤니티 탭에서 크리에이터의 커뮤니티 게시글을 탐색하는 사용자
|
||||
- 앱 클라이언트: 커뮤니티 탭 구성에 필요한 전체 개수와 게시글 목록을 단일 API 응답으로 표시하려는 클라이언트
|
||||
- 서버 개발자: 홈 API와 커뮤니티 탭 API에서 커뮤니티 조회 정책을 중복 없이 재사용하려는 개발자
|
||||
|
||||
---
|
||||
|
||||
## 6. User Stories
|
||||
- 사용자는 크리에이터 채널 커뮤니티 탭에 들어가면 자신이 조회 가능한 게시글 전체 개수를 확인하고 싶다.
|
||||
- 사용자는 커뮤니티 게시글을 최신순으로 추가 로딩하고 싶다.
|
||||
- 성인 콘텐츠 노출이 꺼진 사용자는 19금 게시글이 개수와 목록에 포함되지 않기를 원한다.
|
||||
- 사용자는 이미지가 없는 게시글도 정상적으로 목록에서 확인하고 싶다.
|
||||
- 사용자는 구매한 유료 게시글의 오디오 콘텐츠를 재생할 수 있어야 한다.
|
||||
- 구매하지 않은 사용자는 유료 게시글의 오디오 콘텐츠 URL을 받지 않아야 한다.
|
||||
- 앱 클라이언트는 댓글 작성 가능 여부, 좋아요 개수, 댓글 개수, pin 여부를 게시글 item에서 바로 확인하고 싶다.
|
||||
- 서버 개발자는 홈 API와 커뮤니티 탭 API가 동일한 커뮤니티 조회 도메인을 사용한다는 것을 패키지 의존 방향으로 확인하고 싶다.
|
||||
|
||||
---
|
||||
|
||||
## 7. Core Features
|
||||
|
||||
### Feature A. 크리에이터 채널 커뮤니티 탭 조회 API
|
||||
|
||||
#### Requirements
|
||||
- 신규 API는 크리에이터 채널 전용 v2 API로 작성한다.
|
||||
- API endpoint는 `GET /api/v2/creator-channels/{creatorId}/community`로 한다.
|
||||
- `creatorId`는 path variable로 받는다.
|
||||
- 커뮤니티 게시글 추가 로딩을 위해 `page`, `size` query parameter를 받는다.
|
||||
- `page`는 0부터 시작하는 page index로 처리한다.
|
||||
- `page`를 보내지 않으면 기본값 `0`을 사용한다.
|
||||
- `size`를 보내지 않으면 기본값 `20`을 사용한다.
|
||||
- `page`가 0보다 작으면 `0`으로 보정한다.
|
||||
- `size`가 20보다 작으면 `20`으로 보정한다.
|
||||
- `size`가 50보다 크면 `50`으로 보정한다.
|
||||
- API는 인증 회원만 조회할 수 있어야 한다.
|
||||
- 비회원이 조회하면 기존 인증 필요 API와 동일하게 `common.error.bad_credentials` 계열 오류를 반환한다.
|
||||
- 조회 대상 회원이 존재하지 않으면 기존 정책과 동일하게 `member.validation.user_not_found` 계열 오류를 반환한다.
|
||||
- 조회 대상 회원이 크리에이터가 아니면 기존 정책과 동일하게 `member.validation.creator_not_found` 계열 오류를 반환한다.
|
||||
- 조회자와 크리에이터 사이에 차단 관계가 있으면 기존 크리에이터 채널 접근 정책과 동일하게 접근 차단 오류를 반환한다.
|
||||
- 공개된 커뮤니티 게시글이 없어도 전체 API는 성공 처리한다.
|
||||
|
||||
#### Edge Cases
|
||||
- 조회자 본인이 크리에이터인 경우에도 같은 응답 스키마를 사용한다.
|
||||
- `page`가 0보다 작거나 `size`가 허용 범위를 벗어나도 400 오류를 반환하지 않고 실제 적용값으로 보정한다.
|
||||
- 요청한 page 범위에 게시글이 없으면 `communityPosts`는 빈 배열, `hasNext`는 `false`로 내려주되 `communityPostCount`는 전체 개수를 유지한다.
|
||||
|
||||
### Feature B. 응답 스키마
|
||||
|
||||
#### Requirements
|
||||
- 응답 DTO는 구현 전에 명시하고 공개 API 계약으로 관리한다.
|
||||
- 응답 최상위 DTO 이름은 `CreatorChannelCommunityTabResponse`로 한다.
|
||||
- 응답에는 다음 값을 포함한다.
|
||||
- `communityPostCount`: 조회자가 조회 가능한 커뮤니티 게시글 전체 개수
|
||||
- `communityPosts`: 커뮤니티 게시글 목록
|
||||
- `page`: 현재 응답의 page index
|
||||
- `size`: 현재 응답의 page size
|
||||
- `hasNext`: 다음 page 존재 여부
|
||||
- `communityPostCount`는 목록 조회와 같은 공개 여부, 작성자, 성인 콘텐츠 노출, 차단 정책을 적용해 계산한다.
|
||||
- `communityPostCount`에는 현재 page에 포함되지 않은 게시글도 포함한다.
|
||||
- `communityPostCount`는 pinned 게시글과 일반 게시글을 모두 포함한 전체 개수다.
|
||||
- `page`, `size`는 fallback 보정 이후 실제 적용된 값을 내려준다.
|
||||
- `hasNext`는 같은 조건에서 다음 page에 노출할 게시글이 있으면 `true`로 내려준다.
|
||||
- 응답 스키마 예시는 다음과 같다.
|
||||
|
||||
```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 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
|
||||
)
|
||||
```
|
||||
|
||||
#### Edge Cases
|
||||
- 조회 가능한 커뮤니티 게시글이 없으면 `communityPostCount`는 `0`, `communityPosts`는 빈 배열, `hasNext`는 `false`로 내려준다.
|
||||
- 이미지가 없는 게시글은 `imageUrl`을 `null`로 내려준다.
|
||||
- 오디오가 없는 게시글은 `audioUrl`을 `null`로 내려준다.
|
||||
- `isCommentAvailable == false`인 게시글의 `commentCount`는 기존 커뮤니티 목록 정책과 동일하게 `0`으로 내려준다.
|
||||
- Boolean 응답 필드는 Jackson 직렬화 시 `commentAvailable`, `pinned`로 바뀌지 않고 `isCommentAvailable`, `isPinned`로 내려가야 한다.
|
||||
|
||||
### Feature C. 커뮤니티 게시글 목록과 개수
|
||||
|
||||
#### Requirements
|
||||
- 조회 대상은 지정한 `creatorId`가 작성한 커뮤니티 게시글로 제한한다.
|
||||
- 활성 게시글만 조회한다.
|
||||
- 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 게시글은 목록에서 제외한다.
|
||||
- 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 게시글은 `communityPostCount`에서도 제외한다.
|
||||
- 성인 콘텐츠 필터는 구매 여부보다 우선 적용한다.
|
||||
- 조회자가 19금 게시글을 구매했더라도 성인 콘텐츠 노출 정책이 false이면 해당 게시글은 목록과 전체 개수에 포함하지 않는다.
|
||||
- 목록은 pinned 게시글을 먼저 노출하고, 그 다음 일반 게시글을 노출한다.
|
||||
- pinned 게시글 사이의 정렬은 `fixedAt desc`, `id desc`를 따른다.
|
||||
- 일반 게시글 사이의 정렬은 `createdAt desc`, `id desc`를 따른다.
|
||||
- 목록은 `page`, `size` 기준으로 페이징 조회한다.
|
||||
- 다음 page 존재 여부는 `size + 1`개를 조회하거나 동등한 방식으로 판단하되, 응답 목록에는 최대 `size`개만 내려준다.
|
||||
- `createdAtUtc`는 게시글 생성 시간을 UTC 기준 ISO-8601 문자열로 내려준다.
|
||||
- `imageUrl`은 커뮤니티 게시글 이미지 path가 있으면 기존 CDN URL 조합 정책으로 내려준다.
|
||||
- `likeCount`는 활성 좋아요 수를 기준으로 계산한다.
|
||||
- `commentCount`는 조회자가 볼 수 있는 활성 최상위 댓글 수를 기준으로 계산한다.
|
||||
- 댓글 수 계산에는 기존 커뮤니티 댓글의 차단 관계와 비밀 댓글 노출 정책을 적용한다.
|
||||
|
||||
#### Edge Cases
|
||||
- pinned 게시글과 일반 게시글이 섞여 있어도 전체 목록은 하나의 페이징 결과로 내려준다.
|
||||
- pinned 게시글 개수가 page size를 초과하면 첫 page는 pinned 게시글만 포함될 수 있다.
|
||||
- 게시글 작성자가 조회자인 경우에도 성인 콘텐츠 노출 정책이 false이면 19금 게시글은 제외한다.
|
||||
- 좋아요나 댓글이 없는 게시글은 `likeCount`, `commentCount`를 `0`으로 내려준다.
|
||||
|
||||
### Feature D. 유료 오디오 콘텐츠 접근 정책
|
||||
|
||||
#### Requirements
|
||||
- 커뮤니티 게시글에 오디오 path가 없으면 `audioUrl`은 `null`이다.
|
||||
- 무료 게시글에 오디오 path가 있으면 signed URL을 내려준다.
|
||||
- 유료 게시글에 오디오 path가 있고 조회자가 해당 게시글을 구매했으면 signed URL을 내려준다.
|
||||
- 유료 게시글에 오디오 path가 있고 조회자가 게시글 작성자이면 signed URL을 내려준다.
|
||||
- 유료 게시글에 오디오 path가 있지만 조회자가 구매하지 않았고 게시글 작성자도 아니면 `audioUrl`은 `null`이다.
|
||||
- signed URL 생성은 기존 `AudioContentCloudFront.generateSignedURL` 방식을 재사용한다.
|
||||
- signed URL 만료 시간은 legacy 커뮤니티 목록 정책과 동일하게 30분을 기본으로 한다.
|
||||
- 유료 게시글 본문은 기존 크리에이터 채널 홈 API의 유료 커뮤니티 본문 마스킹 정책을 따른다.
|
||||
- 유료 게시글 오디오 접근 여부는 `CanUsage.PAID_COMMUNITY_POST`의 유효 구매 내역을 기준으로 판단한다.
|
||||
- 환불된 구매 내역은 접근 가능 구매로 보지 않는다.
|
||||
|
||||
#### Edge Cases
|
||||
- 조회자가 구매했더라도 성인 콘텐츠 노출 정책이 false인 19금 게시글은 목록에 포함되지 않으므로 signed URL도 내려주지 않는다.
|
||||
- 구매 내역이 중복으로 있어도 응답 item은 게시글 1개로 중복 없이 내려준다.
|
||||
- signed URL 생성 대상 path가 blank이면 `audioUrl`은 `null`로 내려준다.
|
||||
|
||||
### Feature E. 커뮤니티 조회 도메인 분리
|
||||
|
||||
#### Requirements
|
||||
- 커뮤니티 탭 공개 API controller/facade/response DTO는 `kr.co.vividnext.sodalive.v2.api.creator.channel.community` 하위에 둔다.
|
||||
- 커뮤니티 게시글 조회 service, 순수 정책, domain model, port, repository는 `kr.co.vividnext.sodalive.v2.creator.channel.community` 하위에 둔다.
|
||||
- 도메인 조회 계층은 API response DTO를 import하지 않는다.
|
||||
- 도메인 조회 계층은 API facade나 controller를 import하지 않는다.
|
||||
- 의존 방향은 항상 `v2.api.creator.channel.community -> v2.creator.channel.community`이다.
|
||||
- 크리에이터 채널 홈 API는 홈 도메인 내부에 커뮤니티 조회 쿼리를 직접 보유하지 않고, 분리된 커뮤니티 조회 도메인을 사용한다.
|
||||
- 홈 API의 공개 응답 필드명과 필드 의미는 변경하지 않는다.
|
||||
- 홈 API의 커뮤니티 요약 조회 limit와 notice 조회 정책은 기존 동작을 유지한다.
|
||||
- legacy `kr.co.vividnext.sodalive.explorer.profile.creatorCommunity` 쓰기/상세/댓글/좋아요/구매 기능은 이번 분리 대상에 포함하지 않는다.
|
||||
|
||||
#### Edge Cases
|
||||
- 홈 API와 커뮤니티 탭 API가 같은 domain model을 사용하더라도 각 API response DTO는 각 API 패키지에서 따로 소유한다.
|
||||
- 커뮤니티 도메인 분리 과정에서 기존 홈 API controller mapping과 신규 커뮤니티 탭 controller mapping이 충돌하면 안 된다.
|
||||
- 도메인 분리 후 `v2.creator.channel.community` 하위에서 `v2.api.*` import 검색 결과가 0건이어야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 8. Technical Constraints
|
||||
- 빌드 도구는 Gradle Wrapper(`./gradlew`)를 사용한다.
|
||||
- Kotlin + Spring Boot 2.7.14 기존 스타일을 따른다.
|
||||
- 신규 공개 API 스키마는 구현 전에 PRD와 구현 계획/TASK 문서에 명시한다.
|
||||
- 공개 API controller/facade/response DTO는 `kr.co.vividnext.sodalive.v2.api.creator.channel.community` 하위에 둔다.
|
||||
- API 조립 계층은 HTTP 계약과 공개 응답 변환만 담당한다.
|
||||
- 도메인 조회 코드는 `kr.co.vividnext.sodalive.v2.creator.channel.community` 하위에 둔다.
|
||||
- 도메인 패키지는 `kr.co.vividnext.sodalive.v2.api.*` 패키지에 의존하지 않는다.
|
||||
- 기존 크리에이터 채널 홈/라이브/오디오/시리즈 API의 인증, 예외, 성인 콘텐츠 노출, 차단 관계 정책을 재사용한다.
|
||||
- 성인 콘텐츠 노출 여부는 기존 v2 탭 API와 동일하게 `MemberContentPreferenceService`와 `isAdultVisibleByPolicy`를 기준으로 계산한다.
|
||||
- 페이징 응답은 기존 오디오/시리즈 탭 API와 같은 `page`, `size`, `hasNext` 패턴을 따른다.
|
||||
- 이미지 URL은 기존 `String?.toCdnUrl(cloudFrontHost)` 방식과 같은 CDN URL 조합 정책을 따른다.
|
||||
- 오디오 URL은 콘텐츠 CloudFront signed URL 생성 정책을 따른다.
|
||||
- 날짜 응답은 UTC 기준 ISO-8601 문자열로 내려준다.
|
||||
|
||||
---
|
||||
|
||||
## 9. Metrics
|
||||
- 커뮤니티 탭 API 성공/실패 건수
|
||||
- 커뮤니티 탭 API 응답 시간
|
||||
- 커뮤니티 탭 추가 로딩 요청 건수
|
||||
- 성인 콘텐츠 노출 정책이 false인 조회에서 19금 게시글이 개수와 목록에 포함되지 않는 테스트 통과 여부
|
||||
- 유료 오디오 콘텐츠 signed URL/null 처리 테스트 통과 여부
|
||||
- 홈 API 커뮤니티 요약 조회 회귀 테스트 통과 여부
|
||||
- `v2.creator.channel.community` 도메인 패키지의 `v2.api.*` import 검색 결과 0건 여부
|
||||
|
||||
---
|
||||
|
||||
## 10. Open Questions
|
||||
- 없음. 구현 중 새 정책 결정이 필요하면 구현 전에 이 PRD와 `plan-task.md`를 먼저 갱신한다.
|
||||
Reference in New Issue
Block a user