docs(home): 팔로잉 최근 소식 계약을 갱신한다
This commit is contained in:
@@ -175,22 +175,22 @@
|
||||
- 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` 기준을 기본으로 한다.
|
||||
- 커뮤니티 게시글 업로드 소식의 `occurredAtUtc`와 `visibleFromAtUtc`는 게시글 생성 시각을 기본값으로 한다.
|
||||
- 오디오 콘텐츠 업로드 소식의 `occurredAtUtc`는 콘텐츠 업로드 또는 공개 예약 생성 시각, `visibleFromAtUtc`는 콘텐츠 공개 시각을 기본값으로 한다.
|
||||
- 즉시 공개 콘텐츠는 `visibleFromAtUtc = occurredAtUtc`로 저장할 수 있다.
|
||||
- `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이고 `createdAt` timezone은 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_RANKING` inbox row도 월요일 09:00 KST 전에는 API에 노출되지 않아야 한다.
|
||||
- 최근 소식에서 순위 변화와 신규 진입 여부는 사용하지 않는다.
|
||||
- 랭킹 소식은 이번에 몇 위에 올랐는지를 나타내는 `rank`를 내려준다.
|
||||
- `COMMUNITY_POST`, `AUDIO_CONTENT`, `PHOTO_CONTENT`의 `rank`는 `null`로 내려준다.
|
||||
- 최근 소식에서 순위 변화와 신규 진입 여부는 사용하지 않는다. 랭킹 타입은 nested DTO의 `rank`만 내려준다.
|
||||
|
||||
#### Edge Cases
|
||||
- inbox row가 없거나 필터링 후 결과가 없으면 빈 배열을 내려준다.
|
||||
@@ -198,7 +198,8 @@
|
||||
- 랭킹 소식의 순위 값이 없거나 오래된 경우 해당 item은 생성하지 않는다.
|
||||
- 같은 회원, 같은 소식 타입, 같은 `sourceKey`에 대해 중복 inbox row를 생성하지 않는다.
|
||||
- 언팔로우와 inbox 적재가 동시에 발생하면, 최종적으로 언팔로우 상태인 크리에이터의 새 소식은 노출하지 않는다.
|
||||
- 콘텐츠 썸네일이 없으면 `thumbnailImageUrl`은 `null`로 내려준다.
|
||||
- 타입별 이미지가 없으면 해당 nested DTO의 이미지 URL 필드는 `null`로 내려준다.
|
||||
- 무료 커뮤니티 게시글이 생성되더라도 조회 시 원천 게시글이 비활성화되었거나 성인/차단 정책에 의해 제외되면 `COMMUNITY_POST` 최근 소식은 노출하지 않는다.
|
||||
|
||||
### Feature G. Response 재사용 정책
|
||||
|
||||
@@ -275,15 +276,45 @@ data class FollowingScheduleResponse(
|
||||
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?
|
||||
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 {
|
||||
@@ -297,7 +328,7 @@ enum class FollowingNewsType {
|
||||
```
|
||||
|
||||
- `ChatRoomListItemResponse`는 기존 `v2.chat.dto` 응답 DTO를 직접 재사용한다.
|
||||
- `scheduleId`와 `newsId`는 서로 다른 원천 타입의 id 충돌을 피하기 위해 `{TYPE}:{targetId}` 형식의 문자열을 기본안으로 한다.
|
||||
- `scheduleId`와 `newsId`는 서로 다른 원천 타입의 id 충돌을 피하기 위해 `{TYPE}:{targetId}` 형식의 문자열을 기본안으로 한다. 최근 소식의 이동 대상 id는 타입별 nested DTO 안의 id 필드를 사용한다.
|
||||
|
||||
---
|
||||
|
||||
@@ -332,11 +363,11 @@ enum class FollowingNewsType {
|
||||
- MySQL DDL은 `docs/20260625_메인_홈_팔로잉_탭_API/create-home-following-news-inbox-table.sql`에 기록한다.
|
||||
- inbox는 사용자별 소식 저장소다.
|
||||
- inbox table의 `creator_id`는 언팔로우 비활성화, 차단 관계 확인, 운영 조회를 위한 내부 컬럼이며 공개 응답의 별도 `creatorId` 필드로 내려주지 않는다.
|
||||
- 커뮤니티/콘텐츠 업로드 소식은 업로드 또는 공개 이벤트에서 현재 follower 회원별로 적재한다.
|
||||
- 콘텐츠 업로드 소식은 업로드 또는 공개 이벤트에서 현재 follower 회원별로 적재한다. 커뮤니티 업로드 소식은 무료 커뮤니티 게시글 생성 이벤트에서만 현재 follower 회원별로 적재한다.
|
||||
- 크리에이터 랭킹 소식은 크리에이터 랭킹 스냅샷 생성 시점에 현재 follower 회원별로 적재하되, `visibleFromAtUtc`는 랭킹 스냅샷의 공개 시각을 사용한다.
|
||||
- 이번 구현은 외부 MQ, outbox table, 별도 worker 없이 내부 publish service에서 follower 조회와 inbox bulk insert를 수행하는 최소 구조로 한다.
|
||||
- 콘텐츠/커뮤니티/랭킹 생성 로직은 inbox 저장소를 직접 호출하지 않고 publish service만 호출한다.
|
||||
- publish service는 `publishContentUploaded(...)`, `publishCommunityPostCreated(...)`, `publishCreatorRankingVisible(...)`처럼 이벤트별 명시적 메서드를 제공한다.
|
||||
- publish service는 `publishContentUploaded(...)`, `publishFreeCommunityPostCreated(...)`, `publishCreatorRankingVisible(...)`처럼 이벤트별 명시적 메서드를 제공한다. 유료 커뮤니티 게시글은 publish service 호출 대상이 아니다.
|
||||
- 운영 규모가 커지면 publish service 내부에서 outbox row 저장 또는 비동기 worker 위임으로 전환할 수 있도록 호출부 계약을 작게 유지한다.
|
||||
- `CREATOR_RANKING` 타입은 크리에이터 랭킹 소식만 포함한다.
|
||||
- `CONTENT_RANKING` 타입은 향후 콘텐츠 랭킹 소식용으로 enum과 table 값만 예약하고, 이번 범위에서는 생성하지 않는다.
|
||||
|
||||
Reference in New Issue
Block a user