Files

31 KiB

PRD: 메인 홈 팔로잉 탭 API

1. Overview

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


2. Problem

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

3. Goals

  • 메인 홈 팔로잉 탭 조회 API를 kr.co.vividnext.sodalive.v2 하위 신규 코드로 제공한다.
  • 기존 패턴과 동일하게 API 조립 계층과 도메인 조회 계층을 분리한다.
  • 비로그인 사용자도 API 호출은 허용하되, 로그인 유도 화면을 그릴 수 있는 응답을 제공한다.
  • 사용자가 팔로우한 크리에이터 목록을 오래된 팔로우순 20개 응답한다.
  • 사용자가 팔로우한 크리에이터의 현재 진행 중인 라이브를 최신순 10개 응답한다.
  • DM/AI 채팅방 중 최신 대화순 10개를 응답한다.
  • 사용자가 팔로우한 크리에이터들의 이번 달 오늘 이후 스케줄을 오늘과 가까운 순으로 최대 3개 응답한다.
  • 사용자가 팔로우한 크리에이터들의 최근 소식을 최신 노출 가능 시각순 최대 30개 응답한다.
  • CREATOR_RANKING 최근 소식은 현재 시점에 공개된 최신 크리에이터 랭킹 배치만 응답한다.
  • CONTENT_RANKING 최근 소식은 같은 콘텐츠의 노출 가능한 inbox가 여러 개여도 가장 최신 항목 하나만 응답한다.
  • 최근 소식은 팔로우 중인 크리에이터의 이벤트 발생 시점에 사용자별 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는 이번 범위에 포함하지 않는다.
  • 화보 업로드 기능 자체 구현은 포함하지 않는다. 단, 향후 콘텐츠 타입 확장을 고려한 응답 타입은 정의한다.
  • CONTENT_RANKING inbox 발행과 콘텐츠 랭킹 스냅샷 연동은 이번 보완 범위에 포함하지 않는다.
  • 전체보기/페이징 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를 기본으로 한다.
  • CREATOR_RANKING은 적용 가능한 creator_ranking_snapshot_jobrankingType = WEEKLY, status = DONE, visibleFromAtUtc <= nowUtc를 만족하는 최신 완료 job의 visibleFromAtUtc와 같은 inbox row만 조회한다.
  • 적용 가능한 완료 job이 전혀 없는 legacy/backfill 데이터에서만 creator_ranking_snapshotWEEKLY, visibleFromAtUtc <= nowUtc 최신 공개 시각을 fallback으로 사용한다.
  • 최신 완료 job의 결과가 0건이면 과거 snapshot으로 보충하지 않는다.
  • 다음 랭킹 배치가 공개되기 전에는 직전 공개 배치를 최신 배치로 유지하고, 새 배치가 공개된 뒤에는 이전 배치의 CREATOR_RANKING inbox를 조회하지 않는다.
  • 사용자가 팔로우한 크리에이터가 최신 공개 배치에 없으면 해당 크리에이터의 과거 CREATOR_RANKING inbox로 대체하지 않는다.
  • CONTENT_RANKING은 같은 회원과 같은 targetId를 콘텐츠 동일성 기준으로 사용하고, 노출 가능한 row 중 visibleFromAtUtc desc, newsId desc 기준 첫 항목 하나만 조회한다.
  • 랭킹 배치 필터와 콘텐츠 중복 제거는 전체 최대 30개 제한보다 먼저 적용한다.
  • 조회 시 원천 target의 비활성/삭제 여부, 차단 관계, 성인 노출 가능 여부를 최종 확인한다.
  • FollowingNewsResponse 최상위 응답 필드는 newsId, type, visibleFromAtUtc만 공통으로 포함한다.
  • 타입별 세부 값은 nullable nested DTO로 내려주며, type과 일치하는 nested DTO만 non-null이고 나머지는 null이다.
  • CREATOR_RANKINGcreatorRankingrank, creatorId, nickname, profileImageUrl을 포함한다.
  • CONTENT_RANKINGcontentRankingrank, contentId, contentImageUrl, title을 포함한다.
  • AUDIO_CONTENTaudioContentcontentId, contentImageUrl, title, creatorProfileImageUrl, creatorNickname을 포함한다. 콘텐츠 공개 시각은 최상위 visibleFromAtUtc를 사용한다.
  • PHOTO_CONTENTphotoContentcontentId, contentImageUrl, title, creatorProfileImageUrl, creatorNickname을 포함한다. 콘텐츠 공개 시각은 최상위 visibleFromAtUtc를 사용한다.
  • COMMUNITY_POSTcommunityPostpostId, creatorProfileImage, creatorNickname, imageUrl, content, createdAt, likeCount, commentCount를 포함한다. imageUrl은 nullable이고 createdAt timezone은 UTC다.
  • COMMUNITY_POST 최근 소식은 무료 커뮤니티 게시글만 발행한다. 유료 커뮤니티 게시글은 팔로잉 최근 소식 inbox row를 생성하지 않는다.
  • 타입별 터치 액션 target은 각 nested DTO의 id를 사용한다. creatorRanking.creatorId, contentRanking.contentId, audioContent.contentId, photoContent.contentId, communityPost.postId가 이동 대상 id다.
  • 화면의 상대 시간 표시는 최상위 visibleFromAtUtc 기준을 기본으로 한다.
  • 커뮤니티 게시글 업로드 소식의 visibleFromAtUtccommunityPost.createdAt은 게시글 생성 시각을 기본값으로 한다.
  • 오디오/화보 콘텐츠 업로드 소식의 visibleFromAtUtc는 콘텐츠 공개 시각을 기본값으로 한다.
  • 즉시 공개 콘텐츠는 visibleFromAtUtc = releaseDate로 저장할 수 있다.
  • 크리에이터 랭킹 소식은 크리에이터 랭킹 스냅샷 생성 시 inbox row를 생성할 수 있으나, visibleFromAtUtc는 랭킹 스냅샷의 visibleFromAtUtc를 그대로 사용한다.
  • 크리에이터 랭킹 스냅샷이 월요일 01:00 KST에 생성되고 월요일 09:00 KST에 화면 반영되는 경우, CREATOR_RANKING inbox row도 월요일 09:00 KST 전에는 API에 노출되지 않아야 한다.
  • 월요일 신규 랭킹 공개 전에는 직전 공개 배치의 CREATOR_RANKING 소식을 표시하고, 신규 배치 공개 시점부터는 신규 배치 소식만 표시한다.
  • 최근 소식에서 순위 변화와 신규 진입 여부는 사용하지 않는다. 랭킹 타입은 nested DTO의 rank만 내려준다.

