14 KiB
14 KiB
PRD: 크리에이터 채널 FanTalk 탭 API
1. Overview
크리에이터 채널의 FanTalk 탭에서 전체 FanTalk 개수와 FanTalk 글 목록을 페이징 조회하는 API를 제공한다.
2. Problem
- 크리에이터 채널 홈 API는 FanTalk 전체 개수와 최신 FanTalk 1건만 요약으로 제공한다.
- FanTalk 탭은 전체 개수, 페이징된 글 목록, 각 글에 달린 크리에이터 답글을 함께 표시해야 한다.
- legacy
/profile/{id}/cheersAPI는 FanTalk를 조회하지만 날짜를 timezone 기반 표시 문자열로 내려주므로, V2 크리에이터 채널 탭 API에서 요구하는 UTC 기반 응답 계약과 맞지 않는다. - FanTalk 엔티티는 legacy
CreatorCheers를 사용하되, 신규 API 조립 계층과 도메인 조회 계층은 기존 V2 크리에이터 채널 탭 패턴처럼 분리해야 한다.
3. Goals
- 크리에이터 채널 FanTalk 탭 조회 API를 제공한다.
- API endpoint는
GET /api/v2/creator-channels/{creatorId}/fan-talks로 한다. - 클라이언트에서 호출하는 공개 API controller/facade/response DTO는
kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk하위 조립 계층에 둔다. - FanTalk 목록, 전체 개수, 답글 조회, 페이징 보정, 차단 필터링 같은 조회 책임은 API 패키지 밖의
kr.co.vividnext.sodalive.v2.creator.channel.fantalk도메인 조회 계층에 둔다. - FanTalk 저장 엔티티는 기존
kr.co.vividnext.sodalive.explorer.profile.CreatorCheers를 사용한다. - 응답에는 조회 가능한 전체 FanTalk 개수, FanTalk 글 목록, page, size, hasNext를 포함한다.
- FanTalk 글 item에는 글쓴이 닉네임, 글쓴이 ID, 글쓴이 프로필 이미지, 글쓴이가 쓴 글, 글 쓴 시간 UTC, 크리에이터가 쓴 답글 목록을 포함한다.
- 크리에이터 답글 item도 FanTalk 글과 동일한 작성자/본문/시간 필드 구조를 사용한다.
- 페이징 요청값은 기존 V2 크리에이터 채널 커뮤니티/시리즈 탭 API와 같은 보정 규칙을 따른다.
4. Non-Goals
- FanTalk 작성, 수정, 삭제 API는 포함하지 않는다.
- FanTalk 답글 작성, 수정, 삭제 API는 포함하지 않는다.
- 팬 회원 간 답글 작성/조회 기능은 포함하지 않는다. 현재 팬끼리 답글을 작성할 수 없으므로 FanTalk 탭 응답에서도 팬 간 답글을 고려하지 않는다.
- legacy
/profile/{id}/cheersAPI의 공개 endpoint나 응답 스키마 변경은 포함하지 않는다. - 크리에이터 채널 홈 API의 공개 응답 스키마 변경은 포함하지 않는다.
- DB schema, 운영 DDL, 마이그레이션은 포함하지 않는다.
- 앱 표시용 상대 시간 문구나 timezone 변환 문자열은 서버에서 새로 조합하지 않는다.
- 신고, 언어 감지, 푸시 알림 정책 변경은 포함하지 않는다.
5. Target Users
- 회원: 크리에이터 채널 FanTalk 탭에서 다른 팬들의 FanTalk 글과 크리에이터 답글을 탐색하는 사용자
- 앱 클라이언트: FanTalk 탭 구성에 필요한 전체 개수와 페이징 목록을 단일 API 응답으로 표시하려는 클라이언트
- 서버 개발자: 기존
CreatorCheers저장 구조를 유지하면서 V2 조회 계층을 분리하려는 개발자
6. User Stories
- 사용자는 크리에이터 채널 FanTalk 탭에 들어가면 전체 FanTalk 개수를 확인하고 싶다.
- 사용자는 FanTalk 글을 최신순으로 추가 로딩하고 싶다.
- 사용자는 각 FanTalk 글에 크리에이터가 남긴 답글을 같은 화면에서 확인하고 싶다.
- 사용자는 글쓴이 닉네임, ID, 프로필 이미지, 본문, 작성 시간을 목록 item에서 바로 확인하고 싶다.
- 앱 클라이언트는 page, size, hasNext를 이용해 추가 로딩 상태를 안정적으로 제어하고 싶다.
- 서버 개발자는 API DTO가 도메인 조회 계층으로 새어 들어가지 않는 패키지 의존 방향을 유지하고 싶다.
7. Core Features
Feature A. 크리에이터 채널 FanTalk 탭 조회 API
Requirements
- 신규 API는 크리에이터 채널 전용 V2 API로 작성한다.
- API endpoint는
GET /api/v2/creator-channels/{creatorId}/fan-talks로 한다. creatorId는 path variable로 받는다.- FanTalk 추가 로딩을 위해
page,sizequery parameter를 받는다. page는 기존 V2 탭 API와 동일하게 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계열 오류를 반환한다. - 조회자와 크리에이터 사이에 차단 관계가 있으면 기존 크리에이터 채널 접근 정책과 동일하게 접근 차단 오류를 반환한다.
- 조회 가능한 FanTalk가 없어도 전체 API는 성공 처리한다.
Edge Cases
page가 0보다 작거나size가 허용 범위를 벗어나도 400 오류를 반환하지 않고 실제 적용값으로 보정한다.- 요청한 page 범위에 FanTalk가 없으면
fanTalks는 빈 배열,hasNext는false로 내려주되fanTalkCount는 전체 개수를 유지한다. - 조회자 본인이 크리에이터인 경우에도 같은 응답 스키마를 사용한다.
Feature B. 응답 스키마
Requirements
- 응답 DTO는 구현 전에 명시하고 공개 API 계약으로 관리한다.
- 응답 최상위 DTO 이름은
CreatorChannelFanTalkTabResponse로 한다. - 응답에는 다음 값을 포함한다.
fanTalkCount: 조회자가 조회 가능한 전체 FanTalk 개수fanTalks: FanTalk 글 목록page: 현재 응답의 page indexsize: 현재 응답의 page sizehasNext: 다음 page 존재 여부
fanTalkCount는 최상위 FanTalk 글만 계산한다.fanTalkCount에는 현재 page에 포함되지 않은 FanTalk 글도 포함한다.page,size는 fallback 보정 이후 실제 적용된 값을 내려준다.hasNext는 같은 조건에서 다음 page에 노출할 FanTalk 글이 있으면true로 내려준다.- 응답 스키마 예시는 다음과 같다.
data class CreatorChannelFanTalkTabResponse(
val fanTalkCount: Int,
val fanTalks: List<CreatorChannelFanTalkResponse>,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
)
data class CreatorChannelFanTalkResponse(
val fanTalkId: Long,
val writerId: Long,
val writerNickname: String,
val writerProfileImageUrl: String,
val content: String,
val createdAtUtc: String,
val creatorReplies: List<CreatorChannelFanTalkReplyResponse>
)
data class CreatorChannelFanTalkReplyResponse(
val fanTalkId: Long,
val writerId: Long,
val writerNickname: String,
val writerProfileImageUrl: String,
val content: String,
val createdAtUtc: String
)
Edge Cases
- 조회 가능한 FanTalk가 없으면
fanTalkCount는0,fanTalks는 빈 배열,hasNext는false로 내려준다. - FanTalk 글에 크리에이터 답글이 없으면
creatorReplies는 빈 배열로 내려준다. - 작성자 프로필 이미지가 없으면 기존 V2 크리에이터 채널 API와 동일하게 기본 프로필 이미지 URL을 내려준다.
- 탈퇴 회원 닉네임 prefix 제거는 기존 legacy FanTalk 조회와 홈 FanTalk 요약 응답 정책을 따른다.
createdAtUtc는CreatorCheers.createdAt을 UTC 기준 ISO-8601 문자열로 내려준다.- Boolean 응답 필드는 현재 스키마에 없지만, 추후 추가 시 Jackson 직렬화 필드명을 명시해야 한다.
Feature C. FanTalk 목록과 개수
Requirements
- 조회 대상은 지정한
creatorId의 FanTalk로 제한한다. - 저장 엔티티는
CreatorCheers를 사용한다. - 최상위 FanTalk 글은
CreatorCheers.parent is null인 활성 데이터로 정의한다. - 활성 데이터는
CreatorCheers.isActive == true인 데이터로 정의한다. - 목록은 최상위 FanTalk 글만 페이징한다.
- 목록 정렬은 최신순을 기본으로 하며
createdAt desc,id desc를 따른다. - 전체 개수는 목록과 같은 creator, active, parent, 차단 필터 조건을 적용해 계산한다.
- 다음 page 존재 여부는
size + 1개를 조회하거나 동등한 방식으로 판단하되, 응답 목록에는 최대size개만 내려준다. - 글쓴이 ID는
CreatorCheers.member.id를 사용한다. - 글쓴이 닉네임은
CreatorCheers.member.nickname을 사용하고 기존 삭제 회원 prefix 제거 정책을 적용한다. - 글쓴이 프로필 이미지는
CreatorCheers.member.profileImage를 기존 CDN URL 조합 정책으로 변환한다. - 글쓴이가 쓴 글은
CreatorCheers.cheers를 사용한다. - 글 쓴 시간은
CreatorCheers.createdAt을 UTC 기준 ISO-8601 문자열로 변환한다. languageCode는 이번 FanTalk 탭 응답에 포함하지 않는다.
Edge Cases
CreatorCheers.createdAt이 nullable 기반 엔티티 필드에서 온 경우에도 조회 결과 응답에는 null이 나오지 않아야 한다.- FanTalk 작성자가 조회자와 차단 관계이면 해당 최상위 글은 목록과 개수에서 제외한다.
- 차단으로 제외된 최상위 글의 답글도 응답에 포함하지 않는다.
- 같은 작성자의 FanTalk가 여러 건 있어도 각각 별도 item으로 내려준다.
Feature D. 크리에이터 답글 포함
Requirements
- 각 FanTalk 글에는 크리에이터가 쓴 활성 답글 목록을
creatorReplies로 포함한다. - 답글은
CreatorCheers.parent가 해당 최상위 FanTalk 글인 데이터로 조회한다. - 답글 작성자가 조회 대상 크리에이터인 데이터만 포함한다.
- 답글도
CreatorCheers.isActive == true인 데이터만 포함한다. - 답글 item의 필드 구조는 최상위 FanTalk 글과 동일한 작성자 ID, 닉네임, 프로필 이미지, 본문, UTC 작성 시간을 사용한다.
- 답글 정렬은 오래된 답글부터 확인할 수 있도록
createdAt asc,id asc를 따른다. - 현재 팬끼리 답글을 작성할 수 없으므로 크리에이터가 아닌 회원의 답글은 정상 응답 대상이 아니다.
- 과거 데이터나 비정상 데이터로 크리에이터가 아닌 회원의 답글이 존재하더라도 응답에 포함하지 않는다.
Edge Cases
- 크리에이터 답글이 여러 개면 모두
creatorReplies에 포함한다. - 크리에이터가 작성했지만 비활성 처리된 답글은 포함하지 않는다.
- 답글 작성자인 크리에이터 프로필 이미지가 없으면 기본 프로필 이미지 URL을 내려준다.
- 답글 작성자인 크리에이터가 조회자와 차단 관계인 경우는 이미 채널 접근 차단 조건에서 처리된다.
Feature E. V2 재사용 범위와 계층 분리
Requirements
- 공개 API controller/facade/response DTO는
kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk하위에 둔다. - FanTalk 조회 service, 순수 정책, domain model, port, QueryDSL repository는
kr.co.vividnext.sodalive.v2.creator.channel.fantalk하위에 둔다. - 도메인 조회 계층은 API response DTO를 import하지 않는다.
- 도메인 조회 계층은 API facade나 controller를 import하지 않는다.
- 의존 방향은 항상
v2.api.creator.channel.fantalk -> v2.creator.channel.fantalk이다. - 페이징 값 보정과
offset,fetchLimit계산은 기존CreatorChannelPage패턴을 재사용한다. - 인증 회원 확인, creator role 검증, 채널 차단 접근 오류는 기존 V2 크리에이터 채널 탭 API와 같은 흐름을 따른다.
- 프로필 이미지 CDN URL 변환과 기본 프로필 이미지 URL은 기존 V2 크리에이터 채널 API 정책을 따른다.
- UTC ISO 변환은 기존
toUtcIso확장 함수 또는 같은 의미의 기존 V2 변환 방식을 재사용한다. - 기존 홈 API의 FanTalk 요약 조회 로직은 참고하되, 홈 도메인 repository에 신규 탭 페이징 책임을 추가하지 않는다.
- legacy
ExplorerQueryRepository.getCheersList의 timezone 기반 날짜 포맷 응답은 신규 V2 API에서 재사용하지 않는다.
Edge Cases
- 신규
fantalk도메인 패키지에서v2.api.*import 검색 결과가 0건이어야 한다. - 홈 API의
fanTalk.totalCount,fanTalk.latestFanTalk공개 응답 의미는 변경하지 않는다. - legacy FanTalk 작성/수정/삭제 기능은 기존 패키지에 남겨두고 이번 조회 계층 분리 대상에 포함하지 않는다.
8. Technical Constraints
- 빌드 도구는 Gradle Wrapper(
./gradlew)를 사용한다. - 언어/런타임은 Kotlin + Java 17을 따른다.
- 프레임워크는 Spring Boot 2.7.14를 따른다.
- 기존 Kotlin/Spring 스타일과 ktlint 규칙을 따른다.
- QueryDSL 조회는 기존 V2 크리에이터 채널 탭 repository 패턴을 따른다.
- 공개 API 스키마는 구현 중 임의 변경하지 않고, 변경이 필요하면 PRD와 구현 계획/TASK 문서를 먼저 갱신한다.
9. Decisions
- endpoint 이름은
GET /api/v2/creator-channels/{creatorId}/fan-talks로 확정한다. page는 기존 크리에이터 채널 V2 탭 API와 동일하게 0 기반 page index로 처리한다.