Files
sodalive-backend-spring-boot/docs/20260625_메인_홈_팔로잉_탭_API/prd.md

24 KiB

PRD: 메인 홈 팔로잉 탭 API

1. Overview

메인 홈의 내부 팔로잉 탭에서 사용할 팔로잉 크리에이터, 진행 중인 라이브, 최근 대화, 이달의 스케줄, 최근 소식을 한 번에 조회하는 v2 API를 제공한다.


2. Problem

  • 팔로잉 탭 화면은 로그인 사용자가 팔로우한 크리에이터 기준으로 여러 섹션을 조립해야 한다.
  • 기존 v2 홈 추천 API는 추천/랭킹 중심이며, 팔로잉 관계를 기준으로 섹션 전체를 구성하지 않는다.
  • 기존 채팅 목록 API, 크리에이터 채널 홈 API, 크리에이터 랭킹 스냅샷 패턴에는 재사용 가능한 코드가 있지만, 팔로잉 탭의 공개 응답 필드는 화면 요구사항과 다르다.
  • 최근 소식은 랭킹, 커뮤니티 게시글 업로드, 콘텐츠 업로드가 섞인 피드라 매 요청마다 팔로잉한 모든 크리에이터의 모든 원천 데이터를 크게 조인하면 응답 지연과 DB 부하가 커질 수 있다.
  • 최근 소식은 전체 후보를 매번 조회하는 모델보다, 팔로우 중인 크리에이터의 이벤트가 발생할 때 각 follower의 우체통에 소식 row를 넣는 사용자별 Inbox Feed 모델이 요구사항에 더 맞다.
  • 따라서 공개 API 조립 계층과 도메인 조회 계층을 분리하고, 최근 소식은 사용자별 inbox row를 최신순으로 읽는 구조가 필요하다.

3. Goals

  • 메인 홈 팔로잉 탭 조회 API를 kr.co.vividnext.sodalive.v2 하위 신규 코드로 제공한다.
  • 기존 패턴과 동일하게 API 조립 계층과 도메인 조회 계층을 분리한다.
  • 비로그인 사용자도 API 호출은 허용하되, 로그인 유도 화면을 그릴 수 있는 응답을 제공한다.
  • 사용자가 팔로우한 크리에이터 목록을 최신 팔로우순 20개 응답한다.
  • 사용자가 팔로우한 크리에이터의 현재 진행 중인 라이브를 최신순 10개 응답한다.
  • DM/AI 채팅방 중 최신 대화순 10개를 응답한다.
  • 사용자가 팔로우한 크리에이터들의 이번 달 오늘 이후 스케줄을 오늘과 가까운 순으로 최대 3개 응답한다.
  • 사용자가 팔로우한 크리에이터들의 최근 소식을 최신 노출 가능 시각순 최대 30개 응답한다.
  • 최근 소식은 팔로우 중인 크리에이터의 이벤트 발생 시점에 사용자별 inbox row를 생성하고, 조회 시 열람 가능 시각/활성 여부/차단/성인 노출 조건을 적용한다.
  • 새로 팔로우한 사용자는 과거 소식을 받지 않는다.
  • 언팔로우하면 해당 크리에이터가 보낸 기존 inbox row를 비활성화한다.
  • 재팔로우해도 기존에 비활성화된 inbox row는 복구하지 않고, 재팔로우 이후 새 이벤트부터 새 inbox row를 생성한다.
  • PRD에 API endpoint와 Response data class 초안을 포함한다.

