# 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, @SerializedName("onAirLives") val onAirLives: List, @SerializedName("recentChats") val recentChats: List, @SerializedName("monthlySchedules") val monthlySchedules: List, @SerializedName("recentNews") val recentNews: List ) @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`, 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 실제 이동 목적지는 아직 만들지 않는다. 이번 범위에서는 터치 액션 연결까지만 지정한다.