Files

19 KiB

PRD: 커뮤니티 게시물 상세 API

1. Overview

커뮤니티 게시물 ID로 게시물 상세와 초기 댓글 20개를 조회하고, 이후 댓글/답글을 페이징 조회하는 v2 API를 제공한다.


2. Problem

  • 크리에이터 채널 커뮤니티 탭은 게시글 목록을 제공하지만, 게시물 상세 화면에서 게시물 1건과 초기 댓글을 함께 조회하는 v2 API가 없다.
  • 기존 legacy /creator-community/{id} 상세 API는 firstComment 1개만 제공하고, 클라이언트가 상세 진입 직후 댓글 목록을 별도로 다시 조회해야 한다.
  • legacy 댓글 API는 timezone 기반 표시 문자열을 반환하지만, 신규 상세 화면은 UTC 기반 날짜/시간과 v2 응답 패턴이 필요하다.
  • 커뮤니티 탭의 CreatorChannelCommunityPost에는 isLiked가 이미 포함되어 있으므로 상세 API도 동일한 게시물 필드 의미를 유지해야 한다.
  • 댓글/답글 조회는 legacy entity와 필터 정책을 재사용할 수 있지만, 공개 API 조립 계층과 도메인 조회 계층은 기존 v2 패키지 경계를 따라 분리되어야 한다.

3. Goals

  • 커뮤니티 게시물 ID로 게시물 상세를 조회하는 API를 제공한다.
  • 상세 응답의 게시물 필드는 크리에이터 채널 커뮤니티 탭의 CreatorChannelCommunityPostResponse와 동일한 의미와 필드명을 사용한다.
  • 상세 응답의 게시물 필드에는 조회자의 활성 좋아요 여부인 isLiked를 포함한다.
  • 상세 응답에는 첫 댓글 20개를 함께 포함하며, 이 댓글 묶음은 댓글 조회 API 응답과 동일한 response data class를 사용한다.
  • 커뮤니티 댓글 목록을 page/size 기반으로 페이징 조회하는 API를 제공한다.
  • 커뮤니티 댓글의 답글 목록을 page/size 기반으로 페이징 조회하는 API를 제공한다.
  • API controller/facade/response DTO는 kr.co.vividnext.sodalive.v2.api.creator.channel.community 하위 조립 계층에 둔다.
  • 게시물 상세, 댓글, 답글 조회 정책과 domain model, port, repository는 kr.co.vividnext.sodalive.v2.creator.channel.community 하위 도메인 조회 계층에 둔다.
  • 기존 커뮤니티 탭 조회에서 재사용 가능한 CreatorChannelCommunityPost, CreatorChannelCommunityQueryService, CreatorChannelCommunityQueryPolicy, repository helper를 우선 재사용한다.
  • legacy CreatorCommunityComment entity와 차단/비밀 댓글 필터 정책은 재사용하되, legacy response DTO를 v2 공개 응답으로 직접 노출하지 않는다.

4. Non-Goals

  • 커뮤니티 게시물 작성, 수정, 삭제 API는 포함하지 않는다.
  • 커뮤니티 게시물 좋아요 생성/취소 API는 변경하지 않는다.
  • 커뮤니티 댓글/답글 작성, 수정, 삭제 API는 포함하지 않는다.
  • legacy /creator-community API의 endpoint와 응답 스키마는 변경하지 않는다.
  • 커뮤니티 탭 목록 API의 공개 응답 스키마는 변경하지 않는다.
  • DB schema, 운영 DDL, 마이그레이션은 포함하지 않는다.
  • 앱 표시용 상대 시간 문구는 서버에서 새로 조합하지 않는다.
  • 댓글 좋아요, 댓글 신고, 답글 전체 개수의 댓글 item 내 노출은 포함하지 않는다.

5. Target Users

  • 회원: 커뮤니티 게시물 상세 화면에서 게시물 내용과 댓글을 확인하는 사용자
  • 앱 클라이언트: 상세 진입 시 게시물과 초기 댓글 20개를 한 번에 렌더링하고 이후 댓글/답글을 추가 로딩하려는 클라이언트
  • 서버 개발자: v2 커뮤니티 탭, 상세, 댓글 조회 정책을 중복 없이 재사용해야 하는 개발자