4. Non-Goals

  • 기존 GET /api/v2/home/recommendations 공개 API 스키마를 변경하지 않는다.
  • 기존 GET /api/v2/chat/rooms 공개 API 스키마를 변경하지 않는다.
  • 기존 크리에이터 채널 홈/라이브/커뮤니티/콘텐츠 API 공개 스키마를 변경하지 않는다.
  • 팔로잉 추가/해제 공개 API 스키마 변경은 이번 범위에 포함하지 않는다.
  • 단, 최근 소식 정책을 위해 기존 팔로잉/언팔로잉 처리에 inbox 적재/비활성화 연동이 필요하면 내부 동작 보강 범위에 포함한다.
  • 채팅방 생성, 메시지 전송, 읽음 처리 정책 변경은 포함하지 않는다.
  • 최근 소식의 운영자 수동 고정/숨김 기능은 포함하지 않는다.
  • 최근 소식 발송용 외부 MQ, outbox table, 별도 worker, cursor/retry dashboard는 이번 범위에 포함하지 않는다.
  • 화보 업로드 기능 자체 구현은 포함하지 않는다. 단, 향후 콘텐츠 타입 확장을 고려한 응답 타입은 정의한다.
  • 전체보기/페이징 API는 이번 요구사항에 포함하지 않는다.

5. Target Users

  • 회원: 홈 팔로잉 탭에서 자신이 팔로우한 크리에이터의 활동을 빠르게 확인하는 사용자
  • 비회원: 홈 팔로잉 탭에 진입했을 때 로그인 필요 상태를 확인하고 로그인 화면으로 이동하는 사용자
  • 앱 클라이언트: 팔로잉 탭 첫 화면의 여러 섹션을 하나의 API 응답으로 구성하려는 클라이언트
  • 운영자: 최근 소식 inbox 적재와 노출 정책이 안정적으로 동작하기를 기대하는 내부 사용자

6. User Stories

  • 사용자는 내가 팔로우한 크리에이터 목록을 최근 팔로우한 순서로 보고 싶다.
  • 사용자는 팔로우한 크리에이터가 지금 진행 중인 라이브를 바로 확인하고 싶다.
  • 사용자는 최근 DM/AI 채팅방으로 빠르게 이동하고 싶다.
  • 사용자는 팔로우한 크리에이터의 이번 달 예정 라이브/콘텐츠 일정을 가까운 일정부터 보고 싶다.
  • 사용자는 팔로우한 크리에이터의 이번 주 랭킹 순위, 커뮤니티 게시글, 콘텐츠 업로드 소식을 최신순으로 보고 싶다.
  • 앱 클라이언트는 소식 item의 타입별 터치 액션을 명확한 target id로 처리하고 싶다.

7. Core Features

Feature A. 메인 홈 팔로잉 탭 통합 조회 API

Requirements

  • 신규 API endpoint는 GET /api/v2/home/following으로 정의한다.
  • 응답 wrapper는 기존 패턴과 동일하게 ApiResponse.ok(...)를 사용한다.
  • 비로그인 요청도 성공 응답으로 처리한다.
  • 비로그인 요청은 isLoginRequired = true와 빈 섹션 배열을 내려주고, 앱 클라이언트가 로그인 유도 화면을 표시한다.
  • 로그인 회원 요청은 isLoginRequired = false와 팔로잉 탭 데이터를 내려준다.
  • 인증 회원 조회는 기존 v2 컨트롤러 패턴과 동일하게 @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?를 사용한다.
  • 별도 query parameter는 정의하지 않는다.
  • API 조립 계층은 섹션별 도메인 조회 결과를 받아 공개 응답 DTO로 변환한다.
  • 한 섹션 데이터가 부족하면 가능한 개수만 내려주고 전체 API는 성공 처리한다.
  • 섹션별 데이터가 없으면 빈 배열을 내려준다.

Edge Cases

  • 비로그인 요청에서는 팔로잉 크리에이터, On Air, 최근 대화, 스케줄, 최근 소식을 모두 빈 배열로 내려준다.
  • 비로그인 요청에서는 팔로잉/채팅/스케줄/최근 소식 도메인 조회를 수행하지 않는다.
  • 사용자가 팔로우한 크리에이터가 없으면 팔로잉 크리에이터, On Air, 스케줄, 최근 소식은 빈 배열로 내려준다.
  • 최근 대화는 팔로잉 여부와 무관하게 해당 회원의 DM/AI 채팅 최신순 10개를 내려준다.
  • 조회 중 차단 관계가 있는 크리에이터의 라이브, 스케줄, 최근 소식은 노출하지 않는다.

Feature B. 팔로잉 크리에이터