Edge Cases

  • inbox row가 없거나 필터링 후 결과가 없으면 빈 배열을 내려준다.
  • inbox 적재 실패 시 API 조회에서 실시간 fallback 집계를 무조건 수행하지 않는다.
  • 랭킹 소식의 순위 값이 없거나 오래된 경우 해당 item은 생성하지 않는다.
  • 최신 공개 크리에이터 랭킹 배치에 해당하는 inbox가 없으면 과거 배치로 보충하지 않고 CREATOR_RANKING 소식을 가능한 개수만 응답한다.
  • 같은 콘텐츠의 CONTENT_RANKING row가 여러 개이고 visibleFromAtUtc가 같으면 newsId가 큰 row 하나만 응답한다.
  • 과거 크리에이터 랭킹과 중복 콘텐츠 랭킹을 제외한 뒤 최근 소식이 30개 미만이어도 과거·중복 랭킹으로 보충하지 않는다.
  • 같은 회원, 같은 소식 타입, 같은 sourceKey에 대해 중복 inbox row를 생성하지 않는다.
  • 언팔로우와 inbox 적재가 동시에 발생하면, 최종적으로 언팔로우 상태인 크리에이터의 새 소식은 노출하지 않는다.
  • 타입별 이미지가 없으면 해당 nested DTO의 이미지 URL 필드는 null로 내려준다.
  • 무료 커뮤니티 게시글이 생성되더라도 조회 시 원천 게시글이 비활성화되었거나 성인/차단 정책에 의해 제외되면 COMMUNITY_POST 최근 소식은 노출하지 않는다.

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 visibleFromAtUtc: String,
    val creatorRanking: FollowingCreatorRankingNewsResponse?,
    val audioContent: FollowingContentNewsResponse?,
    val photoContent: FollowingContentNewsResponse?,
    val contentRanking: FollowingContentRankingNewsResponse?,
    val communityPost: FollowingCommunityPostNewsResponse?
)

