19 KiB
19 KiB
PRD: 커뮤니티 게시물 상세 API
1. Overview
커뮤니티 게시물 ID로 게시물 상세와 초기 댓글 20개를 조회하고, 이후 댓글/답글을 페이징 조회하는 v2 API를 제공한다.
2. Problem
- 크리에이터 채널 커뮤니티 탭은 게시글 목록을 제공하지만, 게시물 상세 화면에서 게시물 1건과 초기 댓글을 함께 조회하는 v2 API가 없다.
- 기존 legacy
/creator-community/{id}상세 API는firstComment1개만 제공하고, 클라이언트가 상세 진입 직후 댓글 목록을 별도로 다시 조회해야 한다. - 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
CreatorCommunityCommententity와 차단/비밀 댓글 필터 정책은 재사용하되, legacy response DTO를 v2 공개 응답으로 직접 노출하지 않는다.
4. Non-Goals
- 커뮤니티 게시물 작성, 수정, 삭제 API는 포함하지 않는다.
- 커뮤니티 게시물 좋아요 생성/취소 API는 변경하지 않는다.
- 커뮤니티 댓글/답글 작성, 수정, 삭제 API는 포함하지 않는다.
- legacy
/creator-communityAPI의 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.page는0,comments.size는20으로 내려준다. - 상세 응답의
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,sizequery 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는 조회자가 볼 수 있는 활성 최상위 댓글 목록이다.- 댓글 작성자가 조회자와 차단/피차단 관계이면 목록과 전체 개수에서 제외한다.
- 비밀 댓글은 게시물 작성자 또는 댓글 작성자 본인에게만 노출한다.
- 댓글 정렬은 최신순
createdAt desc,id desc를 따른다. - 각 댓글에는 최신 답글 1개를
latestReply로 포함한다. - 최신 답글은 해당 댓글의 활성 답글 중 조회자가 볼 수 있는 답글을
createdAt desc,id desc로 정렬했을 때 첫 번째 항목이다. - 최신 답글이 없으면
latestReply는null이다. - 날짜/시간은 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 writerProfileImageUrl: String,
val writerNickname: String,
val content: String,
val createdAtUtc: String,
val latestReply: CreatorChannelCommunityReplyResponse?
)
data class CreatorChannelCommunityReplyResponse(
val commentId: 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,sizequery 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
GetCommunityPostCommentListResponse와GetCommunityPostCommentListItem은 공개 v2 응답에 재사용하지 않는다.
Edge Cases
- 기존
GET /api/v2/creator-channels/{creatorId}/communitymapping과 신규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를 기준으로 한다. - 댓글/답글 조회는
CreatorCommunityCommententity를 조회하되, 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를 먼저 갱신한다.