Requirements

  • 사용자가 팔로우한 활성 크리에이터를 최신 팔로우순으로 최대 20개 조회한다.
  • 팔로잉 기준은 creator_following.member_id = 요청 회원 id, creator_following.is_active = true다.
  • 크리에이터는 member.role = CREATOR, member.is_active = true인 대상만 노출한다.
  • 응답 필드는 creatorId, creatorNickname, creatorProfileImageUrl을 포함한다.
  • 프로필 이미지는 v2.common.domain.CdnUrlExtensions.toCdnUrl(...) 패턴으로 CDN URL 변환한다.
  • 프로필 이미지가 없으면 기존 채팅/홈 추천과 동일한 기본 프로필 이미지 정책을 따른다.

Edge Cases

  • 팔로잉 row는 활성 상태지만 크리에이터가 비활성 상태이면 제외한다.
  • 차단 관계가 있는 크리에이터는 제외한다.

Feature C. On Air

Requirements

  • 사용자가 팔로우한 활성 크리에이터의 현재 진행 중인 라이브를 최신순으로 최대 10개 조회한다.
  • 현재 진행 중인 라이브는 기존 홈 추천 라이브와 동일하게 live_room.is_active = true, channel_name is not null, channel_name <> '' 조건을 기본으로 한다.
  • 정렬은 live_room.begin_date_time desc, live_room.id desc로 한다.
  • 응답 필드는 liveId, creatorProfileImageUrl, creatorNickname, title, startedAtUtc를 포함한다.
  • 19금 라이브 노출 여부는 기존 MemberContentPreferenceService.canViewAdultContent(member) 결과를 반영한다.
  • 성별 제한, 크리에이터 입장 제한처럼 기존 라이브 조회에서 필요한 접근 조건이 있으면 구현 계획 단계에서 기존 라이브/크리에이터 채널 라이브 조회 정책과 맞춘다.

Edge Cases

  • 라이브 제목이 비어 있으면 기존 라이브 조회 API의 제목 fallback 정책을 확인해 따른다.
  • 차단 관계가 있는 크리에이터의 라이브는 제외한다.

Feature D. 최근 대화

Requirements

  • DM/AI 채팅방 중 최신 대화순으로 최대 10개 조회한다.
  • 기존 ChatRoomListService.getRooms(member, filter = "ALL", cursor = null, limit = 10) 재사용을 우선한다.
  • 터치 시 해당 채팅방으로 이동할 수 있도록 roomIdchatType을 응답에 포함한다.
  • 기존 채팅 목록 응답 ChatRoomListItemResponse는 필드가 팔로잉 탭 요구와 맞으므로 직접 재사용한다.

Edge Cases

  • 채팅방이 없으면 빈 배열을 내려준다.
  • AI/DM 메시지 preview 규칙은 기존 ChatRoomListServicepreviewMessage() 정책을 그대로 따른다.

Feature E. 이달의 스케줄

Requirements

  • 사용자가 팔로우한 크리에이터들의 이번 달 스케줄을 최대 3개 조회한다.
  • 조회 범위는 KST 기준 오늘 00:00:00 이상, 다음 달 00:00:00 미만으로 한다.
  • 오늘 이전의 데이터는 노출하지 않는다.
  • 정렬은 scheduledAt asc, 같은 시각이면 기존 CreatorActivityType 정렬 정책과 target id 순으로 안정화한다.
  • 스케줄 원천은 기존 크리에이터 채널 홈 스케줄 정책을 팔로잉 전체로 확장한다.
    • 라이브 예약: live_room.begin_date_time
    • 오디오 콘텐츠 예약: content.release_date
  • 응답 필드는 scheduleId, creatorId, creatorProfileImageUrl, creatorNickname, title, type, targetId, scheduledAtUtc, isOnAir를 포함한다.
  • type은 기존 CreatorActivityType을 우선 재사용한다.
  • 화면의 On Air 표시를 위해 예약 라이브가 이미 진행 중이면 isOnAir = true로 내려준다.