data class FollowingCreatorRankingNewsResponse(
    val rank: Int,
    val creatorId: Long,
    val nickname: String,
    val profileImageUrl: String
)

data class FollowingContentNewsResponse(
    val contentId: Long,
    val contentImageUrl: String?,
    val title: String,
    val creatorProfileImageUrl: String,
    val creatorNickname: String
)

data class FollowingContentRankingNewsResponse(
    val rank: Int,
    val contentId: Long,
    val contentImageUrl: String?,
    val title: String
)

data class FollowingCommunityPostNewsResponse(
    val postId: Long,
    val creatorProfileImage: String,
    val creatorNickname: String,
    val imageUrl: String?,
    val content: String,
    val createdAt: String,
    val likeCount: Int,
    val commentCount: Int
)

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

  • ChatRoomListItemResponse는 기존 v2.chat.dto 응답 DTO를 직접 재사용한다.
  • scheduleId는 서로 다른 원천 타입의 id 충돌을 피하기 위해 {TYPE}:{targetId} 형식의 문자열을 사용한다.
  • newsIdhome_following_news_inbox.id의 10진 문자열이며, 최근 소식의 visibleFromAtUtc desc, newsId desc 정렬과 CONTENT_RANKING 동률 해소에 사용한다. 최근 소식의 이동 대상 id는 타입별 nested DTO 안의 id 필드를 사용한다.

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 회원별로 적재한다.
  • 크리에이터 랭킹 소식은 크리에이터 랭킹 스냅샷 생성 시점에 현재 follower 회원별로 적재하되, visibleFromAtUtc는 랭킹 스냅샷의 공개 시각을 사용한다.
  • 이번 구현은 외부 MQ, outbox table, 별도 worker 없이 내부 publish service에서 follower 조회와 inbox bulk insert를 수행하는 최소 구조로 한다.
  • 콘텐츠/커뮤니티/랭킹 생성 로직은 inbox 저장소를 직접 호출하지 않고 publish service만 호출한다.
  • publish service는 publishContentUploaded(...), publishFreeCommunityPostCreated(...), publishCreatorRankingVisible(...)처럼 이벤트별 명시적 메서드를 제공한다. 유료 커뮤니티 게시글은 publish service 호출 대상이 아니다.
  • 운영 규모가 커지면 publish service 내부에서 outbox row 저장 또는 비동기 worker 위임으로 전환할 수 있도록 호출부 계약을 작게 유지한다.
  • CREATOR_RANKING 타입은 크리에이터 랭킹 소식만 포함한다.
  • CREATOR_RANKING 조회의 최신 공개 배치는 적용 가능한 creator_ranking_snapshot_jobWEEKLY, DONE, visibleFromAtUtc <= nowUtc 조건으로 우선 판정한다. 적용 가능한 완료 job이 전혀 없는 legacy/backfill 데이터에서만 creator_ranking_snapshot의 최신 공개 시각을 fallback으로 사용하며, 최신 완료 job의 결과가 0건이면 과거 snapshot으로 보충하지 않는다.
  • CONTENT_RANKING 타입은 향후 콘텐츠 랭킹 소식용으로 enum과 table 값을 유지하되, 이번 보완에서는 발행 기능을 추가하지 않고 기존 또는 향후 적재된 inbox의 조회 정책만 정의한다.
  • CONTENT_RANKINGtargetId별 최신 노출 가능 row 하나만 남기고, visibleFromAtUtc가 같으면 newsId 내림차순으로 하나를 선택한다.
  • 언팔로우 시 해당 회원과 크리에이터의 활성 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 판단 조건을 같은 조건으로 추출할지 검토한다.

13. Decision Log

  • 2026-07-30: CREATOR_RANKING 최근 소식은 KST 달력 주간이 아니라 현재 시점의 최신 공개 WEEKLY, DONE 크리에이터 랭킹 job을 기준으로 한다. 신규 배치 공개 전에는 직전 공개 배치를 유지하고, 공개 후에는 이전 배치를 노출하지 않으며, 최신 완료 job의 결과가 0건이면 과거 snapshot으로 보충하지 않는다.
  • 2026-07-30: CONTENT_RANKING 발행 기능은 이번 보완 범위에서 제외한다. 조회 시 동일 contentId의 노출 가능한 row 중 visibleFromAtUtc desc, newsId desc 기준 최신 항목 하나만 응답한다.