# 메인 홈 팔로잉 탭 구현 계획/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 연결 - [x] **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 실패. - Result: RED 확인. layout ID와 `HomeMainFragment` 팔로잉 wiring 미구현 상태에서 6개 source test가 모두 실패했다. - [x] **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된다. - Result: PASS. `nsv_home_following_content`, 5개 섹션 RecyclerView, 6개 item layout이 resource merge를 통과했다. - [x] **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가 컴파일된다. - Result: PASS. 팔로잉 creator/live/chat/schedule/news adapter 5개가 `compileDebugKotlin`에서 컴파일됐다. - [x] **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. - Result: PASS. 팔로잉 ViewModel 주입, adapter 연결, 탭 surface 전환, 최초 1회 load, login-required/empty/error 섹션 숨김, content 섹션 binding이 source test로 검증됐다. - [x] **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 콜백 함수가 확인된다. - Result: PASS. creator click은 `openCreatorProfile`, recent chat click은 기존 AI/DM chat 진입 flow로 연결했고 live/schedule/news/more click은 route 없는 callback skeleton으로 유지했다. - 검증 기록: - 2026-06-25 Phase 4 코드 리뷰: Figma `24:5682`와 PRD 기준으로 `On Air` 시작 시간, 이달의 스케줄 프로필/타입 label/On Air 상태, 최근 소식 label/title 바인딩 누락을 확인했다. 누락 항목은 `HomeFollowingFragmentSourceTest` RED로 고정한 뒤 adapter/layout 최소 수정으로 보완했다. - 2026-06-25 Phase 4 재코드 리뷰: 현재 워킹트리 기준 `HomeMainFragment`, 팔로잉 adapter 5개, 팔로잉 layout, `HomeFollowingFragmentSourceTest`를 Figma `24:5682`/PRD와 대조했으며 blocking finding은 발견하지 못했다. --- ### Phase 5: 통합 검증과 문서 기록 - [x] **Task 5.1: 팔로잉 관련 단위 테스트 실행** - 검증: - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"` - Expected: 팔로잉 관련 local unit/source test가 모두 PASS. - 검증 기록: - 2026-06-25 Phase 4 리뷰 보완 후 실행 결과 PASS. 팔로잉 관련 local unit/source test가 모두 통과했다. - [x] **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 없음. - 검증 기록: - 2026-06-25 Phase 4 리뷰 보완 후 `./gradlew :app:mergeDebugResources` PASS. 최초 sandbox lock 권한 오류 후 승인 실행으로 통과했다. - 2026-06-25 Phase 4 리뷰 보완 후 `./gradlew :app:compileDebugKotlin` PASS. - 2026-06-25 Phase 4 리뷰 보완 후 `./gradlew :app:ktlintCheck` PASS. 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. - 2026-06-25 Phase 4 리뷰 보완 후 `git diff --check` PASS. - [ ] **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 터치 시 앱이 크래시하지 않고 화면 이동은 발생하지 않는다. - 검증 기록: - 2026-06-25 Figma `24:5682` 디자인 컨텍스트와 스크린샷 기준 정적 대조를 수행했다. 실제 기기/에뮬레이터에서의 수동 화면 확인은 아직 실행하지 않았다. --- ## 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. - 2026-06-25 Phase 4 코드 리뷰 및 검증: Figma `24:5682` 기준 UI 필드 바인딩 누락을 보완했고, `./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 4 재코드 리뷰 및 검증: blocking finding 없음. Figma `24:5682` 디자인 컨텍스트/스크린샷과 현재 Phase 4 변경을 정적 대조했고, `./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. `mergeDebugResources`는 최초 sandbox lock 권한 오류 후 승인 실행으로 PASS했다.