Edge Cases

  • 오늘 이전 일정은 제외하되, 오늘 시작해서 현재 진행 중인 라이브는 스케줄에 포함할 수 있다.
  • 이번 달 남은 일정이 3개 미만이면 가능한 개수만 내려준다.
  • 19금 스케줄은 회원의 성인 콘텐츠 노출 가능 여부를 따른다.
  • 차단 관계가 있는 크리에이터의 스케줄은 제외한다.

Feature F. 최근 소식

Requirements

  • 사용자가 팔로우한 크리에이터들의 소식을 최신 노출 가능 시각순으로 최대 30개 조회한다.
  • 최근 소식은 사용자별 Inbox Feed로 저장한다.
  • 크리에이터 이벤트 발생 시점에 해당 크리에이터를 현재 팔로우 중인 회원별 inbox row를 생성한다.
  • 이번 범위에서는 별도 비동기 이벤트 발송 시스템을 도입하지 않는다.
  • 이벤트 발생 처리 흐름에서 내부 publish service를 호출해 follower 조회와 inbox bulk insert를 수행한다.
  • publish service는 콘텐츠/커뮤니티/랭킹 도메인 코드에 직접 흩어지지 않고, 향후 outbox/worker로 전환할 수 있는 단일 경계로 둔다.
  • follower가 많아져 동기 bulk insert가 운영 부하를 만들면 publish service 내부 구현을 outbox/worker 방식으로 교체할 수 있어야 한다.
  • 현재 구현은 H2/MySQL 공통 검증이 가능한 JPA portable path를 우선 사용한다. follower 수가 큰 크리에이터 이벤트에서 member_id in (...) 또는 saveAll 배치 크기가 운영 부하를 만들면, 후속 작업에서 follower id chunking, outbox table, 비동기 worker, 재시도/모니터링 대시보드로 전환한다.
  • 새로 팔로우한 사용자는 팔로우 이전에 발생한 과거 소식을 받지 않는다.
  • 언팔로우 시 해당 크리에이터가 보낸 기존 inbox row를 isActive = false로 비활성화한다.
  • 재팔로우 시 비활성화된 기존 inbox row는 복구하지 않는다.
  • 재팔로우 이후 새로 발생한 이벤트부터 새 inbox row를 생성한다.
  • 최근 소식 item 타입은 최소 아래를 지원한다.
    • CREATOR_RANKING: 크리에이터 순위 소식
    • CONTENT_RANKING: 향후 콘텐츠 순위 소식
    • COMMUNITY_POST: 커뮤니티 게시글 업로드
    • AUDIO_CONTENT: 오디오 콘텐츠 업로드
    • PHOTO_CONTENT: 향후 화보 콘텐츠 업로드
  • 이번 범위에서 CONTENT_RANKING은 생성하지 않는다.
  • PHOTO_CONTENT는 화보 기능 구현 전에는 생성되지 않지만, 클라이언트 계약 확장을 위해 enum에 포함한다.
  • 최근 소식은 매 요청마다 모든 팔로잉 크리에이터 원천 데이터를 직접 집계하지 않는다.
  • inbox row에는 소식 타입, 발생 시각, 열람 가능 시각, 수신 회원 id, 크리에이터 id, target id, 표시용 제목/본문/이미지 path, 랭킹 순위 값 등 응답 생성에 필요한 최소 정보를 저장한다.
  • API 조회는 memberId = 요청 회원 id, isActive = true, visibleFromAtUtc <= nowUtc인 inbox row를 최신순으로 조회한다.
  • 조회 정렬은 visibleFromAtUtc desc, newsId desc를 기본으로 한다.
  • 조회 시 원천 target의 비활성/삭제 여부, 차단 관계, 성인 노출 가능 여부를 최종 확인한다.
  • 응답 필드는 newsId, type, creatorProfileImageUrl, creatorNickname, title, body, thumbnailImageUrl, targetId, occurredAtUtc, visibleFromAtUtc, rank를 포함한다.
  • 응답에는 creatorId를 별도 필드로 내려주지 않는다.
  • CREATOR_RANKING 터치 액션은 해당 크리에이터 채널 이동이므로 targetId는 크리에이터 회원 id다.
  • CONTENT_RANKING 터치 액션은 향후 콘텐츠 상세 이동이므로 targetId는 콘텐츠 id로 정의한다.
  • COMMUNITY_POST 터치 액션은 게시글 상세 이동이므로 targetId는 커뮤니티 게시글 id다.
  • AUDIO_CONTENT 터치 액션은 오디오 상세 이동이므로 targetId는 오디오 콘텐츠 id다.
  • PHOTO_CONTENT 터치 액션은 향후 화보 상세 이동이므로 targetId는 화보 콘텐츠 id로 정의한다.
  • 화면의 상대 시간 표시는 visibleFromAtUtc 기준을 기본으로 한다.
  • 커뮤니티 게시글 업로드 소식의 occurredAtUtcvisibleFromAtUtc는 게시글 생성 시각을 기본값으로 한다.
  • 오디오 콘텐츠 업로드 소식의 occurredAtUtc는 콘텐츠 업로드 또는 공개 예약 생성 시각, visibleFromAtUtc는 콘텐츠 공개 시각을 기본값으로 한다.
  • 즉시 공개 콘텐츠는 visibleFromAtUtc = occurredAtUtc로 저장할 수 있다.
  • 크리에이터 랭킹 소식은 크리에이터 랭킹 스냅샷 생성 시 inbox row를 생성할 수 있으나, visibleFromAtUtc는 랭킹 스냅샷의 visibleFromAtUtc를 그대로 사용한다.
  • 크리에이터 랭킹 스냅샷이 월요일 01:00 KST에 생성되고 월요일 09:00 KST에 화면 반영되는 경우, CREATOR_RANKING inbox row도 월요일 09:00 KST 전에는 API에 노출되지 않아야 한다.
  • 최근 소식에서 순위 변화와 신규 진입 여부는 사용하지 않는다.
  • 랭킹 소식은 이번에 몇 위에 올랐는지를 나타내는 rank를 내려준다.
  • COMMUNITY_POST, AUDIO_CONTENT, PHOTO_CONTENTranknull로 내려준다.