6. 조사 결과

재사용 후보

  • CreatorChannelCommunityPost

    • 파일: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityTab.kt
    • 현재 필드: postId, creatorId, creatorNickname, creatorProfileUrl, imageUrl, audioUrl, content, price, createdAt, existOrdered, isCommentAvailable, likeCount, commentCount, isPinned, isLiked
    • 상세 응답의 게시물 본문은 이 domain model을 재사용한다.
  • CreatorChannelCommunityPostResponse

    • 파일: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityTabResponse.kt
    • 현재 필드: postId, creatorId, creatorNickname, creatorProfileUrl, createdAtUtc, content, imageUrl, audioUrl, price, isCommentAvailable, existOrdered, likeCount, commentCount, isPinned, isLiked
    • 상세 응답의 게시물 필드는 이 response data class와 동일한 필드명/의미를 사용한다.
  • CreatorChannelCommunityQueryService

    • 파일: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt
    • 현재 커뮤니티 탭과 채널 홈 커뮤니티 게시글 조회를 담당한다.
    • 게시물 상세 조회 method를 추가해 게시물 1건 조립과 유료 콘텐츠 접근 정책, CDN URL, signed audio URL, 본문 마스킹, isLiked 전달을 재사용한다.
  • CreatorChannelCommunityQueryPolicy

    • 파일: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicy.kt
    • page 기본값/보정 규칙과 hasNext 판정, 유료 본문 마스킹 정책을 제공한다.
    • 댓글/답글 page도 같은 page=0, size=20, size min=20, size max=50 규칙을 따른다.
  • CreatorCommunityComment

    • 파일: src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/comment/CreatorCommunityComment.kt
    • legacy 댓글 entity이며 parent로 답글을 표현한다.
    • 신규 v2 조회 repository에서 같은 entity를 조회하되 legacy DTO는 공개 API로 재사용하지 않는다.
  • legacy 댓글 필터 정책

    • 파일: src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/comment/CreatorCommunityCommentRepository.kt
    • 활성 댓글, 최상위 댓글/답글 구분, 작성자 차단/피차단 제외, 비밀 댓글 노출 정책을 참고한다.

기존 endpoint

  • 커뮤니티 탭: GET /api/v2/creator-channels/{creatorId}/community
  • legacy 게시물 상세: GET /creator-community/{id}
  • legacy 댓글 목록: GET /creator-community/{id}/comment
  • legacy 답글 목록: GET /creator-community/comment/{id}

7. User Stories

  • 사용자는 커뮤니티 게시물 상세 화면에 들어가면 게시물 내용, 미디어, 좋아요 상태, 좋아요 수, 댓글 수를 즉시 확인하고 싶다.
  • 사용자는 게시물 상세 화면 진입 직후 최신 댓글 20개를 바로 보고 싶다.
  • 사용자는 댓글 목록을 계속 아래로 스크롤하며 추가 조회하고 싶다.
  • 사용자는 댓글에 달린 최신 답글 1개를 댓글 목록에서 미리 보고 싶다.
  • 사용자는 특정 댓글의 답글 목록을 추가로 페이징 조회하고 싶다.
  • 앱 클라이언트는 상세 API의 초기 댓글 응답과 댓글 조회 API 응답을 같은 data class로 처리하고 싶다.

8. Core Features

Feature A. 커뮤니티 게시물 상세 조회 API

Endpoint

  • GET /api/v2/creator-channels/community-posts/{postId}

