docs(home): 팔로잉 탭 요구와 계획을 기록한다
This commit is contained in:
436
docs/20260625_메인_홈_팔로잉_탭/plan-task.md
Normal file
436
docs/20260625_메인_홈_팔로잉_탭/plan-task.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user