Files
sodalive-android/docs/20260625_메인_홈_팔로잉_탭/prd.md

375 lines
25 KiB
Markdown

# PRD: 메인 홈 팔로잉 탭
## 1. Overview
Figma `home_003` 팔로잉 탭 화면(`24:5682`)을 기준으로 메인 홈의 `팔로잉` 탭 본문을 구성하고, `GET /api/v2/home/following` 응답을 기존 v2 widget 중심으로 바인딩한다.
---
## 2. Problem
- `HomeMainFragment`는 이미 `추천`, `랭킹`, `팔로잉` 탭을 표시하지만, 팔로잉 탭 선택 시 본문 전환 로직이 아직 구현되어 있지 않다.
- 팔로잉 탭은 로그인 사용자에게 팔로잉 크리에이터, On Air, 최근 대화, 이달의 스케줄, 최근 소식을 한 화면에서 보여줘야 한다.
- 같은 API endpoint는 비로그인 조회도 허용하며, 비로그인 사용자는 실제 섹션 데이터 대신 로그인 유도 화면을 봐야 한다.
- Figma의 여러 요소는 기존 v2 widget과 유사하므로, 신규 UI를 만들기 전에 `v2` 패키지 하위의 재사용 가능 후보를 확인해야 한다.
- 서버 응답에는 `ChatRoomListItemResponse`, `CreatorActivityType`처럼 기존 앱 타입과 연결될 수 있는 항목이 포함되므로, 기존 모델/mapper와 중복되지 않게 설계해야 한다.
---
## 3. Goals
- 팔로잉 탭 선택 시 `GET /api/v2/home/following`을 호출하고 응답 상태에 따라 팔로잉 본문 또는 로그인 필요 상태로 분기한다.
- 비로그인 응답의 `isLoginRequired = true`를 앱 상태로 매핑하고, 팔로잉 탭 본문 섹션을 숨긴다.
- 로그인 응답의 `followingCreators`, `onAirLives`, `recentChats`, `monthlySchedules`, `recentNews`를 Figma 섹션 순서에 맞게 표시한다.
- `GET /api/v2/home/following`은 query parameter 없이 호출한다.
- 앱 DTO는 현재 Android 클라이언트 관례에 맞춰 Gson `@SerializedName` 기반으로 작성한다.
- 기존 `HomeMainFragment`, `TextTabBarView`, `view_section_title`, v2 widget/adapter 재사용을 우선 검토한다.
- 재사용이 어려운 영역만 팔로잉 탭 전용 adapter item 또는 view로 최소 구현한다.
- API DTO, UI state, mapper, empty/error/loading 정책, click routing은 후속 `plan-task.md`에서 검증 가능하도록 정리한다.
---
## 4. Non-Goals
- 이번 PRD 작성 단계에서는 코드, 리소스, 레이아웃 파일을 구현하지 않는다.
- Android 저장소에서 서버 `SecurityConfig`를 직접 수정하지 않는다. 단, 백엔드 전제 조건으로 `GET /api/v2/home/following``permitAll` 요구사항은 기록한다.
- 팔로잉/언팔로잉 액션, 전체 팔로잉 관리, 스케줄 생성/수정, 채팅방 생성 API는 이번 범위에 포함하지 않는다.
- API pagination, cursor, filter, sort query parameter는 추가하지 않는다.
- Figma에 없는 skeleton loading, shimmer, 임의 애니메이션, 추가 badge는 만들지 않는다.
- 기존 레거시 홈 화면 전체 리팩터링은 포함하지 않는다.
- Compose 화면으로 전환하지 않는다.
- 백엔드 응답 필드명, enum 이름, 정렬 기준은 앱에서 임의 변경하지 않는다.
---
## 5. Target Users
- 팔로우한 크리에이터의 라이브, 대화, 스케줄, 소식을 메인 홈에서 빠르게 확인하려는 로그인 사용자.
- 팔로잉 탭에 접근했지만 아직 로그인하지 않아 로그인 필요 안내를 받아야 하는 비로그인 사용자.
- 기존 XML View와 v2 widget을 재사용해 홈 팔로잉 화면을 구현/유지보수하는 Android 개발자.
---
## 6. User Stories
- 사용자는 홈의 `팔로잉` 탭에서 내가 팔로우한 크리에이터 목록을 가로로 탐색하고 싶다.
- 사용자는 팔로우 중인 크리에이터가 라이브 중이면 `On Air` 섹션에서 바로 확인하고 라이브로 이동하고 싶다.
- 사용자는 최근 대화한 채팅방을 팔로잉 탭에서 확인하고 대화방으로 진입하고 싶다.
- 사용자는 팔로우한 크리에이터의 이번 달 예정 콘텐츠와 현재 On Air 상태를 확인하고 싶다.
- 사용자는 최근 소식에서 랭킹, 커뮤니티, 오디오, 화보 콘텐츠 업데이트를 한 흐름으로 확인하고 싶다.
- 비로그인 사용자는 팔로잉 탭 접근 시 빈 화면이 아니라 로그인하면 볼 수 있다는 안내와 로그인 진입점을 기대한다.
- 개발자는 기존 v2 widget을 최대한 재사용해 화면별 UI 중복과 스타일 차이를 줄이고 싶다.
---
## 7. Core Features
### 팔로잉 홈 API 연동
팔로잉 탭은 단일 API 응답을 섹션별 UI model로 변환해 표시한다.
#### Endpoint Contract
- Method: `GET`
- Path: `/api/v2/home/following`
- Header: `Authorization: Bearer {accessToken}` optional
- Query parameter: 없음
#### Backend Preconditions
- 비로그인 조회를 허용한다.
- 서버 `SecurityConfig``GET /api/v2/home/following` `permitAll` 설정이 필요하다.
- 컨트롤러에서 `member == null`이면 `isLoginRequired = true`와 빈 섹션 배열을 담은 응답을 반환한다.
#### Android Response Contract
```kotlin
@Keep
data class HomeFollowingTabResponse(
@SerializedName("isLoginRequired") val isLoginRequired: Boolean,
@SerializedName("followingCreators") val followingCreators: List<FollowingCreatorResponse>,
@SerializedName("onAirLives") val onAirLives: List<FollowingLiveResponse>,
@SerializedName("recentChats") val recentChats: List<ChatRoomListItemResponse>,
@SerializedName("monthlySchedules") val monthlySchedules: List<FollowingScheduleResponse>,
@SerializedName("recentNews") val recentNews: List<FollowingNewsResponse>
)
@Keep
data class FollowingCreatorResponse(
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
)
@Keep
data class FollowingLiveResponse(
@SerializedName("liveId") val liveId: Long,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("title") val title: String,
@SerializedName("startedAtUtc") val startedAtUtc: String
)
@Keep
data class FollowingScheduleResponse(
@SerializedName("scheduleId") val scheduleId: String,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("title") val title: String,
@SerializedName("type") val type: CreatorActivityType,
@SerializedName("targetId") val targetId: Long,
@SerializedName("scheduledAtUtc") val scheduledAtUtc: String,
@SerializedName("isOnAir") val isOnAir: Boolean
)
@Keep
data class FollowingNewsResponse(
@SerializedName("newsId") val newsId: String,
@SerializedName("type") val type: FollowingNewsType,
@SerializedName("visibleFromAtUtc") val visibleFromAtUtc: String,
@SerializedName("creatorRanking") val creatorRanking: FollowingCreatorRankingNewsResponse?,
@SerializedName("audioContent") val audioContent: FollowingContentNewsResponse?,
@SerializedName("photoContent") val photoContent: FollowingContentNewsResponse?,
@SerializedName("contentRanking") val contentRanking: FollowingContentRankingNewsResponse?,
@SerializedName("communityPost") val communityPost: FollowingCommunityPostNewsResponse?
)
@Keep
data class FollowingCreatorRankingNewsResponse(
@SerializedName("rank") val rank: Int,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("nickname") val nickname: String,
@SerializedName("profileImageUrl") val profileImageUrl: String
)
@Keep
data class FollowingContentNewsResponse(
@SerializedName("contentId") val contentId: Long,
@SerializedName("contentImageUrl") val contentImageUrl: String?,
@SerializedName("title") val title: String,
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
@SerializedName("creatorNickname") val creatorNickname: String
)
@Keep
data class FollowingContentRankingNewsResponse(
@SerializedName("rank") val rank: Int,
@SerializedName("contentId") val contentId: Long,
@SerializedName("contentImageUrl") val contentImageUrl: String?,
@SerializedName("title") val title: String
)
@Keep
data class FollowingCommunityPostNewsResponse(
@SerializedName("postId") val postId: Long,
@SerializedName("creatorProfileImage") val creatorProfileImage: String,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("imageUrl") val imageUrl: String?,
@SerializedName("content") val content: String,
@SerializedName("createdAt") val createdAt: String,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int
)
enum class FollowingNewsType {
CREATOR_RANKING,
CONTENT_RANKING,
COMMUNITY_POST,
AUDIO_CONTENT,
PHOTO_CONTENT
}
```
#### Android DTO Requirements
- 위 응답 예시는 서버 응답 형태를 현재 Android 프로젝트의 DTO 관례로 옮긴 기준이다.
- DTO 작성 시 `androidx.annotation.Keep`, `com.google.gson.annotations.SerializedName`을 사용한다.
- DTO 파일은 `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/` 하위에 둔다.
- API 인터페이스는 기존 `HomeRecommendationApi`, `HomeCreatorRankingApi`처럼 v2 홈 전용 Retrofit API로 추가한다.
- Repository/ViewModel은 기존 `Api -> Repository -> ViewModel -> Fragment` 흐름을 따른다.
- `recentChats`는 기존 `ChatRoomListItemResponse`와 필드 계약이 같으면 해당 타입 재사용을 우선 검토한다.
- `CreatorActivityType`은 기존 `kr.co.vividnext.sodalive.v2.common.CreatorActivityType` 재사용을 우선 검토한다.
- `Authorization` header는 optional이어야 하므로, 토큰이 비어 있을 때 `"Bearer "` 문자열을 보내지 않는 방식을 구현 계획에서 확정한다.
- 응답 DTO를 화면에 직접 노출하지 않고 팔로잉 탭 전용 UI state/model로 변환한다.
#### State Requirements
- `isLoginRequired = true`이면 섹션 리스트 값과 무관하게 로그인 유도 상태로 처리한다.
- `isLoginRequired = false`이고 모든 섹션 리스트가 비어 있으면 로그인 유도 화면이 아니라 로그인 사용자용 empty 상태로 처리한다.
- API 실패 시 기존 v2 홈 화면의 error/toast/loading 패턴을 따른다.
- 탭 최초 선택 시 1회 로드하고, 후속 새로고침 정책은 구현 계획에서 기존 홈 탭 패턴을 기준으로 결정한다.
### 팔로잉 탭 UI 구성
Figma `24:5682` 기준 상단 title bar와 tab bar는 기존 홈 화면 구조를 유지하고, `팔로잉` 탭 선택 상태의 본문만 추가한다.
#### Requirements
- title bar는 기존 `view_title_bar_home`을 유지한다.
- tab bar는 기존 `TextTabBarView`를 사용한다.
- tab 항목은 `추천`, `랭킹`, `팔로잉` 순서로 유지한다.
- `팔로잉` tab 선택 시 추천 content와 랭킹 content는 숨기고 팔로잉 content를 표시한다.
- 세로 스크롤 시 title bar와 `TextTabBarView`는 화면에 유지하고, `TextTabBarView` 아래 팔로잉 content 영역만 스크롤되도록 구성한다.
- 섹션 타이틀은 기존 `view_section_title` include와 `ViewSectionTitleBinding` helper 패턴 재사용을 우선 검토한다.
- 각 섹션 리스트가 비어 있으면 해당 섹션은 숨기는 것을 기본 정책으로 한다.
### 로그인 유도 화면
비로그인 사용자는 팔로잉 섹션 대신 로그인 유도 화면을 본다.
#### Requirements
- `isLoginRequired = true`일 때 팔로잉 탭 본문 섹션 배열을 표시하지 않는다.
- 로그인 유도 화면의 정확한 디자인과 문구는 아직 정해지지 않았다.
- 구현 계획에서는 `isLoginRequired = true` 상태 분기와 팔로잉 섹션 숨김 처리까지만 확정하고, 로그인 유도 UI는 별도 확정 후 구현 가능한 영역으로 분리한다.
- 로그인 유도 화면이 확정되면 문구는 string resource로 관리하고 `values`, `values-en`, `values-ja` 반영을 검토한다.
### 팔로잉 크리에이터 섹션
`followingCreators`를 Figma 상단의 프로필 원형 리스트로 표시한다.
#### Requirements
- profile image와 creator nickname을 표시한다.
- item 터치 시 해당 크리에이터 채널로 이동한다.
- `followingCreators`가 1명 이상이면 가로 RecyclerView의 마지막 item으로 `전체` 버튼을 항상 표시한다.
- `전체` 버튼 item은 `width=wrap_content`, creator item 전체 높이와 동일한 높이, `paddingHorizontal=16dp`, `textColor=@color/soda_400`로 표시한다.
- `전체` 버튼 터치 시 기존 전체 팔로잉 크리에이터 목록 화면으로 이동한다.
- 리스트가 비어 있으면 섹션을 숨긴다.
- 이미지 URL이 비어 있거나 로드 실패하면 기존 홈 creator profile placeholder 정책을 따른다.
- 기존 `HomeCreatorProfileAdapter`, `HomeRecentActivityCreatorAdapter`, `HomeCreatorProfileImageLoader` 재사용 가능성을 우선 검토한다.
### On Air 섹션
`onAirLives`를 현재 라이브 중인 팔로잉 크리에이터 리스트로 표시한다.
#### Requirements
- live title, creator nickname, creator profile image, live start time을 표시한다.
- `startedAtUtc`는 기존 상대/경과 시간 formatter 재사용을 우선 검토한다.
- item 터치 시 해당 live room/detail로 이동한다.
- 리스트가 비어 있으면 섹션을 숨긴다.
- 기존 `LiveThumbnailSimpleView`, `LiveThumbnailDetailView`, `LiveThumbnailItem`, `HomeLiveAdapter` 재사용 가능성을 우선 검토한다.
- Figma의 On Air 카드가 기존 추천 홈의 간단 썸네일보다 상세형에 가까우므로, 기존 detail widget으로 부족한 항목만 팔로잉 전용 adapter에서 보완한다.
### 최근 대화 섹션
`recentChats`를 팔로잉 탭 본문 안의 최근 대화 카드로 표시한다.
#### Requirements
- `ChatRoomListItemResponse``roomId`, `targetName`, `targetImageUrl`, `lastMessage`, `lastMessageAt`, `chatType`을 사용한다.
- item 터치 시 기존 채팅방 진입 flow를 재사용한다.
- 리스트가 비어 있으면 섹션을 숨긴다.
- 기존 `ChatRoomListAdapter`, `ChatRoomListUiItem`, `ChatRoomMappers`, `item_v2_chat_room.xml` 재사용 가능성을 우선 검토한다.
- Figma 팔로잉 탭의 최근 대화 카드는 전체 채팅 탭 리스트보다 작은 가로 카드 형태이므로, 기존 adapter를 그대로 쓰기 어렵다면 mapper와 image/time formatter만 재사용하고 전용 item layout을 최소 추가한다.
### 이달의 스케줄 섹션
`monthlySchedules`를 월간 스케줄 리스트로 표시한다.
#### Requirements
- schedule date, creator nickname/profile, title, type label, scheduled time 또는 On Air 상태를 표시한다.
- `type``CreatorActivityType` 표시 문자열로 변환하고 string resource 기반 다국어 처리를 적용한다.
- `isOnAir = true`이면 Figma처럼 `On Air` 상태를 강조 표시한다.
- `isOnAir = false`이면 `scheduledAtUtc`를 사용자 지역 시간 기준 시간 텍스트로 표시한다.
- 서버는 `monthlySchedules`를 이번 달 범위로 고정 정렬해 내려주며, 앱은 별도 월 필터나 정렬을 수행하지 않는다.
- item 터치 시 `type``targetId`에 맞는 목적지로 이동한다.
- 리스트가 비어 있으면 섹션을 숨긴다.
- Figma의 캘린더/타임라인형 UI와 정확히 맞는 기존 v2 widget은 확인되지 않았으므로 팔로잉 전용 schedule item view를 추가하되, section title, profile image loader, typography/color token은 기존 리소스를 재사용한다.
### 최근 소식 섹션
`recentNews`를 타입별 feed 카드로 표시한다.
#### Requirements
- `CREATOR_RANKING``creatorRanking` payload로 creator ranking 소식을 표시하고 `rank`, `nickname`, `profileImageUrl`, `creatorId`를 사용한다.
- `CONTENT_RANKING``contentRanking` payload로 content ranking 소식을 표시하고 `rank`, `contentId`, `contentImageUrl`, `title`을 사용한다.
- `AUDIO_CONTENT``audioContent` payload로 오디오 콘텐츠 feed를 표시하고 `contentId`, `contentImageUrl`, `title`, `creatorProfileImageUrl`, `creatorNickname`을 사용한다.
- `PHOTO_CONTENT``photoContent` payload로 화보 콘텐츠 feed를 표시하고 `contentId`, `contentImageUrl`, `title`, `creatorProfileImageUrl`, `creatorNickname`을 사용한다.
- `COMMUNITY_POST``communityPost` payload로 커뮤니티 feed를 표시하고 `postId`, `creatorProfileImage`, `creatorNickname`, `imageUrl`, `content`, `createdAt`, `likeCount`, `commentCount`를 사용한다.
- `type`에 대응하는 payload가 null이면 해당 news item은 표시하지 않는다.
- 초기 운영 기간에는 `CREATOR_RANKING` 위주로 내려올 예정이다.
- `PHOTO_CONTENT`는 우선 `화보` label로 표시한다. 이 label은 이후 기획 변경 가능성이 있다.
- 시간 표시는 `visibleFromAtUtc`를 기준으로 디바이스 타임존으로 변환한 뒤 상대 시간으로 표시한다.
- `contentImageUrl` 또는 `imageUrl`이 null이면 타입별 기본 이미지 표시/숨김 정책을 구현 계획에서 확정한다.
- item 터치 시 `type`과 payload의 target id(`creatorId`, `contentId`, `postId`)에 맞는 목적지로 이동한다.
- 리스트가 비어 있으면 섹션을 숨긴다.
- 기존 `FeedAdapter`, `FeedItem.Rank`, `FeedItem.Community`, `FeedItem.Content`, `FeedRankView`, `FeedCommunityView`, `FeedContentView` 재사용 가능성을 우선 검토한다.
- `PHOTO_CONTENT`는 기존 `FeedItem.Content`의 category 확장 또는 팔로잉 전용 item 추가 중 더 작은 변경을 구현 계획에서 선택한다.
- 각 섹션의 “더보기” chevron은 터치 액션만 연결하고, 실제 이동 목적지는 아직 만들지 않는다.
#### 2026-06-30 Response 변경 Requirements
- 기존 최근 소식 공통 평면 필드(`creatorProfileImageUrl`, `creatorNickname`, `title`, `body`, `thumbnailImageUrl`, `targetId`, `occurredAtUtc`, `rank`)는 사용하지 않고, `type`별 nested payload를 사용한다.
- Android DTO, mapper, UI model, adapter binding, 테스트 fixture는 새 nested payload 구조를 기준으로 갱신한다.
- 새 응답 변경은 최근 소식 섹션에만 적용하며, 팔로잉 크리에이터, On Air, 최근 대화, 이달의 스케줄, 로그인 유도, API endpoint, optional authorization 정책은 변경하지 않는다.
#### 2026-07-12 COMMUNITY_POST 상세 이동 Requirements
- 최근 소식 섹션의 `COMMUNITY_POST` item 터치 시 `communityPost.postId`를 사용해 `CreatorChannelCommunityDetailActivity`로 이동한다.
- 이동은 기존 MainV2 로그인 가드(`ensureMainV2NavigationAllowed`)를 통과한 뒤 실행한다.
- `COMMUNITY_POST`가 아닌 최근 소식 item은 이번 작업 범위에서 기존처럼 이동하지 않는다.
- legacy `CreatorCommunityAllActivity``Constants.EXTRA_COMMUNITY_POST_ID` 직접 extra 조립 경로는 사용하지 않는다.
#### 2026-07-13 전체 empty 문구 Requirements
- `isLoginRequired = false`이고 `followingCreators`, `onAirLives`, `recentChats`, `monthlySchedules`, `recentNews`가 모두 비어 있는 `HomeFollowingUiState.Empty` 상태에서는 팔로잉 탭 본문 중앙에 empty 문구를 표시한다.
- empty 문구는 기존 `screen_home_following_empty` string resource를 재사용해 수정한다.
- 한국어 기본 문구는 `아직 팔로잉 소식이 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.`로 표시한다.
- 영어 문구는 `No following updates yet.\nFollow creators you are interested in.`로 표시한다.
- 일본어 문구는 `フォロー中のお知らせはまだありません。\n気になるクリエイターをフォローしてみましょう。`로 표시한다.
- empty 문구 `TextView`는 팔로잉 탭 content 영역 전체의 가운데에 배치하고, 텍스트도 가운데 정렬한다.
- `HomeFollowingUiState.Content`에서는 empty 문구를 숨기고 기존 섹션별 visibility 정책을 유지한다.
- `HomeFollowingUiState.LoginRequired``Error`는 이번 범위에서 empty 문구를 표시하지 않고 기존처럼 섹션을 숨긴다.
- API/DTO/mapper 계약과 팔로잉 크리에이터 `전체` 버튼 노출 정책은 변경하지 않는다.
#### 2026-06-30 후속 수정 Requirements
- `CREATOR_RANKING` 최근 소식 본문은 순위 숫자만 단독 표시하지 않고, Figma `24:5717` 기준으로 크리에이터 이름과 순위를 포함한 문장형 메시지를 표시한다.
- ranking 문장 안의 순위 텍스트는 기존 `FeedRankTextStyler` highlight range로 강조한다.
- `AUDIO_CONTENT` 최근 소식은 Figma `1229:27212` 기준으로 `88dp` 콘텐츠 이미지, `14dp` 카드 padding/gap, `20dp` 프로필 이미지, 프로필과 제목 사이 `6dp` 간격을 유지한다.
- 이번 수정은 팔로잉 탭 최근 소식 feed 표시 정합성 보완으로 한정하고 API 계약, 라우팅, 로그인 유도 정책은 변경하지 않는다.
### 재사용 가능한 v2 위젯 후보
PRD 작성 전 확인한 `v2` 패키지 하위 재사용 후보는 다음과 같다.
#### Common/Home Structure
- `HomeMainFragment`: 기존 홈 탭 컨테이너, title bar, `TextTabBarView`, 추천/랭킹 전환 구조.
- `TextTabBarView`: 홈 내부 `추천`, `랭킹`, `팔로잉` 탭 표시.
- `view_section_title` + `ViewSectionTitleBinding`: 섹션 타이틀과 우측 chevron 표시.
- `HomeCreatorProfileImageLoader`: 크리에이터 프로필 이미지 로딩/placeholder 정책 후보.
#### Creator/Profile
- `HomeCreatorProfileAdapter`: 원형 프로필 + 닉네임 리스트 후보.
- `HomeRecentActivityCreatorAdapter`: 프로필 원형 리스트와 label 표시 패턴 후보.
#### Live
- `LiveThumbnailSimpleView`, `LiveThumbnailDetailView`, `LiveThumbnailItem`: On Air 프로필/라이브 카드 후보.
- `HomeLiveAdapter`: horizontal live list adapter 패턴 후보.
#### Chat
- `ChatRoomListAdapter`: 채팅방 row binding, profile image circle crop, click routing 후보.
- `ChatRoomListUiItem`, `ChatRoomMappers`, `ChatRoomTimeTextFormatter`: 최근 대화 데이터 변환/시간 표시 후보.
#### Feed/News
- `FeedAdapter`: rank/live/content/community variant를 가진 feed list 후보.
- `FeedRankView`, `FeedContentView`, `FeedCommunityView`: 최근 소식 타입별 카드 후보.
- `FeedItem`, `FeedContentCategory`, `FeedRankHighlight`: 최근 소식 UI model 후보.
#### Content
- `AudioContentCardView`, `SeriesContentCardView`: 최근 소식 콘텐츠 썸네일 표시 보조 후보.
- `CreatorActivityType`: 스케줄 type 표시 변환 후보.
---
## 8. UX / UI Expectations
- 팔로잉 탭 첫 진입 시 기존 홈 탭과 동일한 검은 배경, spacing, typography, color token을 유지한다.
- 섹션 노출 순서는 Figma와 API 응답 순서에 맞춰 `팔로잉 크리에이터``On Air``최근 대화``이달의 스케줄``최근 소식` 순서를 기본으로 한다.
- 가로 리스트는 Figma처럼 좌우 padding과 item 간격을 유지하고, 세로 스크롤 중 레이아웃이 튀지 않아야 한다.
- 이미지 로딩 실패, 빈 URL, 긴 제목/닉네임은 기존 v2 widget의 말줄임/placeholder 정책을 우선 따른다.
- `isLoginRequired = true` 상태는 error나 empty가 아니라 명시적인 로그인 필요 상태로 다룬다.
- 사용자가 탭을 빠르게 전환해도 loading/dialog/toast가 중복되거나 이전 탭 content가 겹쳐 보이지 않아야 한다.
---
## 9. Technical Constraints
- Android XML View 기반으로 구현한다.
- 신규 파일과 연결 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다.
- 기존 레거시 코드는 직접 수정하지 않고, 필요한 경우 호출 또는 wrapper/adapter로 사용한다.
- 기존 `ApiResponse<T>`, RxJava3 `Single`, `BaseViewModel`, Koin DI 패턴을 따른다.
- 공개 API 스키마와 서버 필드명은 임의 변경하지 않는다.
- API에는 query parameter를 추가하지 않는다.
- optional authorization 처리에서 `BuildConfig` 값, token, URL을 로그/Toast/크래시 메시지에 노출하지 않는다.
- string resource는 `values`, `values-en`, `values-ja` 반영 필요 여부를 구현 계획에서 확인한다.
- 신규 mapper 또는 formatter 로직은 local unit test 검증을 우선한다.
---
## 10. Metrics
- 팔로잉 탭 노출 성공률: API success 후 적어도 하나의 섹션 또는 로그인 유도 화면이 정상 표시되는 비율.
- 로그인 필요 상태 노출 수: `isLoginRequired = true` 응답을 받은 팔로잉 탭 진입 수.
- 팔로잉 섹션 item click: creator, live, chat, schedule, news item별 클릭 이벤트.
- API 실패율과 empty 상태 비율.
- 팔로잉 탭 최초 렌더링 시간.
---
## 11. Open Questions
- 로그인 유도 화면의 정확한 디자인, 문구, CTA, 로그인 완료 후 복귀 정책은 아직 정해지지 않았다.
- `PHOTO_CONTENT``화보` label은 임시 정책이며 이후 기획 변경 가능성이 있다.
- 각 섹션의 “더보기” chevron 실제 이동 목적지는 아직 만들지 않는다. 이번 범위에서는 터치 액션 연결까지만 지정한다.