Edge Cases

  • inbox row가 없거나 필터링 후 결과가 없으면 빈 배열을 내려준다.
  • inbox 적재 실패 시 API 조회에서 실시간 fallback 집계를 무조건 수행하지 않는다.
  • 랭킹 소식의 순위 값이 없거나 오래된 경우 해당 item은 생성하지 않는다.
  • 같은 회원, 같은 소식 타입, 같은 sourceKey에 대해 중복 inbox row를 생성하지 않는다.
  • 언팔로우와 inbox 적재가 동시에 발생하면, 최종적으로 언팔로우 상태인 크리에이터의 새 소식은 노출하지 않는다.
  • 콘텐츠 썸네일이 없으면 thumbnailImageUrlnull로 내려준다.

Feature G. Response 재사용 정책

Requirements

  • 공개 응답 DTO는 화면 계약이 명확해야 하므로 팔로잉 탭 전용 최상위 응답 HomeFollowingTabResponse를 신규로 만든다.
  • 기존 응답 DTO를 무조건 새로 만들지는 않는다.
  • recentChats는 기존 ChatRoomListItemResponse를 직접 재사용한다.
  • followingCreators는 기존 HomeCreatorItem과 필드 의미가 유사하지만 v2.api.home.dto.recommendation 패키지의 추천 탭 전용 DTO이므로, API 결합을 줄이기 위해 팔로잉 탭 전용 FollowingCreatorResponse를 만든다.
  • onAirLives는 기존 HomeLiveItem에 title/start time이 없고, CreatorChannelLiveResponse에는 creator profile/nickname이 없어 그대로 재사용하지 않는다.
  • monthlySchedules는 기존 CreatorChannelScheduleResponse에 creator 정보와 isOnAir가 없어 그대로 재사용하지 않는다.
  • recentNews는 타입별 target/action이 필요한 신규 피드이므로 전용 DTO를 만든다.
  • DTO를 새로 만들더라도 CDN URL 변환, UTC ISO 변환, 채팅 목록 조회, 성인 콘텐츠 노출 판단, 차단 관계 필터, 크리에이터 랭킹 스냅샷 visible 시각 정책은 기존 코드를 재사용한다.