Requirements

  • postId는 path variable로 받는다.
  • API는 인증 회원만 조회할 수 있어야 한다.
  • 비회원이 조회하면 기존 인증 필요 v2 API와 동일하게 common.error.bad_credentials 계열 오류를 반환한다.
  • 게시물이 존재하지 않거나 비활성 상태이면 기존 커뮤니티 정책과 동일하게 조회 불가 오류를 반환한다.
  • 조회자의 성인 콘텐츠 노출 정책이 false이고 게시물이 19금이면 조회 불가 오류를 반환한다.
  • 조회자와 게시물 작성자 사이에 차단 관계가 있으면 기존 크리에이터 채널 접근 정책과 동일하게 접근 차단 오류를 반환한다.
  • 게시물 필드는 커뮤니티 탭의 CreatorChannelCommunityPostResponse와 동일한 필드명과 의미를 사용한다.
  • 게시물 필드에는 isLiked를 포함하며, 조회자의 CreatorCommunityLike.isActive == true인 좋아요가 있으면 true다.
  • 상세 응답에는 comments 필드로 첫 댓글 20개를 포함한다.
  • comments는 댓글 조회 API의 CreatorChannelCommunityCommentsResponse와 동일한 response data class를 사용한다.
  • 상세 응답의 comments.page0, comments.size20으로 내려준다.
  • 상세 응답의 comments.hasNext는 같은 조건에서 21번째 댓글이 있으면 true다.
  • isCommentAvailable == false인 게시물은 comments.commentCount=0, comments.comments=[], comments.hasNext=false로 내려준다.
  • 유료 게시물의 본문, 이미지, 오디오 접근 정책은 커뮤니티 탭 CreatorChannelCommunityPost와 동일하게 적용한다.

Response Data Class

data class CreatorChannelCommunityPostDetailResponse(
    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 existOrdered: Boolean,
    val likeCount: Int,
    val commentCount: Int,
    @JsonProperty("isPinned")
    val isPinned: Boolean,
    @JsonProperty("isLiked")
    val isLiked: Boolean,
    val comments: CreatorChannelCommunityCommentsResponse
)

Edge Cases

  • 게시물 작성자가 조회자인 경우에도 성인 콘텐츠 노출 정책이 false이면 19금 게시물은 조회할 수 없다.
  • 유료 게시물을 구매하지 않은 조회자에게는 커뮤니티 탭과 동일하게 이미지 URL과 오디오 URL을 null로 내려주고 본문을 마스킹한다.
  • 게시물에 이미지나 오디오가 없으면 각각 null로 내려준다.
  • 댓글이 없어도 상세 API는 성공하며 comments.comments=[]를 내려준다.

Feature B. 커뮤니티 댓글 조회 API

Endpoint

  • GET /api/v2/creator-channels/community-posts/{postId}/comments

Requirements

  • postId는 path variable로 받는다.
  • page, size query parameter를 받는다.
  • page 기본값은 0이다.
  • size 기본값은 20이다.
  • page가 0보다 작으면 0으로 보정한다.
  • size가 20보다 작으면 20으로 보정한다.
  • size가 50보다 크면 50으로 보정한다.
  • API는 인증 회원만 조회할 수 있어야 한다.
  • 게시물이 존재하지 않거나 비활성 상태이면 기존 커뮤니티 정책과 동일하게 조회 불가 오류를 반환한다.
  • 조회자의 성인 콘텐츠 노출 정책이 false이고 게시물이 19금이면 조회 불가 오류를 반환한다.
  • 조회자와 게시물 작성자 사이에 차단 관계가 있으면 빈 목록이 아니라 접근 차단 오류를 반환한다.
  • isCommentAvailable == false인 게시물은 commentCount=0, comments=[], hasNext=false로 내려준다.
  • commentCount는 조회자가 볼 수 있는 활성 최상위 댓글 전체 개수다.
  • comments는 조회자가 볼 수 있는 활성 최상위 댓글 목록이다.
  • 댓글 작성자가 조회자와 차단/피차단 관계이면 목록과 전체 개수에서 제외한다.
  • 비밀 댓글은 게시물 작성자 또는 댓글 작성자 본인에게만 노출한다.
  • 각 댓글에는 비밀 댓글 여부인 isSecret을 포함한다.
  • 댓글 정렬은 최신순 createdAt desc, id desc를 따른다.
  • 각 댓글에는 최신 답글 1개를 latestReply로 포함한다.
  • 최신 답글은 해당 댓글의 활성 답글 중 조회자가 볼 수 있는 답글을 createdAt desc, id desc로 정렬했을 때 첫 번째 항목이다.
  • 최신 답글이 없으면 latestReplynull이다.
  • 날짜/시간은 UTC 기준 ISO-8601 문자열인 createdAtUtc로 내려준다.
  • 작성자 프로필 이미지가 없으면 기존 기본 프로필 이미지 URL을 내려준다.
  • 탈퇴 회원 닉네임 prefix 제거는 기존 removeDeletedNicknamePrefix 정책을 적용한다.

