Files

14 KiB

PRD: 크리에이터 채널 FanTalk 탭 API

1. Overview

크리에이터 채널의 FanTalk 탭에서 전체 FanTalk 개수와 FanTalk 글 목록을 페이징 조회하는 API를 제공한다.


2. Problem

  • 크리에이터 채널 홈 API는 FanTalk 전체 개수와 최신 FanTalk 1건만 요약으로 제공한다.
  • FanTalk 탭은 전체 개수, 페이징된 글 목록, 각 글에 달린 크리에이터 답글을 함께 표시해야 한다.
  • legacy /profile/{id}/cheers API는 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}/cheers API의 공개 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, size query 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는 빈 배열, hasNextfalse로 내려주되 fanTalkCount는 전체 개수를 유지한다.
  • 조회자 본인이 크리에이터인 경우에도 같은 응답 스키마를 사용한다.

Feature B. 응답 스키마

Requirements

  • 응답 DTO는 구현 전에 명시하고 공개 API 계약으로 관리한다.
  • 응답 최상위 DTO 이름은 CreatorChannelFanTalkTabResponse로 한다.
  • 응답에는 다음 값을 포함한다.
    • fanTalkCount: 조회자가 조회 가능한 전체 FanTalk 개수
    • fanTalks: FanTalk 글 목록
    • page: 현재 응답의 page index
    • size: 현재 응답의 page size
    • hasNext: 다음 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가 없으면 fanTalkCount0, fanTalks는 빈 배열, hasNextfalse로 내려준다.
  • FanTalk 글에 크리에이터 답글이 없으면 creatorReplies는 빈 배열로 내려준다.
  • 작성자 프로필 이미지가 없으면 기존 V2 크리에이터 채널 API와 동일하게 기본 프로필 이미지 URL을 내려준다.
  • 탈퇴 회원 닉네임 prefix 제거는 기존 legacy FanTalk 조회와 홈 FanTalk 요약 응답 정책을 따른다.
  • createdAtUtcCreatorCheers.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로 처리한다.