Edge Cases

  • 기존 ChatRoomListItemResponse 변경이 팔로잉 탭 공개 스키마에도 영향을 줄 수 있으므로, 채팅 목록 API 변경 시 팔로잉 탭 회귀 테스트를 함께 수행한다.

8. API Endpoint

GET /api/v2/home/following
Authorization: Bearer {accessToken} (optional)
  • 비로그인 조회를 허용한다.
  • 별도 query parameter는 정의하지 않는다.
  • SecurityConfigGET /api/v2/home/following permitAll 설정을 추가한다.
  • 컨트롤러에서 member == null이면 isLoginRequired = true와 빈 섹션 배열을 담은 응답을 반환한다.
  • 앱 클라이언트는 isLoginRequired = true일 때 팔로잉 탭 본문 대신 로그인 유도 화면을 표시한다.

9. Response Data Class

data class HomeFollowingTabResponse(
    @JsonProperty("isLoginRequired")
    val isLoginRequired: Boolean,
    val followingCreators: List<FollowingCreatorResponse>,
    val onAirLives: List<FollowingLiveResponse>,
    val recentChats: List<ChatRoomListItemResponse>,
    val monthlySchedules: List<FollowingScheduleResponse>,
    val recentNews: List<FollowingNewsResponse>
)

data class FollowingCreatorResponse(
    val creatorId: Long,
    val creatorNickname: String,
    val creatorProfileImageUrl: String
)

data class FollowingLiveResponse(
    val liveId: Long,
    val creatorProfileImageUrl: String,
    val creatorNickname: String,
    val title: String,
    val startedAtUtc: String
)

data class FollowingScheduleResponse(
    val scheduleId: String,
    val creatorId: Long,
    val creatorProfileImageUrl: String,
    val creatorNickname: String,
    val title: String,
    val type: CreatorActivityType,
    val targetId: Long,
    val scheduledAtUtc: String,
    @JsonProperty("isOnAir")
    val isOnAir: Boolean
)

data class FollowingNewsResponse(
    val newsId: String,
    val type: FollowingNewsType,
    val creatorProfileImageUrl: String,
    val creatorNickname: String,
    val title: String,
    val body: String,
    val thumbnailImageUrl: String?,
    val targetId: Long,
    val occurredAtUtc: String,
    val visibleFromAtUtc: String,
    val rank: Int?
)

enum class FollowingNewsType {
    CREATOR_RANKING,
    CONTENT_RANKING,
    COMMUNITY_POST,
    AUDIO_CONTENT,
    PHOTO_CONTENT
}

  • ChatRoomListItemResponse는 기존 v2.chat.dto 응답 DTO를 직접 재사용한다.
  • scheduleIdnewsId는 서로 다른 원천 타입의 id 충돌을 피하기 위해 {TYPE}:{targetId} 형식의 문자열을 기본안으로 한다.

10. Technical Constraints

패키지 구조

  • 공개 API 조립 계층은 kr.co.vividnext.sodalive.v2.api.home.following 하위에 둔다.
    • Controller: ...adapter.in.web
    • Facade: ...application
    • Response DTO: ...dto
  • 도메인 조회 계층은 kr.co.vividnext.sodalive.v2.home.following 하위에 둔다.
    • Query service: ...application
    • 최근 소식 publish service: ...application
    • 도메인 모델/정책: ...domain
    • 조회 port: ...port.out
    • QueryDSL/JPA 구현: ...adapter.out.persistence
  • 의존 방향은 v2.api.home.following -> v2.home.following만 허용한다.

