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.
|
||||
308
docs/20260625_메인_홈_팔로잉_탭/prd.md
Normal file
308
docs/20260625_메인_홈_팔로잉_탭/prd.md
Normal file
@@ -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<FollowingCreatorResponse>,
|
||||
@SerializedName("onAirLives") val onAirLives: List<FollowingLiveResponse>,
|
||||
@SerializedName("recentChats") val recentChats: List<ChatRoomListItemResponse>,
|
||||
@SerializedName("monthlySchedules") val monthlySchedules: List<FollowingScheduleResponse>,
|
||||
@SerializedName("recentNews") val recentNews: List<FollowingNewsResponse>
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class FollowingCreatorResponse(
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class FollowingLiveResponse(
|
||||
@SerializedName("liveId") val liveId: Long,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("startedAtUtc") val startedAtUtc: String
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class FollowingScheduleResponse(
|
||||
@SerializedName("scheduleId") val scheduleId: String,
|
||||
@SerializedName("creatorId") val creatorId: Long,
|
||||
@SerializedName("creatorProfileImageUrl") val creatorProfileImageUrl: String,
|
||||
@SerializedName("creatorNickname") val creatorNickname: String,
|
||||
@SerializedName("title") val title: String,
|
||||
@SerializedName("type") val type: CreatorActivityType,
|
||||
@SerializedName("targetId") val targetId: Long,
|
||||
@SerializedName("scheduledAtUtc") val scheduledAtUtc: String,
|
||||
@SerializedName("isOnAir") val isOnAir: Boolean
|
||||
)
|
||||
|
||||
@Keep
|
||||
data class FollowingNewsResponse(
|
||||
@SerializedName("newsId") val newsId: String,
|
||||
@SerializedName("type") val type: FollowingNewsType,
|
||||
@SerializedName("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<T>`, RxJava3 `Single`, `BaseViewModel`, Koin DI 패턴을 따른다.
|
||||
- 공개 API 스키마와 서버 필드명은 임의 변경하지 않는다.
|
||||
- API에는 query parameter를 추가하지 않는다.
|
||||
- optional authorization 처리에서 `BuildConfig` 값, token, URL을 로그/Toast/크래시 메시지에 노출하지 않는다.
|
||||
- string resource는 `values`, `values-en`, `values-ja` 반영 필요 여부를 구현 계획에서 확인한다.
|
||||
- 신규 mapper 또는 formatter 로직은 local unit test 검증을 우선한다.
|
||||
|
||||
---
|
||||
|
||||
## 10. Metrics
|
||||
- 팔로잉 탭 노출 성공률: API success 후 적어도 하나의 섹션 또는 로그인 유도 화면이 정상 표시되는 비율.
|
||||
- 로그인 필요 상태 노출 수: `isLoginRequired = true` 응답을 받은 팔로잉 탭 진입 수.
|
||||
- 팔로잉 섹션 item click: creator, live, chat, schedule, news item별 클릭 이벤트.
|
||||
- API 실패율과 empty 상태 비율.
|
||||
- 팔로잉 탭 최초 렌더링 시간.
|
||||
|
||||
---
|
||||
|
||||
## 11. Open Questions
|
||||
- 로그인 유도 화면의 정확한 디자인, 문구, CTA, 로그인 완료 후 복귀 정책은 아직 정해지지 않았다.
|
||||
- `PHOTO_CONTENT`의 `화보` label은 임시 정책이며 이후 기획 변경 가능성이 있다.
|
||||
- 각 섹션의 “더보기” chevron 실제 이동 목적지는 아직 만들지 않는다. 이번 범위에서는 터치 액션 연결까지만 지정한다.
|
||||
Reference in New Issue
Block a user