31 KiB
31 KiB
PRD: 메인 홈 팔로잉 탭 API
1. Overview
메인 홈의 내부 팔로잉 탭에서 사용할 팔로잉 크리에이터, 진행 중인 라이브, 최근 대화, 이달의 스케줄, 최근 소식을 한 번에 조회하는 v2 API를 제공한다.
2. Problem
- 팔로잉 탭 화면은 로그인 사용자가 팔로우한 크리에이터 기준으로 여러 섹션을 조립해야 한다.
- 기존 v2 홈 추천 API는 추천/랭킹 중심이며, 팔로잉 관계를 기준으로 섹션 전체를 구성하지 않는다.
- 기존 채팅 목록 API, 크리에이터 채널 홈 API, 크리에이터 랭킹 스냅샷 패턴에는 재사용 가능한 코드가 있지만, 팔로잉 탭의 공개 응답 필드는 화면 요구사항과 다르다.
- 최근 소식은 랭킹, 커뮤니티 게시글 업로드, 콘텐츠 업로드가 섞인 피드라 매 요청마다 팔로잉한 모든 크리에이터의 모든 원천 데이터를 크게 조인하면 응답 지연과 DB 부하가 커질 수 있다.
- 최근 소식은 전체 후보를 매번 조회하는 모델보다, 팔로우 중인 크리에이터의 이벤트가 발생할 때 각 follower의 우체통에 소식 row를 넣는 사용자별 Inbox Feed 모델이 요구사항에 더 맞다.
- 따라서 공개 API 조립 계층과 도메인 조회 계층을 분리하고, 최근 소식은 사용자별 inbox row를 최신순으로 읽는 구조가 필요하다.
- 현재 최근 소식 조회는 노출 가능한 과거
CREATOR_RANKINGinbox도 함께 조회하므로, 전체 소식이 30개 미만이면 직전 공개 배치보다 오래된 크리에이터 순위가 표시될 수 있다. - 향후
CONTENT_RANKINGinbox가 적재되면 같은 콘텐츠가 여러 랭킹 소식에 포함될 수 있으므로, 조회 시 동일 콘텐츠의 중복 노출을 방지하는 정책이 필요하다.
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_RANKINGinbox 발행과 콘텐츠 랭킹 스냅샷 연동은 이번 보완 범위에 포함하지 않는다.- 전체보기/페이징 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)재사용을 우선한다. - 터치 시 해당 채팅방으로 이동할 수 있도록
roomId와chatType을 응답에 포함한다. - 기존 채팅 목록 응답
ChatRoomListItemResponse는 필드가 팔로잉 탭 요구와 맞으므로 직접 재사용한다.
Edge Cases
- 채팅방이 없으면 빈 배열을 내려준다.
- AI/DM 메시지 preview 규칙은 기존
ChatRoomListService의previewMessage()정책을 그대로 따른다.
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_job중rankingType = WEEKLY,status = DONE,visibleFromAtUtc <= nowUtc를 만족하는 최신 완료 job의visibleFromAtUtc와 같은 inbox row만 조회한다.- 적용 가능한 완료 job이 전혀 없는 legacy/backfill 데이터에서만
creator_ranking_snapshot의WEEKLY,visibleFromAtUtc <= nowUtc최신 공개 시각을 fallback으로 사용한다. - 최신 완료 job의 결과가 0건이면 과거 snapshot으로 보충하지 않는다.
- 다음 랭킹 배치가 공개되기 전에는 직전 공개 배치를 최신 배치로 유지하고, 새 배치가 공개된 뒤에는 이전 배치의
CREATOR_RANKINGinbox를 조회하지 않는다. - 사용자가 팔로우한 크리에이터가 최신 공개 배치에 없으면 해당 크리에이터의 과거
CREATOR_RANKINGinbox로 대체하지 않는다. CONTENT_RANKING은 같은 회원과 같은targetId를 콘텐츠 동일성 기준으로 사용하고, 노출 가능한 row 중visibleFromAtUtc desc,newsId desc기준 첫 항목 하나만 조회한다.- 랭킹 배치 필터와 콘텐츠 중복 제거는 전체 최대 30개 제한보다 먼저 적용한다.
- 조회 시 원천 target의 비활성/삭제 여부, 차단 관계, 성인 노출 가능 여부를 최종 확인한다.
FollowingNewsResponse최상위 응답 필드는newsId,type,visibleFromAtUtc만 공통으로 포함한다.- 타입별 세부 값은 nullable nested DTO로 내려주며,
type과 일치하는 nested DTO만 non-null이고 나머지는null이다. CREATOR_RANKING은creatorRanking에rank,creatorId,nickname,profileImageUrl을 포함한다.CONTENT_RANKING은contentRanking에rank,contentId,contentImageUrl,title을 포함한다.AUDIO_CONTENT는audioContent에contentId,contentImageUrl,title,creatorProfileImageUrl,creatorNickname을 포함한다. 콘텐츠 공개 시각은 최상위visibleFromAtUtc를 사용한다.PHOTO_CONTENT는photoContent에contentId,contentImageUrl,title,creatorProfileImageUrl,creatorNickname을 포함한다. 콘텐츠 공개 시각은 최상위visibleFromAtUtc를 사용한다.COMMUNITY_POST는communityPost에postId,creatorProfileImage,creatorNickname,imageUrl,content,createdAt,likeCount,commentCount를 포함한다.imageUrl은 nullable이고createdAttimezone은 UTC다.COMMUNITY_POST최근 소식은 무료 커뮤니티 게시글만 발행한다. 유료 커뮤니티 게시글은 팔로잉 최근 소식 inbox row를 생성하지 않는다.- 타입별 터치 액션 target은 각 nested DTO의 id를 사용한다.
creatorRanking.creatorId,contentRanking.contentId,audioContent.contentId,photoContent.contentId,communityPost.postId가 이동 대상 id다. - 화면의 상대 시간 표시는 최상위
visibleFromAtUtc기준을 기본으로 한다. - 커뮤니티 게시글 업로드 소식의
visibleFromAtUtc와communityPost.createdAt은 게시글 생성 시각을 기본값으로 한다. - 오디오/화보 콘텐츠 업로드 소식의
visibleFromAtUtc는 콘텐츠 공개 시각을 기본값으로 한다. - 즉시 공개 콘텐츠는
visibleFromAtUtc = releaseDate로 저장할 수 있다. - 크리에이터 랭킹 소식은 크리에이터 랭킹 스냅샷 생성 시 inbox row를 생성할 수 있으나,
visibleFromAtUtc는 랭킹 스냅샷의visibleFromAtUtc를 그대로 사용한다. - 크리에이터 랭킹 스냅샷이 월요일 01:00 KST에 생성되고 월요일 09:00 KST에 화면 반영되는 경우,
CREATOR_RANKINGinbox row도 월요일 09:00 KST 전에는 API에 노출되지 않아야 한다. - 월요일 신규 랭킹 공개 전에는 직전 공개 배치의
CREATOR_RANKING소식을 표시하고, 신규 배치 공개 시점부터는 신규 배치 소식만 표시한다. - 최근 소식에서 순위 변화와 신규 진입 여부는 사용하지 않는다. 랭킹 타입은 nested DTO의
rank만 내려준다.
Edge Cases
- inbox row가 없거나 필터링 후 결과가 없으면 빈 배열을 내려준다.
- inbox 적재 실패 시 API 조회에서 실시간 fallback 집계를 무조건 수행하지 않는다.
- 랭킹 소식의 순위 값이 없거나 오래된 경우 해당 item은 생성하지 않는다.
- 최신 공개 크리에이터 랭킹 배치에 해당하는 inbox가 없으면 과거 배치로 보충하지 않고
CREATOR_RANKING소식을 가능한 개수만 응답한다. - 같은 콘텐츠의
CONTENT_RANKINGrow가 여러 개이고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는 정의하지 않는다.
SecurityConfig에GET /api/v2/home/followingpermitAll 설정을 추가한다.- 컨트롤러에서
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}형식의 문자열을 사용한다.newsId는home_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
- Controller:
- 도메인 조회 계층은
kr.co.vividnext.sodalive.v2.home.following하위에 둔다.- Query service:
...application - 최근 소식 publish service:
...application - 도메인 모델/정책:
...domain - 조회 port:
...port.out - QueryDSL/JPA 구현:
...adapter.out.persistence
- Query service:
- 의존 방향은
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_job의WEEKLY,DONE,visibleFromAtUtc <= nowUtc조건으로 우선 판정한다. 적용 가능한 완료 job이 전혀 없는 legacy/backfill 데이터에서만creator_ranking_snapshot의 최신 공개 시각을 fallback으로 사용하며, 최신 완료 job의 결과가 0건이면 과거 snapshot으로 보충하지 않는다.CONTENT_RANKING타입은 향후 콘텐츠 랭킹 소식용으로 enum과 table 값을 유지하되, 이번 보완에서는 발행 기능을 추가하지 않고 기존 또는 향후 적재된 inbox의 조회 정책만 정의한다.CONTENT_RANKING은targetId별 최신 노출 가능 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기준 최신 항목 하나만 응답한다.