V2 공통화/재사용 대상

  • v2.chat.service.ChatRoomListService: 최근 대화 조회
  • v2.chat.dto.ChatRoomListItemResponse: 최근 대화 공개 응답 직접 재사용
  • v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepository.findSchedules(...): 스케줄 조회 조건 참고
  • v2.creator.channel.home.domain.CreatorChannelSchedule: 스케줄 도메인 의미 참고
  • v2.common.domain.CreatorActivityType: 스케줄/소식 타입 중 활동 타입 재사용
  • v2.common.domain.CdnUrlExtensions.toCdnUrl: 이미지 URL 변환
  • v2.api.home.dto.recommendation.toUtcIso: UTC ISO 문자열 변환 패턴
  • MemberContentPreferenceService.canViewAdultContent(...): 성인 콘텐츠 노출 가능 여부 판단
  • v2.ranking: 크리에이터 랭킹 스냅샷, visibleFromAtUtc, rank 의미 참고

최근 소식 Inbox

  • 신규 Entity와 DB table을 생성한다.
  • MySQL DDL은 docs/20260625_메인_홈_팔로잉_탭_API/create-home-following-news-inbox-table.sql에 기록한다.
  • inbox는 사용자별 소식 저장소다.
  • inbox table의 creator_id는 언팔로우 비활성화, 차단 관계 확인, 운영 조회를 위한 내부 컬럼이며 공개 응답의 별도 creatorId 필드로 내려주지 않는다.
  • 커뮤니티/콘텐츠 업로드 소식은 업로드 또는 공개 이벤트에서 현재 follower 회원별로 적재한다.
  • 크리에이터 랭킹 소식은 크리에이터 랭킹 스냅샷 생성 시점에 현재 follower 회원별로 적재하되, visibleFromAtUtc는 랭킹 스냅샷의 공개 시각을 사용한다.
  • 이번 구현은 외부 MQ, outbox table, 별도 worker 없이 내부 publish service에서 follower 조회와 inbox bulk insert를 수행하는 최소 구조로 한다.
  • 콘텐츠/커뮤니티/랭킹 생성 로직은 inbox 저장소를 직접 호출하지 않고 publish service만 호출한다.
  • publish service는 publishContentUploaded(...), publishCommunityPostCreated(...), publishCreatorRankingVisible(...)처럼 이벤트별 명시적 메서드를 제공한다.
  • 운영 규모가 커지면 publish service 내부에서 outbox row 저장 또는 비동기 worker 위임으로 전환할 수 있도록 호출부 계약을 작게 유지한다.
  • CREATOR_RANKING 타입은 크리에이터 랭킹 소식만 포함한다.
  • CONTENT_RANKING 타입은 향후 콘텐츠 랭킹 소식용으로 enum과 table 값만 예약하고, 이번 범위에서는 생성하지 않는다.
  • 언팔로우 시 해당 회원과 크리에이터의 활성 inbox row를 비활성화한다.
  • 재팔로우 시 비활성화된 기존 inbox row는 복구하지 않는다.
  • 현재 creator_following에는 재팔로우 시점이 명확히 남지 않으므로, 조회 조건으로 재팔로우 시점을 추론하지 않는다.
  • 조회 시 차단 관계, 성인 노출 여부, 원천 target 활성 여부는 최종 확인한다.
  • 중복 방지를 위해 memberId, newsType, sourceKey 기준의 유니크 정책을 필수로 둔다.
  • sourceKey{TYPE}:{targetId}:{periodKey}처럼 같은 소식을 안정적으로 식별할 수 있는 값으로 정의한다.
  • 언팔로우 비활성화와 사용자별 조회 성능을 위해 memberId, creatorId, isActive 축의 인덱스를 고려한다.
  • 최신 30개 조회 성능을 위해 memberId, isActive, visibleFromAtUtc 축의 인덱스를 고려한다.

11. Metrics

  • GET /api/v2/home/following 응답 시간
  • 섹션별 item count
  • 최근 소식 inbox 적재 성공/실패 횟수
  • 최근 소식 inbox 적재 지연 시간
  • 최근 소식 조회 시 필터링 후 노출 수
  • 빈 섹션 비율

12. Open Questions

  • 현재 PRD 기준의 미결정 요구사항은 없다.
  • 구현 계획 단계에서는 기존 라이브 조회 코드의 진행 중 판단 조건과 스케줄 isOnAir 판단 조건을 같은 조건으로 추출할지 검토한다.