From e331a7e072a3cefbc9398239b04166b9ba06407f Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 25 Jun 2026 22:21:33 +0900 Subject: [PATCH] =?UTF-8?q?docs(home):=20=ED=8C=94=EB=A1=9C=EC=9E=89=20?= =?UTF-8?q?=ED=83=AD=20=EC=9A=94=EA=B5=AC=EC=99=80=20=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=EC=9D=84=20=EA=B8=B0=EB=A1=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/20260625_메인_홈_팔로잉_탭/plan-task.md | 436 +++++++++++++++++++ docs/20260625_메인_홈_팔로잉_탭/prd.md | 308 +++++++++++++ 2 files changed, 744 insertions(+) create mode 100644 docs/20260625_메인_홈_팔로잉_탭/plan-task.md create mode 100644 docs/20260625_메인_홈_팔로잉_탭/prd.md diff --git a/docs/20260625_메인_홈_팔로잉_탭/plan-task.md b/docs/20260625_메인_홈_팔로잉_탭/plan-task.md new file mode 100644 index 00000000..58ddebcb --- /dev/null +++ b/docs/20260625_메인_홈_팔로잉_탭/plan-task.md @@ -0,0 +1,436 @@ +# 메인 홈 팔로잉 탭 구현 계획/TASK + +> **For agentic workers:** REQUIRED SUB-SKILL: 구현 시 `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`를 사용해 task 단위로 진행한다. 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다. + +**Goal:** `GET /api/v2/home/following` 응답을 기반으로 메인 홈 `팔로잉` 탭에 팔로잉 크리에이터, On Air, 최근 대화, 이달의 스케줄, 최근 소식을 표시한다. + +**Architecture:** 기존 `HomeMainFragment`의 title bar, `TextTabBarView`, 추천/랭킹 탭 구조는 유지하고, `팔로잉` 선택 시 전용 content surface를 노출한다. 신규 API/Repository/DTO/UI state/mapper/ViewModel/adapter는 `kr.co.vividnext.sodalive.v2.main.home` 하위에 두며, `ChatRoomListItemResponse`, `CreatorActivityType`, `formatUtcRelativeTimeText`, 기존 feed/live/profile widget을 우선 재사용한다. 로그인 유도 화면은 아직 디자인/문구가 정해지지 않았으므로 이번 구현 계획에서는 `isLoginRequired` 상태 분기와 팔로잉 섹션 숨김까지만 고정한다. + +**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test. + +--- + +## 전제와 성공 기준 +- PRD: `docs/20260625_메인_홈_팔로잉_탭/prd.md` +- Figma: `home_003` 팔로잉 탭 `24:5682` +- API endpoint는 `GET /api/v2/home/following`이다. +- `Authorization` header는 optional이며, token이 blank이면 header를 보내지 않는다. +- query parameter는 보내지 않는다. +- `isLoginRequired = true`이면 팔로잉 섹션을 표시하지 않는다. +- 로그인 유도 화면의 실제 UI, 문구, CTA, 로그인 완료 후 복귀 정책은 별도 확정 후 구현한다. +- `recentNews` 시간은 `visibleFromAtUtc`를 디바이스 타임존 기준으로 상대 시간 표시한다. +- `PHOTO_CONTENT` label은 우선 `화보`로 표시한다. +- ranking news의 `rank`가 null이면 해당 news item은 표시하지 않는다. +- `monthlySchedules`는 서버가 이번 달 범위로 정렬해서 내려주며 앱은 재정렬하지 않는다. +- 섹션 title chevron은 터치 콜백까지만 연결하고 실제 이동 목적지는 만들지 않는다. +- 구현 완료 후 최소 다음 명령을 실행한다. + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"` + - `./gradlew :app:mergeDebugResources` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + - `git diff --check` + +--- + +## Figma 참조 필요 Phase +- Phase 1: 제한 참조 + - 기존 홈 탭 구조, v2 위젯, DI/API 패턴 확인 중심으로 진행한다. +- Phase 2: Figma 참조 불필요 + - API/DTO/Repository와 mapper 상태는 PRD 서버 계약과 기존 v2 data layer 패턴을 따른다. +- Phase 3: Figma 참조 불필요 + - ViewModel 상태, optional auth header, `isLoginRequired` 분기는 단위 테스트 중심으로 검증한다. +- Phase 4: 필수 참조 + - 팔로잉 크리에이터, On Air, 최근 대화, 이달의 스케줄, 최근 소식 섹션 배치와 spacing은 Figma `24:5682`를 기준으로 확인한다. +- Phase 5: 필수 참조 + - 최종 수동 화면 검증은 PRD의 포함/제외 항목과 실제 화면을 대조한다. + +--- + +## 파일 구조 +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingApi.kt` + - `GET /api/v2/home/following` Retrofit endpoint를 정의한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt` + - `HomeFollowingTabResponse`, `FollowingCreatorResponse`, `FollowingLiveResponse`, `FollowingScheduleResponse`, `FollowingNewsResponse`, `FollowingNewsType` DTO를 정의한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingRepository.kt` + - API 호출을 repository method로 감싼다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingUiState.kt` + - `Loading`, `LoginRequired`, `Content`, `Empty`, `Error` 상태를 정의한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingUiModels.kt` + - 팔로잉 크리에이터, live, chat, schedule, news section/item UI model을 정의한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingMappers.kt` + - DTO를 UI model/state로 변환하고, `rank == null` news 숨김, `PHOTO_CONTENT` label, `visibleFromAtUtc` 시간 기준을 적용한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingAuthHeader.kt` + - blank token이면 `null`, 값이 있으면 `Bearer {token}`을 반환한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingViewModel.kt` + - 팔로잉 탭 API 호출, loading/error/login-required/content 상태를 관리한다. +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - `HomeFollowingApi`, `HomeFollowingRepository`, `HomeFollowingViewModel`을 Koin에 등록한다. +- Modify: `app/src/main/res/layout/fragment_v2_main_home.xml` + - 팔로잉 탭 content surface와 섹션 RecyclerView들을 추가한다. +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - `HOME_TAB_FOLLOWING` 분기, ViewModel observer, adapter binding, section visibility, chevron click callback을 연결한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt` + - `followingCreators` horizontal profile list를 바인딩한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt` + - `onAirLives` horizontal live card list를 바인딩한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingChatAdapter.kt` + - `recentChats` horizontal chat card list를 바인딩한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingScheduleAdapter.kt` + - `monthlySchedules` vertical schedule list를 바인딩한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt` + - `recentNews` vertical news list를 바인딩한다. +- Create: `app/src/main/res/layout/item_home_following_creator.xml` + - 팔로잉 크리에이터 profile item이다. +- Create: `app/src/main/res/layout/item_home_following_live.xml` + - On Air live item이다. +- Create: `app/src/main/res/layout/item_home_following_chat.xml` + - 최근 대화 compact card item이다. +- Create: `app/src/main/res/layout/item_home_following_schedule.xml` + - 이달의 스케줄 item이다. +- Create: `app/src/main/res/layout/item_home_following_news_rank.xml` + - ranking news item이다. +- Create: `app/src/main/res/layout/item_home_following_news_content.xml` + - audio/photo content news item이다. +- Modify: `app/src/main/res/values/strings.xml` + - 팔로잉 섹션 title, `On Air`, `화보`, empty/error label을 추가한다. +- Modify: `app/src/main/res/values-en/strings.xml` + - 팔로잉 섹션 title, `On Air`, photo label, empty/error label을 추가한다. +- Modify: `app/src/main/res/values-ja/strings.xml` + - 팔로잉 섹션 title, `On Air`, photo label, empty/error label을 추가한다. +- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingAuthHeaderTest.kt` + - optional auth header 생성 규칙을 검증한다. +- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingMapperTest.kt` + - DTO to UI mapping, login-required, rank null filtering, news label/time 기준을 검증한다. +- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingViewModelTest.kt` + - API success/error/login-required/loading 상태 전환을 검증한다. +- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt` + - layout id, adapter/ViewModel 연결, `HOME_TAB_FOLLOWING` 분기, chevron click callback 연결을 source-level로 검증한다. + +--- + +### Phase 1: 기존 구조 확인과 작업 경계 고정 + +- [x] **Task 1.1: 홈 탭 삽입 지점 확인** + - 확인: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - `app/src/main/res/layout/fragment_v2_main_home.xml` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeRecommendationViewModel.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeCreatorRankingViewModel.kt` + - 작업: + - 기존 `추천`, `랭킹`, `팔로잉` Text Tab bar는 유지한다. + - `showHomeTab(HOME_TAB_FOLLOWING)` 분기에서 팔로잉 surface를 표시할 위치를 확인한다. + - 추천/랭킹 API와 ViewModel은 리팩터링하지 않는다. + - 검증: + - Run: `rg -n "HOME_TAB_FOLLOWING|showHomeTab|nsvHomeRecommendationContent|rvHomeCreatorRankings|textTabBarHome" app/src/main/java/kr/co/vividnext/sodalive/v2/main/home app/src/main/res/layout/fragment_v2_main_home.xml` + - Expected: 팔로잉 탭 분기와 기존 추천/랭킹 surface visibility 제어 지점이 확인된다. + - Result: PASS. `HomeMainFragment.kt`에서 `showHomeTab`, `HOME_TAB_FOLLOWING`, 추천/랭킹 visibility 제어 지점을 확인했다. + +- [x] **Task 1.2: 재사용 위젯과 신규 adapter 경계 확정** + - 확인: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCreatorProfileImageLoader.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailDetailView.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomMappers.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/common/CreatorActivityType.kt` + - 작업: + - 프로필 이미지 로딩은 `HomeCreatorProfileImageLoader` 또는 같은 placeholder 정책 재사용으로 고정한다. + - 채팅 데이터 변환은 `ChatRoomListItemResponse.toUiItem()` 재사용으로 고정한다. + - 스케줄 type label은 `CreatorActivityType.labelResId` 재사용으로 고정한다. + - Figma와 크기가 맞지 않는 카드들은 팔로잉 전용 adapter/layout 신규 생성으로 고정한다. + - 검증: + - Run: `rg -n "HomeCreatorProfileImageLoader|class LiveThumbnailDetailView|sealed class FeedItem|fun ChatRoomListItemResponse.toUiItem|enum class CreatorActivityType" app/src/main/java/kr/co/vividnext/sodalive/v2` + - Expected: 재사용 후보 클래스와 함수가 확인된다. + - Result: PASS. `LiveThumbnailDetailView`, `FeedItem`, `ChatRoomListItemResponse.toUiItem()`, `CreatorActivityType` 재사용 후보를 확인했다. + +- [x] **Task 1.3: 제외 범위 확인** + - 확인: + - `docs/20260625_메인_홈_팔로잉_탭/prd.md` + - 제외: + - 로그인 유도 화면 실제 디자인/문구/CTA 구현 + - 더보기 chevron 목적지 이동 + - 팔로잉/언팔로잉 액션 + - 스케줄 월 필터와 앱 내 재정렬 + - 레거시 홈 화면 직접 수정 + - 검증: + - Run: `rg -n "Non-Goals|로그인 유도|더보기|monthlySchedules|rank|PHOTO_CONTENT|Open Questions" docs/20260625_메인_홈_팔로잉_탭/prd.md` + - Expected: 제외 범위와 확정 정책이 확인된다. + - Result: PASS. 로그인 유도 UI 미확정, 더보기 목적지 제외, `monthlySchedules` 정렬 정책, `rank == null` 제외, `PHOTO_CONTENT` label 정책을 확인했다. + +--- + +### Phase 2: API, DTO, Repository, mapper 추가 + +- [x] **Task 2.1: optional auth header 테스트 작성** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingAuthHeaderTest.kt` + - 테스트 케이스: + - blank token은 `null`을 반환한다. + - non-blank token은 `Bearer {token}`을 반환한다. + - 앞뒤 공백이 있는 token은 trim 후 `Bearer {token}`을 반환한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingAuthHeaderTest"` + - Expected: helper 구현 전 RED 실패. + - Result: RED 확인. `homeFollowingAuthHeader` 미구현으로 `compileDebugUnitTestKotlin` unresolved reference 실패가 발생했다. + +- [x] **Task 2.2: optional auth header helper 구현** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingAuthHeader.kt` + - 작업: + - `fun homeFollowingAuthHeader(token: String): String?`를 추가한다. + - `token.trim().takeIf { it.isNotEmpty() }?.let { "Bearer $it" }` 규칙을 적용한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingAuthHeaderTest"` + - Expected: PASS. + - Result: PASS. blank/whitespace token은 `null`, non-blank token은 trim 후 `Bearer {token}`으로 검증됐다. + +- [x] **Task 2.3: API/DTO/Repository 계약 추가** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingApi.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingRepository.kt` + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - 작업: + - Retrofit endpoint는 `@GET("/api/v2/home/following")`로 정의한다. + - `@Header("Authorization") authHeader: String?`를 사용한다. + - query parameter는 정의하지 않는다. + - DTO는 PRD의 Android Response Contract 필드를 모두 포함하고 `@Keep`, `@SerializedName`을 사용한다. + - `recentChats`는 기존 `ChatRoomListItemResponse`를 사용한다. + - `FollowingScheduleResponse.type`은 기존 `CreatorActivityType`을 사용한다. + - Koin `networkModule`, `repositoryModule`에 신규 API/Repository를 등록한다. + - 검증: + - Run: `./gradlew :app:compileDebugKotlin` + - Expected: 신규 data layer와 DI 등록이 컴파일된다. + - Result: PASS. `HomeFollowingApi`, DTO, Repository, API/Repository DI 등록이 `compileDebugKotlin`에서 컴파일됐다. + +- [x] **Task 2.4: string resource 추가** + - 수정: + - `app/src/main/res/values/strings.xml` + - `app/src/main/res/values-en/strings.xml` + - `app/src/main/res/values-ja/strings.xml` + - 작업: + - 섹션 title 문자열을 추가한다. + - `screen_home_following_creators_title` + - `screen_home_following_on_air_title` + - `screen_home_following_recent_chats_title` + - `screen_home_following_monthly_schedules_title` + - `screen_home_following_recent_news_title` + - news/category 문자열을 추가한다. + - `screen_home_following_on_air` + - `screen_home_following_photo_content` + - empty/error 문자열을 추가한다. + - `screen_home_following_empty` + - `screen_home_following_error` + - 검증: + - Run: `./gradlew :app:mergeDebugResources` + - Expected: 3개 locale string resource가 중복 없이 merge된다. + - Result: PASS. `values`, `values-en`, `values-ja` string resource merge가 통과했다. + +- [x] **Task 2.5: mapper RED 테스트 작성** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingMapperTest.kt` + - 테스트 케이스: + - `isLoginRequired = true` 응답은 `HomeFollowingUiState.LoginRequired`로 매핑된다. + - `isLoginRequired = false`이고 모든 섹션이 비면 `HomeFollowingUiState.Empty`로 매핑된다. + - `followingCreators`, `onAirLives`, `recentChats`, `monthlySchedules`, `recentNews`가 section UI model로 매핑된다. + - `recentChats`는 `ChatRoomListItemResponse.toUiItem()` 결과가 null인 항목을 제외한다. + - `monthlySchedules`는 서버 응답 순서를 유지한다. + - `PHOTO_CONTENT`는 `screen_home_following_photo_content` label로 매핑된다. + - `CREATOR_RANKING`, `CONTENT_RANKING`의 `rank == null` 항목은 제외된다. + - news 상대 시간은 `visibleFromAtUtc` 값을 formatter에 전달한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest"` + - Expected: UI model/mapper 구현 전 RED 실패. + - Result: RED 확인. DTO/UI model/mapper/string resource 미구현으로 `compileDebugUnitTestKotlin` unresolved reference 실패가 발생했다. + +- [x] **Task 2.6: UI state/model과 mapper 구현** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingUiState.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingUiModels.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingMappers.kt` + - 작업: + - `HomeFollowingUiState`는 `Loading`, `LoginRequired`, `Content`, `Empty`, `Error`를 정의한다. + - `Content`에는 `followingCreators`, `onAirLives`, `recentChats`, `monthlySchedules`, `recentNews` section을 둔다. + - `Content.isEmpty` helper를 추가해 모든 section item이 비었는지 판정한다. + - mapper는 `UtcRelativeTimeTextFormatter`를 받아 `visibleFromAtUtc` 상대 시간을 생성한다. + - ranking news의 `rank == null`은 map 단계에서 제외한다. + - `PHOTO_CONTENT`는 photo label string resource id를 매핑한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest"` + - Expected: PASS. + - Result: PASS. login-required, empty, content mapping, invalid chat 제외, schedule 순서 유지, `PHOTO_CONTENT` label, null rank filtering, `visibleFromAtUtc` formatter 전달이 검증됐다. + +--- + +### Phase 3: ViewModel 상태와 API 호출 연결 + +- [x] **Task 3.1: ViewModel RED 테스트 작성** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingViewModelTest.kt` + - 테스트 케이스: + - `loadFollowing()`은 loading 후 content 상태를 발행한다. + - blank token이면 repository에 null auth header를 전달한다. + - token이 있으면 repository에 `Bearer {token}` auth header를 전달한다. + - `isLoginRequired = true` 응답은 login-required 상태를 발행한다. + - API success이지만 data가 null이면 error 상태와 toast를 발행한다. + - API failure throwable이면 error 상태와 toast를 발행한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingViewModelTest"` + - Expected: ViewModel 구현 전 RED 실패. + - Result: RED 확인. `HomeFollowingViewModel` 미구현으로 `compileDebugUnitTestKotlin` unresolved reference 실패가 발생했다. + +- [x] **Task 3.2: HomeFollowingViewModel 구현** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingViewModel.kt` + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - 작업: + - `HomeFollowingViewModel(repository, relativeTimeTextFormatter)`를 추가한다. + - `SharedPreferenceManager.token`을 `homeFollowingAuthHeader()`로 변환해 repository에 전달한다. + - RxJava3 `subscribeOn(Schedulers.io())`, `observeOn(AndroidSchedulers.mainThread())` 패턴을 따른다. + - success data는 mapper로 UI state 변환한다. + - error는 `HomeFollowingUiState.Error`와 기존 unknown error toast 패턴을 따른다. + - Koin `viewModelModule`에 `HomeFollowingViewModel(get(), get())`를 등록한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingViewModelTest"` + - Expected: PASS. + - Result: PASS. loading/content, optional auth header, login-required, null data error/toast, throwable error/toast 상태 전환이 검증됐다. + +- [x] **Task 3.3: data/model/ViewModel 통합 컴파일 확인** + - 확인: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingApi.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingMappers.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingViewModel.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - 검증: + - Run: `./gradlew :app:compileDebugKotlin` + - Expected: 팔로잉 data/model/ViewModel/DI 코드가 컴파일된다. + - Result: PASS. 팔로잉 data/model/ViewModel/DI 코드가 `compileDebugKotlin`에서 컴파일됐다. + - 검증 기록: + - 2026-06-25 코드 리뷰: Phase 1~3 범위의 API/DTO/Repository/mapper/ViewModel/DI/string/test 변경을 검토했으며, 현재 코드 기준으로 blocking finding은 발견하지 못했다. + - 2026-06-25 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 모두 PASS. + +--- + +### Phase 4: 팔로잉 탭 UI surface와 adapter 연결 + +- [ ] **Task 4.1: Fragment source RED 테스트 작성** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt` + - 테스트 케이스: + - `fragment_v2_main_home.xml`에 `nsv_home_following_content`가 있다. + - layout에 `rv_home_following_creators`, `rv_home_following_on_air_lives`, `rv_home_following_recent_chats`, `rv_home_following_monthly_schedules`, `rv_home_following_recent_news`가 있다. + - `HomeMainFragment`가 `HomeFollowingViewModel`을 주입한다. + - `HOME_TAB_FOLLOWING` 분기에서 팔로잉 surface를 visible 처리한다. + - section title chevron click listener가 연결되어 있고 실제 route 호출은 없다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"` + - Expected: layout/fragment 수정 전 RED 실패. + +- [ ] **Task 4.2: 팔로잉 content layout 추가** + - 수정: + - `app/src/main/res/layout/fragment_v2_main_home.xml` + - 생성: + - `app/src/main/res/layout/item_home_following_creator.xml` + - `app/src/main/res/layout/item_home_following_live.xml` + - `app/src/main/res/layout/item_home_following_chat.xml` + - `app/src/main/res/layout/item_home_following_schedule.xml` + - `app/src/main/res/layout/item_home_following_news_rank.xml` + - `app/src/main/res/layout/item_home_following_news_content.xml` + - 작업: + - `nsv_home_following_content`를 `text_tab_bar_home` 아래에 추가하고 기본 `visibility="gone"`으로 둔다. + - 섹션 순서는 `followingCreators`, `On Air`, `recentChats`, `monthlySchedules`, `recentNews`로 둔다. + - 각 섹션 title은 `view_section_title`을 include한다. + - 로그인 유도 화면 전용 layout은 이번 phase에서 추가하지 않는다. + - 검증: + - Run: `./gradlew :app:mergeDebugResources` + - Expected: 신규 layout/resource가 merge된다. + +- [ ] **Task 4.3: 팔로잉 adapter 구현** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingCreatorAdapter.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingLiveAdapter.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingChatAdapter.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingScheduleAdapter.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt` + - 작업: + - 각 adapter는 `submitItems()`와 item click callback을 제공한다. + - profile image는 기존 `loadHomeCreatorProfileImage()` 또는 동일 placeholder 정책을 사용한다. + - recent chat은 `ChatRoomListUiItem`을 바인딩한다. + - schedule은 `CreatorActivityType.labelResId`, `isOnAir`, `scheduledAtUtc` 표시 모델을 사용한다. + - news adapter는 rank item과 content item view type을 분리한다. + - 검증: + - Run: `./gradlew :app:compileDebugKotlin` + - Expected: 신규 adapter가 컴파일된다. + +- [ ] **Task 4.4: HomeMainFragment에 팔로잉 탭 연결** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - 작업: + - `private val homeFollowingViewModel: HomeFollowingViewModel by viewModel()`을 추가한다. + - 팔로잉 adapter 5개를 초기화한다. + - `showHomeTab(HOME_TAB_FOLLOWING)`에서 추천/랭킹 surface를 숨기고 팔로잉 surface를 표시한다. + - 팔로잉 탭 최초 선택 시 `homeFollowingViewModel.loadFollowing()`을 1회 호출한다. + - `LoginRequired` 상태에서는 팔로잉 섹션 content를 숨긴다. + - `Content` 상태에서는 각 section item이 비어 있으면 해당 섹션을 숨긴다. + - section title chevron은 `onFollowingSectionMoreClick(section)` callback까지만 연결하고 route는 호출하지 않는다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"` + - Expected: PASS. + +- [ ] **Task 4.5: UI routing skeleton 연결** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - 작업: + - creator item click은 기존 `openCreatorProfile(creatorId)`를 재사용한다. + - recent chat item click은 기존 v2 chat/DM 진입 flow를 확인해 재사용 가능한 메서드로 연결한다. + - live, schedule, news item click은 `type`/`targetId`별 route 함수로 분리한다. + - 목적지가 확정되지 않은 더보기 chevron은 route 없이 callback만 받는다. + - 검증: + - Run: `rg -n "openFollowing|onFollowing|HomeFollowingSection|openCreatorProfile|HOME_TAB_FOLLOWING" app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - Expected: 팔로잉 item click과 more click 콜백 함수가 확인된다. + +--- + +### Phase 5: 통합 검증과 문서 기록 + +- [ ] **Task 5.1: 팔로잉 관련 단위 테스트 실행** + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"` + - Expected: 팔로잉 관련 local unit/source test가 모두 PASS. + - 검증 기록: + - 실행 후 결과를 이 task 아래에 한국어로 누적 기록한다. + +- [ ] **Task 5.2: 리소스/컴파일/린트 검증** + - 검증: + - Run: `./gradlew :app:mergeDebugResources` + - Expected: 신규 layout/string resource merge PASS. + - Run: `./gradlew :app:compileDebugKotlin` + - Expected: Kotlin compile PASS. + - Run: `./gradlew :app:ktlintCheck` + - Expected: ktlint PASS. + - Run: `git diff --check` + - Expected: whitespace error 없음. + - 검증 기록: + - 실행 후 결과를 이 task 아래에 한국어로 누적 기록한다. + +- [ ] **Task 5.3: Figma 기준 수동 확인** + - 확인: + - Figma `24:5682` + - `app/src/main/res/layout/fragment_v2_main_home.xml` + - 수동 확인 항목: + - `팔로잉` 탭 선택 시 추천/랭킹 content가 겹쳐 보이지 않는다. + - 섹션 순서가 `팔로잉 크리에이터` → `On Air` → `최근 대화` → `이달의 스케줄` → `최근 소식`이다. + - title bar와 tab bar는 고정되고 팔로잉 content만 세로 스크롤된다. + - empty section은 숨겨진다. + - `isLoginRequired` 상태에서 팔로잉 섹션 content는 표시되지 않는다. + - 더보기 chevron 터치 시 앱이 크래시하지 않고 화면 이동은 발생하지 않는다. + - 검증 기록: + - 수동 확인 결과를 이 task 아래에 한국어로 누적 기록한다. + +--- + +## Verification Log +- 2026-06-25 Phase 1-3 구현 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 모두 PASS. +- 2026-06-25 Phase 1-3 코드 리뷰 및 재검증: blocking finding 없음. `./gradlew :app:mergeDebugResources`는 최초 sandbox lock 권한 오류 후 승인 실행으로 PASS했고, 나머지 검증 명령도 PASS. diff --git a/docs/20260625_메인_홈_팔로잉_탭/prd.md b/docs/20260625_메인_홈_팔로잉_탭/prd.md new file mode 100644 index 00000000..c6b74375 --- /dev/null +++ b/docs/20260625_메인_홈_팔로잉_탭/prd.md @@ -0,0 +1,308 @@ +# 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("creatorProfileImageUrl") val creatorProfileImageUrl: String, + @SerializedName("creatorNickname") val creatorNickname: String, + @SerializedName("title") val title: String, + @SerializedName("body") val body: String, + @SerializedName("thumbnailImageUrl") val thumbnailImageUrl: String?, + @SerializedName("targetId") val targetId: Long, + @SerializedName("occurredAtUtc") val occurredAtUtc: String, + @SerializedName("visibleFromAtUtc") val visibleFromAtUtc: String, + @SerializedName("rank") val rank: 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 터치 시 해당 크리에이터 채널로 이동한다. +- 리스트가 비어 있으면 섹션을 숨긴다. +- 이미지 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`은 creator ranking 소식으로 표시하고 `rank` 값을 강조한다. +- `CONTENT_RANKING`은 content ranking 소식으로 표시하고 `rank` 값을 강조한다. +- `CREATOR_RANKING`, `CONTENT_RANKING`에서 `rank`가 null이면 해당 ranking news item은 표시하지 않는다. +- 초기 운영 기간에는 `CREATOR_RANKING` 위주로 내려올 예정이다. +- `COMMUNITY_POST`는 커뮤니티 feed 형태로 표시한다. +- `AUDIO_CONTENT`는 오디오 콘텐츠 feed 형태로 표시한다. +- `PHOTO_CONTENT`는 우선 `화보` label로 표시한다. 이 label은 이후 기획 변경 가능성이 있다. +- 시간 표시는 `visibleFromAtUtc`를 기준으로 디바이스 타임존으로 변환한 뒤 상대 시간으로 표시한다. +- `thumbnailImageUrl`이 null이면 타입별 기본 이미지 표시/숨김 정책을 구현 계획에서 확정한다. +- item 터치 시 `type`과 `targetId`에 맞는 목적지로 이동한다. +- 리스트가 비어 있으면 섹션을 숨긴다. +- 기존 `FeedAdapter`, `FeedItem.Rank`, `FeedItem.Community`, `FeedItem.Content`, `FeedRankView`, `FeedCommunityView`, `FeedContentView` 재사용 가능성을 우선 검토한다. +- `PHOTO_CONTENT`는 기존 `FeedItem.Content`의 category 확장 또는 팔로잉 전용 item 추가 중 더 작은 변경을 구현 계획에서 선택한다. +- 각 섹션의 “더보기” chevron은 터치 액션만 연결하고, 실제 이동 목적지는 아직 만들지 않는다. + +### 재사용 가능한 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 실제 이동 목적지는 아직 만들지 않는다. 이번 범위에서는 터치 액션 연결까지만 지정한다.