Response Data Class

data class CreatorChannelCommunityCommentsResponse(
    val commentCount: Int,
    val comments: List<CreatorChannelCommunityCommentResponse>,
    val page: Int,
    val size: Int,
    @JsonProperty("hasNext")
    val hasNext: Boolean
)

data class CreatorChannelCommunityCommentResponse(
    val commentId: Long,
    val writerId: Long,
    val writerProfileImageUrl: String,
    val writerNickname: String,
    val content: String,
    @JsonProperty("isSecret")
    val isSecret: Boolean,
    val createdAtUtc: String,
    val latestReply: CreatorChannelCommunityReplyResponse?
)

data class CreatorChannelCommunityReplyResponse(
    val commentId: Long,
    val writerId: Long,
    val writerProfileImageUrl: String,
    val writerNickname: String,
    val content: String,
    val createdAtUtc: String
)

Edge Cases

  • 요청한 page 범위에 댓글이 없으면 comments=[], hasNext=false이고 commentCount는 전체 개수를 유지한다.
  • 댓글은 있지만 조회자의 차단 관계나 비밀 댓글 정책으로 모두 제외되면 commentCount=0, comments=[]가 된다.
  • 댓글 작성자 프로필 이미지 path가 blank이면 기본 프로필 이미지 URL을 사용한다.
  • 답글 작성자가 조회자와 차단/피차단 관계이면 latestReply 후보에서 제외한다.

Feature C. 커뮤니티 댓글 답글 조회 API

Endpoint

  • GET /api/v2/creator-channels/community-comments/{commentId}/replies

Requirements

  • commentId는 path variable로 받는다.
  • page, size query parameter를 받는다.
  • page/size 보정 규칙은 댓글 조회 API와 동일하다.
  • API는 인증 회원만 조회할 수 있어야 한다.
  • commentId가 최상위 댓글이 아니거나 존재하지 않거나 비활성 상태이면 기존 커뮤니티 정책과 동일하게 조회 불가 오류를 반환한다.
  • 부모 댓글이 속한 게시물이 존재하지 않거나 비활성 상태이면 조회 불가 오류를 반환한다.
  • 조회자의 성인 콘텐츠 노출 정책이 false이고 부모 댓글이 속한 게시물이 19금이면 조회 불가 오류를 반환한다.
  • 조회자와 게시물 작성자 사이에 차단 관계가 있으면 접근 차단 오류를 반환한다.
  • 부모 댓글 자체가 조회자에게 보이지 않는 비밀 댓글이거나 부모 댓글 작성자가 조회자와 차단/피차단 관계이면 조회 불가 오류를 반환한다.
  • replyCount는 조회자가 볼 수 있는 활성 답글 전체 개수다.
  • replies는 조회자가 볼 수 있는 활성 답글 목록이다.
  • 답글 작성자가 조회자와 차단/피차단 관계이면 목록과 전체 개수에서 제외한다.
  • 답글 정렬은 최신순 createdAt desc, id desc를 따른다.
  • 답글 item은 댓글 item과 같은 작성자/본문/UTC 작성 시간 구조를 사용하지만 latestReply 필드는 포함하지 않는다.

Response Data Class

data class CreatorChannelCommunityRepliesResponse(
    val replyCount: Int,
    val replies: List<CreatorChannelCommunityReplyResponse>,
    val page: Int,
    val size: Int,
    @JsonProperty("hasNext")
    val hasNext: Boolean
)

Edge Cases

  • 요청한 page 범위에 답글이 없으면 replies=[], hasNext=false이고 replyCount는 전체 개수를 유지한다.
  • 답글이 있지만 조회자의 차단 관계로 모두 제외되면 replyCount=0, replies=[]가 된다.

Feature D. API 조립 계층과 도메인 조회 계층 분리

