Files

16 KiB

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로 내려준다.
  • 이미지가 없는 게시글은 imageUrlnull로 내려준다.
  • 조회자의 성인 콘텐츠 노출 정책이 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는 빈 배열, hasNextfalse로 내려주되 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로 내려준다.
  • 응답 스키마 예시는 다음과 같다.
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

  • 조회 가능한 커뮤니티 게시글이 없으면 communityPostCount0, communityPosts는 빈 배열, hasNextfalse로 내려준다.
  • 이미지가 없는 게시글은 imageUrlnull로 내려준다.
  • 오디오가 없는 게시글은 audioUrlnull로 내려준다.
  • 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, commentCount0으로 내려준다.

Feature D. 유료 오디오 콘텐츠 접근 정책

Requirements

  • 커뮤니티 게시글에 오디오 path가 없으면 audioUrlnull이다.
  • 무료 게시글에 오디오 path가 있으면 signed URL을 내려준다.
  • 유료 게시글에 오디오 path가 있고 조회자가 해당 게시글을 구매했으면 signed URL을 내려준다.
  • 유료 게시글에 오디오 path가 있고 조회자가 게시글 작성자이면 signed URL을 내려준다.
  • 유료 게시글에 오디오 path가 있지만 조회자가 구매하지 않았고 게시글 작성자도 아니면 audioUrlnull이다.
  • 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이면 audioUrlnull로 내려준다.

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와 동일하게 MemberContentPreferenceServiceisAdultVisibleByPolicy를 기준으로 계산한다.
  • 페이징 응답은 기존 오디오/시리즈 탭 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를 먼저 갱신한다.