Requirements

  • controller/facade/response DTO는 kr.co.vividnext.sodalive.v2.api.creator.channel.community 하위에 둔다.
  • domain model, query service, policy, port, repository는 kr.co.vividnext.sodalive.v2.creator.channel.community 하위에 둔다.
  • API 조립 계층은 인증 회원 확인, path/query parameter 수신, domain 결과를 response DTO로 변환하는 책임만 가진다.
  • 도메인 조회 계층은 API response DTO를 import하지 않는다.
  • 도메인 조회 계층은 API facade나 controller를 import하지 않는다.
  • 의존 방향은 항상 v2.api.creator.channel.community -> v2.creator.channel.community이다.
  • 댓글/답글 조회용 domain model은 v2 전용으로 만들고, legacy GetCommunityPostCommentListResponseGetCommunityPostCommentListItem은 공개 v2 응답에 재사용하지 않는다.

Edge Cases

  • 기존 GET /api/v2/creator-channels/{creatorId}/community mapping과 신규 GET /api/v2/creator-channels/community-posts/{postId} mapping이 충돌하면 안 된다.
  • 도메인 조회 계층에 kr.co.vividnext.sodalive.v2.api.* import가 생기면 안 된다.

9. Technical Constraints

  • 빌드 도구는 Gradle Wrapper(./gradlew)를 사용한다.
  • Kotlin + Spring Boot 2.7.14 기존 스타일을 따른다.
  • 신규 공개 API 스키마는 구현 전에 PRD와 구현 계획/TASK 문서에 명시한다.
  • Boolean 응답 필드는 Jackson 직렬화 이름 보존을 위해 @JsonProperty("isCommentAvailable"), @JsonProperty("isPinned"), @JsonProperty("isLiked"), @JsonProperty("hasNext")를 명시한다.
  • 날짜 응답은 kr.co.vividnext.sodalive.extensions.toUtcIso를 우선 재사용해 UTC 기준 ISO-8601 문자열로 내려준다.
  • 프로필 이미지 URL은 기존 String?.toCdnUrl(cloudFrontHost)와 기본 프로필 이미지 URL 정책을 따른다.
  • 커뮤니티 게시물 상세의 유료 콘텐츠 접근 정책은 커뮤니티 탭과 동일하게 price <= 0 || viewerId == creatorId || existOrdered를 기준으로 한다.
  • 댓글/답글 조회는 CreatorCommunityComment entity를 조회하되, v2 domain record와 response DTO로 변환한다.
  • 댓글/답글 count와 list 조건은 동일해야 한다.
  • list 조회는 size + 1개를 조회하거나 동등한 방식으로 hasNext를 판단하고, 응답 목록에는 최대 size개만 포함한다.

10. Success Criteria

  • GET /api/v2/creator-channels/community-posts/{postId}가 커뮤니티 탭 게시물과 동일한 게시물 필드, isLiked, 초기 댓글 20개를 반환한다.
  • 상세 API의 comments 구조가 댓글 조회 API 응답 data class와 동일하다.
  • GET /api/v2/creator-channels/community-posts/{postId}/comments가 조회 가능한 댓글 전체 개수, 댓글 page, 최신 답글 1개를 반환한다.
  • GET /api/v2/creator-channels/community-comments/{commentId}/replies가 조회 가능한 답글 전체 개수와 답글 page를 반환한다.
  • 댓글/답글 날짜는 UTC ISO-8601 문자열로 내려간다.
  • 댓글/답글 작성자 차단/피차단, 비밀 댓글, 비활성 댓글, 19금 게시물 접근 정책이 테스트로 구분된다.
  • isCommentAvailable == false인 게시물은 상세 초기 댓글과 댓글 목록 API 모두 빈 댓글 응답을 반환한다.
  • 기존 커뮤니티 탭 API와 크리에이터 채널 홈 API의 커뮤니티 게시글 응답이 회귀 없이 통과한다.
  • v2.creator.channel.community 도메인 패키지의 v2.api.* import 검색 결과가 0건이다.

11. Open Questions

  • 없음. 구현 중 댓글 비밀 정책이나 endpoint 경로에 대한 추가 결정이 필요하면 구현 전에 이 PRD와 plan-task.md를 먼저 갱신한다.