diff --git a/docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md b/docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md new file mode 100644 index 00000000..b3e7b32f --- /dev/null +++ b/docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md @@ -0,0 +1,355 @@ +# 메인 홈 추천 UI와 API 연동 Plan / Task + +> **For agentic workers:** 구현 시 `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`를 사용해 task 단위로 진행한다. 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. + +**Goal:** Figma `home_001`(`24:5514`) 기준 메인 홈 추천 화면을 `HomeMainFragment`에 구성하고, `GET /api/v2/home/recommendations` 및 모두 팔로우 API를 기존 XML View + v2 widget 패턴으로 연동한다. + +**Architecture:** 신규 홈 추천 API, Repository, ViewModel, DTO/UI model/mapper는 `kr.co.vividnext.sodalive.v2.main.home` 하위에 둔다. 화면은 `HomeMainFragment`와 `fragment_v2_main_home.xml`을 확장하고, 기존 `v2.widget` 컴포넌트는 재사용하되 추천 화면에 필요한 최소 확장만 수행한다. + +**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, RxJava3, Retrofit, Gson, Koin, Coil, local unit test. + +--- + +## 전제와 성공 기준 +- PRD: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md` +- 기존 `HomeApi`에는 메서드를 추가하지 않고 신규 `HomeRecommendationApi`를 만든다. +- 신규 화면 관련 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다. +- `추천 필모그래피`, `또 다른 모습` 섹션은 만들지 않는다. +- 각 API 리스트가 비어 있으면 해당 section-title과 목록을 숨긴다. +- 구현 완료 후 최소 `./gradlew :app:testDebugUnitTest`와 `./gradlew :app:ktlintCheck`를 실행한다. + +--- + +### Phase 1: 기존 구조 확인과 구현 경계 고정 + +- [x] **Task 1.1: 기존 홈/위젯/네트워크 패턴 확인** + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` + - 확인: `app/src/main/res/layout/fragment_v2_main_home.xml` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/home/HomeApi.kt` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/common/ApiResponse.kt` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/extensions/ImageExtensions.kt` + - 검증: `HomeMainFragment`는 현재 빈 `FrameLayout` 바인딩만 가지고 있고, 신규 API 등록은 `AppDI.kt`의 `networkModule`, `repositoryModule`, `viewModelModule`에 추가해야 함을 확인한다. + +- [x] **Task 1.2: 재사용 위젯과 신규 UI 범위 확정** + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/livethumbnail/LiveThumbnailSimpleView.kt` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/characterchatthumbnail/CharacterChatThumbnailView.kt` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt` + - 확인: `app/src/main/res/layout/view_section_title.xml` + - 신규 필요 후보: 최근 활동 카드, 최근 데뷔 크리에이터 카드, 장르별/응원 크리에이터 profile grid, 모두 팔로우 버튼, 사업자 정보 접기 영역. + - 검증: PRD의 Section Mapping 표와 구현 대상/제외 대상이 일치하는지 문서 체크한다. + +--- + +### Phase 2: API DTO, Repository, DI 추가 + +- [x] **Task 2.1: 홈 추천 API/DTO 파일 생성** + - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationApi.kt` + - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationModels.kt` + - 구현 내용: + - `GET /api/v2/home/recommendations` + - `POST /api/v2/home/recommendations/creators/follow` + - `FollowRecommendedCreatorsRequest(val creatorIds: List)` + - `HomeRecommendationResponse`와 PRD의 모든 item DTO + - 주의: 서버 필드명은 임의 변경하지 않고 필요할 때만 `@SerializedName`을 추가한다. + - 검증: `HomeRecommendationResponse` 필드가 `lives`, `banners`, `recentlyActiveCreators`, `recentDebutCreators`, `firstAudioContents`, `aiCharacters`, `genreCreators`, `cheerCreators`, `popularCommunityPosts`를 모두 포함한다. + +- [x] **Task 2.2: Repository 생성** + - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationRepository.kt` + - 구현 내용: + - `getRecommendations(token: String)` + - `followRecommendedCreators(request: FollowRecommendedCreatorsRequest, token: String)` + - 검증: 기존 Repository처럼 Retrofit `Single>`를 그대로 반환하고, token은 호출부에서 `"Bearer ${SharedPreferenceManager.token}"` 형태로 전달한다. + +- [x] **Task 2.3: Koin DI 등록** + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - 구현 내용: + - `networkModule`에 `HomeRecommendationApi` 등록 + - `repositoryModule`에 `HomeRecommendationRepository` 등록 + - 이후 Phase 4에서 생성할 `HomeRecommendationViewModel` 등록 + - 검증 명령: `./gradlew :app:compileDebugKotlin` + - 기대 결과: 신규 API/Repository import 및 Koin 등록 컴파일 성공. + +--- + +### Phase 3: 순수 mapper와 unit test 작성 + +- [x] **Task 3.1: activity type 표시 mapper 테스트 작성** + - 생성: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/RecommendedActivityTypeTest.kt` + - 대상: `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`, 알 수 없는 code. + - 기대: + - `LIVE`, `LIVE_REPLAY` -> `R.string.home_recommendation_activity_live` + - `AUDIO` -> `R.string.home_recommendation_activity_audio` + - `COMMUNITY` -> `R.string.home_recommendation_activity_community` + - unknown -> null 또는 미표시 정책 + - 검증 명령: `./gradlew :app:testDebugUnitTest --tests kr.co.vividnext.sodalive.v2.main.home.RecommendedActivityTypeTest` + - 기대 결과: mapper 구현 전 실패. + +- [x] **Task 3.2: activity type mapper 구현** + - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/RecommendedActivityType.kt` + - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationMappers.kt` + - 구현 내용: + - 백엔드 code 문자열을 앱 enum으로 변환 + - 표시 문자열은 `@StringRes`로 반환해 Fragment/ViewHolder에서 `getString()` 처리 + - 알 수 없는 code는 크래시 없이 null 반환 후 UI에서 activity label 미표시 + - 검증 명령: `./gradlew :app:testDebugUnitTest --tests kr.co.vividnext.sodalive.v2.main.home.RecommendedActivityTypeTest` + - 기대 결과: PASS. + +- [x] **Task 3.3: 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` + - 추가 문자열: + - 추천/랭킹/팔로잉 tab + - 섹션 제목 + - `라이브`, `오디오`, `커뮤니티` + - `모두 팔로우 하기`, `모두 팔로우 완료` + - `더보기`, `접기` + - 사업자 정보 텍스트 + - 검증 명령: `./gradlew :app:mergeDebugResources` + - 기대 결과: resource merge 성공. + +--- + +### Phase 4: ViewModel과 화면 상태 구성 + +- [ ] **Task 4.1: UI model 정의** + - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiState.kt` + - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt` + - 구현 내용: + - `Loading`, `Content`, `Empty`, `Error` 상태 + - 섹션별 UI model 또는 adapter item + - `HomeFirstAudioContentItem` -> `AudioContentTag.First`, `Point`, `Free` 조건 매핑 + - `HomeAiCharacterItem.originalWorkTitle == null`이면 기존 `CharacterChatThumbnailView`의 `shouldShowOriginalTitle = false` + - `HomePopularCommunityPostItem`의 `price`, `existOrdered` 기반 유료 상태 모델 + - 검증: DTO를 Fragment/ViewHolder에 직접 노출하지 않는다. + +- [ ] **Task 4.2: ViewModel 생성** + - 생성: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeRecommendationViewModel.kt` + - 구현 내용: + - `recommendationStateLiveData` + - `toastLiveData` + - `isLoading` + - `loadRecommendations()` + - `followCreators(sectionKey: String, creatorIds: List)` + - 빈 `creatorIds`면 API 미호출 + - 모두 팔로우 success 후 해당 section/button 상태 완료 처리 + - 검증: `ApiResponse.success == false`, `data == null`, Throwable 상황에서 기존 패턴처럼 unknown error toast를 노출한다. + +- [ ] **Task 4.3: ViewModel DI 등록 완료** + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - 구현 내용: `viewModel { HomeRecommendationViewModel(get()) }` + - 검증 명령: `./gradlew :app:compileDebugKotlin` + - 기대 결과: Koin 등록 및 ViewModel 생성 컴파일 성공. + +--- + +### Phase 5: HomeMainFragment 레이아웃과 상단 UI 구현 + +- [ ] **Task 5.1: 홈 추천 화면 레이아웃 작성** + - 수정: `app/src/main/res/layout/fragment_v2_main_home.xml` + - 포함: + - `include` 또는 직접 배치로 `view_title_bar_home` + - `TextTabBarView` + - `TextTabBarView` 아래 추천 content 전용 세로 `NestedScrollView` 또는 동등한 scroll container + - 추천 content 내부 섹션별 컨테이너와 가로 `RecyclerView` + - 추천 content 내부 사업자 정보 영역 + - 제외: + - `ViewPager2` + - `FragmentStateAdapter` + - `랭킹`, `팔로잉`용 별도 Fragment + - `랭킹`, `팔로잉`용 숨김 View를 미리 배치하는 `VISIBLE`/`GONE` 구조 + - 검증: root background는 기존처럼 `@color/black` 유지하고, 세로 스크롤 시 title-bar와 `TextTabBarView`는 화면에 남으며 그 아래 추천 content만 스크롤된다. + +- [ ] **Task 5.2: title bar와 tab bar 바인딩** + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` + - 확인: `app/src/main/res/layout/view_title_bar_home.xml` + - 확인: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/TextTabBarView.kt` + - 구현 내용: + - right icons 순서: `ic_bar_cash`, `ic_bar_search`, `ic_bar_bell` + - tabs 순서: 추천, 랭킹, 팔로잉 + - 추천 tab selected 상태 + - tab 전환은 글자 터치 callback으로만 처리 + - 좌우 swipe 전환은 추가하지 않음 + - 이번 범위에서는 추천 tab content만 바인딩하고 `랭킹`, `팔로잉` content는 생성하지 않음 + - 검증: 앱 실행 시 상단 bar와 tab이 표시되고 text resource가 적용된다. 좌우 swipe로 tab 전환이 발생하지 않는다. + +--- + +### Phase 6: 섹션 adapter와 재사용 위젯 바인딩 + +> Phase 6까지는 실제 API/ViewModel 연동 전 단계이므로, 실제 기기에서 UI 배치와 섹션 표시 형태를 확인할 수 있도록 Fragment 또는 adapter 호출부에 샘플 데이터를 임시 주입할 수 있다. 샘플 데이터는 Phase 9의 실제 `HomeRecommendationViewModel` observe/API 바인딩 전 제거하거나 실제 상태 바인딩으로 대체한다. + +- [ ] **Task 6.1: 라이브, 배너 섹션 바인딩** + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeLiveAdapter.kt` + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeBannerBinder.kt` + - 재사용: `LiveThumbnailSimpleView`, `BannerView` + - 구현 내용: + - `lives`가 비면 라이브 섹션 숨김 + - `banners`가 비면 배너 섹션 숨김 + - 이미지 로딩은 `ImageView.loadUrl()` 또는 기존 widget adapter의 방식 사용 + - 검증: 샘플 `lives`, `banners` 데이터로 실제 기기에서 라이브 가로 목록과 배너 캐러셀이 의도한 위치/크기로 보인다. 빈 리스트일 때 title/RecyclerView가 함께 `GONE`. + +- [ ] **Task 6.2: 최근 활동/최근 데뷔 크리에이터 UI 구현** + - 생성 후보: `app/src/main/res/layout/item_home_recent_activity_creator.xml` + - 생성 후보: `app/src/main/res/layout/item_home_recent_debut_creator.xml` + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeRecentActivityCreatorAdapter.kt` + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeRecentDebutCreatorAdapter.kt` + - 구현 내용: + - 최근 활동 activity type label은 Phase 3 mapper 결과 사용 + - unknown activity type은 label view 숨김 + - 닉네임/제목은 기존 typography와 말줄임 정책 적용 + - 검증: 샘플 creator 데이터로 실제 기기에서 최근 활동/최근 데뷔 카드 배치가 보인다. `LIVE_REPLAY`도 `라이브`로 표시. + +- [ ] **Task 6.3: 첫 오디오 콘텐츠와 AI 캐릭터 섹션 바인딩** + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFirstAudioAdapter.kt` + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeAiCharacterAdapter.kt` + - 재사용: `AudioContentCardView`, `CharacterChatThumbnailView` + - 구현 내용: + - 첫 오디오 콘텐츠는 `AudioContentTag.First` 항상 포함 + - `isPointAvailable == true`면 `AudioContentTag.Point` + - `price == 0`이면 `AudioContentTag.Free` + - `AudioContentTag.Original`은 포함하지 않음 + - AI character image는 `profileImage` 사용 + - 검증: 샘플 audio/AI character 데이터로 실제 기기에서 카드 배치와 이미지/텍스트 영역이 보인다. 오디오 태그 조합이 PRD 조건과 일치. + +- [ ] **Task 6.4: 장르별/응원 크리에이터와 모두 팔로우 버튼 구현** + - 생성 후보: `app/src/main/res/layout/item_home_creator_profile.xml` + - 생성 후보: `app/src/main/res/layout/view_home_follow_all_button.xml` + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeGenreCreatorAdapter.kt` + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCheerCreatorAdapter.kt` + - 구현 내용: + - `cheerCreators.creatorId` 목록으로 모두 팔로우 API 호출 + - `genreCreators`의 해당 그룹 `creators.creatorId` 목록으로 모두 팔로우 API 호출 + - success 후 `모두 팔로우 완료`, `ic_new_following`, click disabled + - 실패 시 toast/error 표시 + - 검증: 샘플 genre/cheer creator 데이터로 실제 기기에서 profile grid와 모두 팔로우 버튼 배치가 보인다. 빈 creator list에서는 API 호출하지 않고 버튼 상태 유지. + +--- + +### Phase 7: FeedCommunityView 추천 페이지 확장 + +- [ ] **Task 7.1: FeedItem.Community 추천 필드 확장** + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedItem.kt` + - 추가 후보: + - `imageUrl: String?` + - `price: Int` + - `existOrdered: Boolean` + - `showKeyword: Boolean` + - 주의: 기존 호출부 기본값을 제공해 기존 feed 사용처 영향 최소화. + - 검증 명령: `./gradlew :app:compileDebugKotlin` + +- [ ] **Task 7.2: 커뮤니티 이미지/유료 overlay 레이아웃 추가** + - 수정: `app/src/main/res/layout/view_feed_community.xml` + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt` + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedAdapter.kt` + - 구현 내용: + - keyword는 추천 페이지에서 숨김 + - `imageUrl != null`이면 `346dp x 236dp`, radius `14dp`, centerCrop 이미지 표시 + - `price > 0 && existOrdered == false`면 blur/lock overlay와 가격 capsule 표시 + - 무료/구매 완료는 overlay와 가격 capsule 숨김 + - `FeedImageViews.primary`로 커뮤니티 이미지도 외부에서 로드 가능하게 노출 + - 검증: 기존 Community variant는 기본값으로 keyword를 유지하고, 추천용 item만 keyword를 숨긴다. + +- [ ] **Task 7.3: 인기 커뮤니티 섹션 바인딩** + - 생성 후보: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomePopularCommunityAdapter.kt` + - 재사용: `FeedAdapter` 또는 `FeedCommunityView` + - 구현 내용: + - `postId`, `creatorId`, `creatorNickname`, `creatorProfileImage`, `content`, `createdAt`, `likeCount`, `commentCount`, `imageUrl`, `price`, `existOrdered` 매핑 + - `audioUrl`은 별도 플레이어 UI 없이 상세 이동 데이터로만 유지 + - 검증: keyword 미노출, 이미지 null이면 이미지 영역 `GONE`. + +--- + +### Phase 8: 사업자 정보 접기/더보기 구현 + +- [ ] **Task 8.1: 사업자 정보 UI 구현** + - 수정: `app/src/main/res/layout/fragment_v2_main_home.xml` + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` + - 구현 내용: + - 기본 `maxLines = 3`, `ellipsize = end` + - `더보기` 클릭 시 전체 표시 + - `접기` 클릭 시 다시 3줄 말줄임 + - 3줄 이하 텍스트면 toggle 숨김 + - 검증: 외부 라이브러리 없이 `TextView.post { lineCount }` 기반으로 toggle 노출 여부 결정. + +--- + +### Phase 9: 라우팅, empty/error/loading 정책 연결 + +- [ ] **Task 9.1: Fragment observe와 loading/toast 연결** + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` + - 구현 내용: + - `HomeRecommendationViewModel` 주입 + - `loadRecommendations()` 호출 + - `recommendationStateLiveData` observe 후 각 섹션 bind + - `isLoading`은 기존 `LoadingDialog` 또는 홈 화면 패턴 확인 후 적용 + - `toastLiveData`는 `BaseFragment.showToast()` 사용 + - 검증: API 실패 시 앱 크래시 없이 toast/error 처리. + +- [ ] **Task 9.2: 클릭 라우팅 연결** + - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` + - 확인: 기존 상세 화면 Activity/Fragment 라우팅 파일 + - 구현 내용: + - 배너 type별 이동은 기존 딥링크/이동 정책 확인 후 연결 + - live, creator, audio content, AI character, community post 클릭은 adapter callback으로 Fragment에 위임 + - 확정되지 않은 목적지는 임시 주석으로 남기지 않고 no-op 또는 기존 정책 확인 후 최소 연결 + - 검증: UI 컴포넌트 내부에서 목적지를 결정하지 않는다. + +--- + +### Phase 10: 최종 검증과 문서 기록 + +- [ ] **Task 10.1: 단위 테스트와 빌드 검증** + - 실행: `./gradlew :app:testDebugUnitTest` + - 실행: `./gradlew :app:compileDebugKotlin` + - 실행: `./gradlew :app:mergeDebugResources` + - 실행: `./gradlew :app:ktlintCheck` + - 기대 결과: 모두 성공. + +- [ ] **Task 10.2: 수동 확인** + - 확인 항목: + - 추천 tab 선택 상태 + - tab 전환은 글자 터치로만 동작하고 swipe 전환은 없음 + - 세로 스크롤 시 title-bar와 `TextTabBarView`는 유지되고 `TextTabBarView` 아래 추천 content만 스크롤됨 + - 섹션 순서가 PRD 도식과 일치 + - 제외 섹션 미노출 + - 빈 리스트 섹션 숨김 + - 첫 오디오 태그 조건 + - activity type 다국어 문자열 + - 모두 팔로우 success 후 완료 상태 + - 인기 커뮤니티 유료/무료/구매 완료 이미지 상태 + - 사업자 정보 더보기/접기 + - 기대 결과: PRD Metrics 항목 충족. + +- [ ] **Task 10.3: 검증 기록 누적** + - 수정: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md` + - 수정: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md` + - 구현 내용: 실행 명령, 결과, 실패 시 원인과 후속 조치를 문서 하단 `검증 기록`에 누적한다. + - 주의: 기존 검증 기록은 삭제하거나 덮어쓰지 않는다. + +--- + +## 구현 중 확인 필요 +- 배너 `type`별 이동 정책과 서버 code 목록. +- `HomeLiveItem`, `HomeBannerItem`, `HomeActiveCreatorItem`, `HomeCreatorItem`, `HomeGenreCreatorGroupItem`의 최종 서버 필드명. +- 시간 표시 포맷: `activityAt`, `releaseDate`, `createdAt`, `beginDateTime`에 기존 formatter를 재사용할 수 있는지 확인. +- 모두 팔로우 API success response의 `data` 형태. `ApiResponse.success == true`만으로 완료 처리 가능한지 백엔드 계약 확인. +- 사업자 정보 텍스트를 `strings.xml`로 둘지 서버/설정값으로 받을지 운영 정책 확인. + +--- + +## 검증 기록 +- 2026-06-02: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md`, `docs/agent-guides/work-plan-docs.md`를 확인해 계획 문서 위치와 phase/task 형식을 맞췄다. +- 2026-06-02: `HomeMainFragment`, `fragment_v2_main_home.xml`, `HomeApi`, `HomeRepository`, `AppDI`, `ApiResponse`, 주요 v2 widget 파일을 확인해 신규 API/Repository/ViewModel과 UI 작업 범위를 계획에 반영했다. +- 2026-06-02: 사용자 추가 제공 정보에 따라 `ViewPager2`/swipe 전환과 tab별 Fragment 선행 생성을 제외하고, `TextTabBarView` 아래 추천 content만 세로 스크롤되도록 Phase 5와 수동 확인 항목을 갱신했다. +- 2026-06-02: Phase 1 파일과 PRD Section Mapping을 확인해 `HomeMainFragment`/`fragment_v2_main_home.xml`은 아직 빈 홈 v2 shell이고, Phase 1-3 범위에서는 UI/ViewModel/adapter를 만들지 않는 것으로 구현 경계를 고정했다. +- 2026-06-02: `RecommendedActivityTypeTest`를 먼저 추가하고 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.RecommendedActivityTypeTest"`를 실행해 `model` 패키지, mapper 함수, `home_recommendation_activity_*` 리소스 미존재로 실패하는 RED 상태를 확인했다. +- 2026-06-02: `HomeRecommendationApi`, `HomeRecommendationModels`, `HomeRecommendationRepository`를 추가하고 `AppDI.kt`의 `networkModule`/`repositoryModule`에 신규 API/Repository를 등록했다. Phase 4 대상인 `HomeRecommendationViewModel` 등록은 추가하지 않았다. +- 2026-06-02: `RecommendedActivityType`, `HomeRecommendationMappers`, 한국어/영어/일본어 string resource를 추가한 뒤 targeted test를 재실행해 PASS로 GREEN 전환을 확인했다. +- 2026-06-02: `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:testDebugUnitTest`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `lsp_diagnostics`는 `kotlin-lsp` 미설치로 실행되지 않아 Gradle compile/test/ktlint 결과로 보완했다. +- 2026-06-02: 백엔드 activity type code가 대소문자와 무관하게 들어와도 매핑되도록 `RecommendedActivityTypeTest`에 `live`, `Live_RePlay` 케이스를 추가했다. 수정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.RecommendedActivityTypeTest"`에서 해당 2개 테스트 실패로 RED를 확인했다. +- 2026-06-02: `RecommendedActivityType.from()`의 code 비교를 `equals(ignoreCase = true)`로 변경한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.RecommendedActivityTypeTest"`, `./gradlew :app:compileDebugKotlin`을 실행했고 모두 BUILD SUCCESSFUL을 확인했다. +- 2026-06-02: Phase 6까지 진행했을 때 실제 API/ViewModel 연동 전에도 UI 배치와 섹션 표시 형태를 확인할 수 있도록 샘플 데이터를 임시 주입하는 검증 방식을 Phase 6 설명과 섹션별 검증 항목에 추가했다. 샘플 데이터는 Phase 9 실제 상태 바인딩 전 제거하거나 대체해야 한다. diff --git a/docs/20260601_메인_홈_추천_UI와_API_연동/prd.md b/docs/20260601_메인_홈_추천_UI와_API_연동/prd.md new file mode 100644 index 00000000..510db5cf --- /dev/null +++ b/docs/20260601_메인_홈_추천_UI와_API_연동/prd.md @@ -0,0 +1,416 @@ +# PRD: 메인 홈 추천 UI와 API 연동 + +## 1. Overview +Figma `home_001` 화면(`24:5514`)을 기준으로 메인 홈 추천 영역을 구성하고, 홈 추천 API 응답을 기존 v2 widget 중심으로 바인딩한다. + +--- + +## 2. Problem +- 메인 홈 추천 화면은 라이브, 배너, 최근 활동, 신규 데뷔, 첫 오디오 콘텐츠, AI 캐릭터, 장르별 크리에이터, 응원 크리에이터, 인기 커뮤니티 등 여러 데이터 섹션을 한 화면에 표시해야 한다. +- Figma에는 이미 `live`, `banner`, `section-title`, `contents`, `chat-thumbnail`, `profile`, `feed` 등 재사용 가능한 형태가 다수 포함되어 있으므로, 기존에 생성된 widget을 최대한 활용해야 한다. +- `HomeActiveCreatorItem.activityType`은 백엔드 enum code 그대로 표시하면 사용자에게 부자연스러우므로 `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`를 다국어 처리 가능한 표시 문자열로 변환해야 한다. +- 사업자 정보 섹션은 별도 외부 라이브러리 없이 최대 3줄 말줄임과 더보기/접기 토글을 제공해야 한다. +- Figma에 포함된 일부 섹션은 이번 범위에서 제외되어야 하므로 구현 범위를 명확히 분리해야 한다. + +--- + +## 3. Goals +- 홈 추천 API 응답 `HomeRecommendationResponse`를 받아 Figma `24:5514`의 홈 추천 섹션 순서에 맞게 표시한다. +- 기존 `HomeApi`에 메서드를 추가하지 않고, 메인 홈 추천 전용 신규 API 인터페이스/Repository 흐름을 만든다. +- 추천 탭이 선택된 `HomeMainFragment`에 메인 홈 추천 UI를 구현한다. +- Figma에서 기존 widget으로 대응 가능한 항목은 기존 v2 widget을 우선 재사용한다. +- 재사용이 어려운 영역만 최소 범위 신규 UI 또는 adapter item으로 정의한다. +- `추천 필모그래피`, `또 다른 모습` 섹션은 만들지 않는다. +- 최하단 사업자 정보 섹션은 표시하되, 기본 최대 3줄 말줄임과 `더보기`/`접기` 토글을 제공한다. +- `HomeActiveCreatorItem.activityType`은 백엔드 code를 앱 내부 enum 또는 mapper로 변환하고, 표시 문구는 string resource 기반 다국어 처리를 적용한다. +- API DTO, 화면 상태, empty/error/loading 정책, click routing은 구현 계획에서 검증 가능하도록 정리한다. + +--- + +## 4. Non-Goals +- 이번 PRD 작성 단계에서는 코드, 리소스, 레이아웃 파일을 구현하지 않는다. +- `추천 필모그래피` 섹션은 구현하지 않는다. +- `또 다른 모습` 섹션은 구현하지 않는다. +- 추천 알고리즘, API 응답 정렬 기준, 서버 필드명은 변경하지 않는다. +- 외부 라이브러리를 추가하지 않는다. +- Compose 화면으로 전환하지 않는다. +- `ViewPager` 또는 swipe 기반 tab 전환은 추가하지 않는다. +- 이번 범위에서 `랭킹`, `팔로잉`용 별도 Fragment 또는 숨김 View를 미리 만들지 않는다. +- 기존 레거시 홈 화면 전체 리팩터링은 포함하지 않는다. +- Figma에 없는 skeleton loading, shimmer, 임의 애니메이션, 추가 badge는 만들지 않는다. + +--- + +## 5. Target Users +- 메인 홈에서 추천 라이브, 크리에이터, 오디오 콘텐츠, AI 캐릭터, 커뮤니티를 탐색하는 앱 사용자. +- 기존 XML Views와 v2 widget을 재사용해 홈 추천 화면을 구현/유지보수하는 Android 개발자. + +--- + +## 6. User Stories +- 사용자는 홈에서 현재 라이브 중인 크리에이터와 추천 콘텐츠를 빠르게 탐색하고 싶다. +- 사용자는 배너를 스와이프하거나 터치해 관련 이벤트, 크리에이터, 시리즈, 외부 링크로 이동하고 싶다. +- 사용자는 최근 활동한 크리에이터의 활동 유형을 `라이브`, `오디오`, `커뮤니티`처럼 이해 가능한 문구로 보고 싶다. +- 사용자는 인기 커뮤니티 글을 읽다가 사업자 정보가 길어도 홈 화면 탐색이 과도하게 방해받지 않기를 기대한다. +- 개발자는 기존 v2 widget을 최대한 재사용해 화면별 UI 중복과 스타일 차이를 줄이고 싶다. + +--- + +## 7. Core Features + +### 홈 추천 API 연동 +메인 홈 추천 화면은 다음 응답을 단일 API 결과로 받아 섹션별 UI 모델로 변환한다. + +#### Response Contract +```kotlin +data class HomeRecommendationResponse( + val lives: List, + val banners: List, + val recentlyActiveCreators: List, + val recentDebutCreators: List, + val firstAudioContents: List, + val aiCharacters: List, + val genreCreators: List, + val cheerCreators: List, + val popularCommunityPosts: List +) +``` + +#### Requirements +- API endpoint는 `GET /api/v2/home/recommendations`를 사용한다. +- 기존 `HomeApi`에 추가하지 않고 홈 추천 전용 신규 API 인터페이스를 만든다. +- Repository/ViewModel 위치는 구현 계획에서 `HomeMainFragment`와 기존 v2 홈 구조를 확인한 뒤 결정한다. +- 응답 DTO는 서버 필드명을 임의 변경하지 않고, 필요한 경우 `@SerializedName`만 추가한다. +- 화면 UI는 DTO를 직접 노출하지 않고 섹션별 UI model 또는 adapter item으로 변환한다. +- 각 리스트가 비어 있으면 해당 섹션은 숨기는 것을 기본 정책으로 한다. +- 전체 API 실패 시 기존 홈 화면의 error/toast/loading 패턴을 따른다. +- 이미지 URL이 null이면 호출부의 기존 placeholder 정책을 따른다. + +#### Changed Item Contracts +```kotlin +data class HomeFirstAudioContentItem( + val contentId: Long, + val creatorId: Long, + val creatorNickname: String, + val creatorProfileImage: String?, + val title: String, + val price: Int, + val coverImage: String?, + val releaseDate: String, + val isPointAvailable: Boolean +) + +data class HomeAiCharacterItem( + val characterId: Long, + val name: String, + val description: String, + val profileImage: String?, + val totalChatCount: Long, + val originalWorkTitle: String? +) + +data class HomePopularCommunityPostItem( + val postId: Long, + val creatorId: Long, + val creatorNickname: String, + val creatorProfileImage: String?, + val imageUrl: String?, + val audioUrl: String?, + val content: String, + val price: Int, + val createdAt: String, + val likeCount: Long, + val commentCount: Long, + val existOrdered: Boolean +) +``` + +#### Changed Item Requirements +- `HomeFirstAudioContentItem.price`는 첫 오디오 콘텐츠의 무료 여부 표시 정책에 사용한다. +- `HomeFirstAudioContentItem.isPointAvailable`은 첫 오디오 콘텐츠 카드 바인딩 시 포인트 사용 가능 표시 정책에 반영한다. +- `HomeAiCharacterItem.profileImage`는 `CharacterChatThumbnailView`의 실제 이미지 URL로 사용한다. +- `HomeAiCharacterItem.originalWorkTitle`이 null이면 기존 `CharacterChatThumbnailView`의 원작 없음 표시 정책에 맞춰 원작 영역을 유지하거나 숨기는 방식을 구현 계획에서 확정한다. +- `HomePopularCommunityPostItem.postId`는 커뮤니티 게시글 이동/식별 값으로 사용한다. +- `HomePopularCommunityPostItem.price`, `existOrdered`는 추천 페이지용 `FeedCommunityView`의 유료 미구매 UI 판정에 사용한다. +- `HomePopularCommunityPostItem.content`, `price`, `createdAt`, `likeCount`, `commentCount`는 인기 커뮤니티 feed 표시 데이터로 사용한다. + +### Activity Type 다국어 표시 +`HomeActiveCreatorItem.activityType`은 백엔드 enum code를 앱 표시용 enum으로 변환한다. + +#### Backend Codes +```kotlin +enum class RecommendedActivityType(val code: String) { + LIVE("LIVE"), + AUDIO("AUDIO"), + COMMUNITY("COMMUNITY"), + LIVE_REPLAY("LIVE_REPLAY") +} +``` + +#### Requirements +- `LIVE`, `LIVE_REPLAY`는 동일하게 `라이브`로 표시한다. +- `AUDIO`는 `오디오`로 표시한다. +- `COMMUNITY`는 `커뮤니티`로 표시한다. +- 표시 문구는 `strings.xml`, `values-en/strings.xml` 등 string resource를 통해 다국어 처리한다. +- 알 수 없는 code는 크래시시키지 않고 기본값 또는 미표시 정책을 구현 계획에서 확정한다. +- mapper 또는 enum 변환 로직은 local unit test로 검증한다. + +### 사업자 정보 접기/더보기 +Figma 최하단 사업자 정보 섹션(`218:2058`)은 외부 라이브러리 없이 TextView와 클릭 가능한 컨트롤로 구현한다. + +#### Requirements +- 기본 상태는 최대 3줄, 끝 말줄임표로 표시한다. +- 기본 상태에서 `더보기`를 터치하면 전체 텍스트를 표시한다. +- 펼침 상태에서 `접기`를 터치하면 다시 최대 3줄 말줄임 상태로 돌아간다. +- `더보기`/`접기` 표시 문구는 string resource로 관리한다. +- 외부 라이브러리를 사용하지 않는다. +- 사업자 정보 텍스트는 하드코딩 여부를 구현 계획에서 확인하되, 다국어/운영 변경 가능성이 있으면 string resource 또는 서버/설정값 사용을 우선 검토한다. +- 텍스트가 3줄 이하인 경우 `더보기`를 표시하지 않는다. + +### 상단 Bar와 추천 Tab +`HomeMainFragment`에서 추천 탭 선택 상태의 메인 홈 추천 페이지를 표시한다. + +#### Requirements +- title-bar는 기존 `view_title_bar_home`을 사용한다. +- title-bar 우측 아이콘은 왼쪽부터 `ic_bar_cash`, `ic_bar_search`, `ic_bar_bell` 순서로 배치한다. +- tab-bar는 기존 `TextTabBarView`를 사용한다. +- tab 항목은 `추천`, `랭킹`, `팔로잉` 순서로 구성한다. +- 이 PRD의 화면은 `추천` tab이 선택되었을 때 표시되는 page다. +- tab 표시 문구는 string resource로 관리한다. +- tab 전환은 글자 터치로만 처리하고, 좌우 swipe 또는 `ViewPager` 기반 전환은 제공하지 않는다. +- `TextTabBarView` 아래에는 이번 범위의 추천 page view만 구성한다. +- `랭킹`, `팔로잉` page는 이번 PRD에서 Fragment나 `VISIBLE`/`GONE` 대상 View로 미리 만들지 않는다. +- 세로 스크롤 시 title-bar와 `TextTabBarView`는 화면에 유지하고, `TextTabBarView` 아래 추천 content 영역만 스크롤되도록 구성한다. + +### 모두 팔로우 하기 +`최근 응원이 많은 크리에이터`, `장르별 크리에이터` 섹션에는 모두 팔로우 버튼을 제공한다. + +#### API Requirements +- API는 `POST /api/v2/home/recommendations/creators/follow`를 사용한다. +- request body는 `FollowRecommendedCreatorsRequest`를 사용한다. +- 기존 API에 추가하지 않고, 홈 추천 전용 신규 API 인터페이스에 포함한다. +- API 호출이 정상 완료되고 response가 success이면 버튼 상태를 `모두 팔로우 완료`로 변경한다. +- 실패 시 기존 화면의 toast/error 표시 패턴을 따른다. + +#### Request Contract +```kotlin +data class FollowRecommendedCreatorsRequest( + val creatorIds: List +) +``` + +#### Request Requirements +- `creatorIds`에는 사용자가 터치한 `모두 팔로우 하기` 버튼이 속한 섹션의 크리에이터 id 목록을 전달한다. +- `최근 응원이 많은 크리에이터` 섹션에서는 `cheerCreators.creatorId` 목록을 전달한다. +- `장르별 크리에이터` 섹션에서는 해당 장르 그룹의 `creators.creatorId` 목록을 전달한다. +- `creatorIds`가 빈 리스트인 경우 API를 호출하지 않고 버튼 상태를 변경하지 않는다. + +#### Button Requirements +- 기본 상태 버튼 문구는 `모두 팔로우 하기`다. +- 기본 상태 아이콘은 `ic_new_follow`를 사용한다. +- 완료 상태 버튼 문구는 `모두 팔로우 완료`다. +- 완료 상태 아이콘은 `ic_new_following`을 사용한다. +- 완료 상태 Figma reference: https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-9092&m=dev +- 완료 상태 버튼은 Figma `24:9092` 기준 white background, capsule radius `100dp`, horizontal/vertical padding `12dp`, icon `20dp`, icon/text gap `6dp`, Pretendard Medium `16sp`, black text를 기준으로 한다. +- 문구는 string resource로 관리한다. +- 버튼 상태는 API success 후 중복 호출되지 않도록 disabled 또는 click 무시 상태로 전환한다. + +### 추천 페이지 위젯 수정 +추천 페이지에 표시되는 기존 widget은 화면 요구사항에 맞춰 최소 범위로 확장한다. 별도 PRD로 분리하지 않고, 메인 홈 추천 UI의 표시 계약으로 이 문서에 함께 기록한다. + +#### FeedCommunityView Requirements +- 추천 페이지의 인기 커뮤니티는 `HomePopularCommunityPostItem`을 `FeedCommunityView` 또는 `FeedAdapter`의 Community variant로 매핑한다. +- 기존 keyword 영역은 추천 페이지에서는 제거한다. +- `HomePopularCommunityPostItem.content`는 본문 영역에 표시한다. +- `HomePopularCommunityPostItem.imageUrl`이 있으면 본문 아래 이미지 영역을 표시한다. +- 이미지 영역은 Figma `309:19774`, `309:19775` 기준 `346dp x 236dp`, radius `14dp`, centerCrop을 기준으로 한다. +- `imageUrl`이 null이면 이미지 영역은 `GONE` 처리한다. +- `audioUrl`은 이번 추천 카드에서 별도 플레이어 UI를 만들지 않고, 게시글 상세 이동 또는 후속 오디오 정책에서 사용한다. +- `price > 0 && existOrdered == false`이면 유료 미구매 커뮤니티 포스트로 판단한다. +- 유료 미구매 커뮤니티 포스트는 Figma `309:19774` 기준으로 이미지 위에 blur/lock overlay와 가격 capsule을 표시한다. +- 가격 capsule에는 `HomePopularCommunityPostItem.price`를 표시한다. +- `price > 0 && existOrdered == true`이면 유료 구매 완료 포스트로 판단한다. +- `price == 0`이면 무료 포스트로 판단한다. +- 유료 구매 완료 포스트 또는 무료 포스트는 Figma `309:19775`의 이미지 표시 상태를 기준으로 하되, Figma에 있는 `구매완료` 버튼/태그는 표시하지 않는다. +- 댓글 수와 좋아요 수는 기존 reaction row를 유지한다. +- creator profile, creator name, createdAt, body, reaction row typography와 색상은 기존 `FeedCommunityView` 기준을 유지한다. + +#### FeedCommunityView Figma References +- 유료이고 구매하지 않은 UI: https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=309-19774&m=dev +- 유료인데 구매함 또는 무료 UI: https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=309-19775&m=dev + +#### AudioContentCardView Requirements +- 첫 오디오 콘텐츠는 `v2.widget.AudioContentCardView`를 사용한다. +- `HomeFirstAudioContentItem.isPointAvailable == true`이면 `ic_content_tag_point`를 쓰는 `ImageView`를 표시한다. +- `HomeFirstAudioContentItem`에서는 오리지널 여부를 판단하지 않는다. +- 오리지널 판단 변수가 없으므로 `ic_content_tag_original`을 쓰는 `ImageView`는 항상 `GONE` 처리한다. +- `HomeFirstAudioContentItem.price == 0`이면 무료 콘텐츠로 판단하고 `무료` TextView를 `VISIBLE`로 표시한다. +- `HomeFirstAudioContentItem.price > 0`이면 `무료` TextView를 `GONE` 처리한다. +- `HomeFirstAudioContentItem`으로 표시하는 아이템은 모두 크리에이터의 첫 콘텐츠이므로 `FIRST` 글자를 포함하는 `LinearLayout`을 항상 표시한다. +- 위 조건에 해당하지 않는 태그는 모두 `GONE` 처리한다. +- 기존 `AudioContentCardView`의 `AudioContentTag.Original`, `AudioContentTag.Point`, `AudioContentTag.First`, `AudioContentTag.Free` 계약을 우선 재사용한다. +- `HomeFirstAudioContentItem` 응답에는 오리지널 여부 필드가 없으므로 오리지널 태그는 표시하지 않는다. + +### 섹션 구성과 UI 재사용 도식 +Figma `24:5514` 기준 홈 추천 화면은 아래 순서로 배치한다. 제외 섹션은 구현하지 않는다. + +```text +HomeRecommendation 화면 +├─ 상단 title-bar / tab-bar / nav +│ ├─ title-bar: view_title_bar_home +│ │ └─ right icons: ic_bar_cash, ic_bar_search, ic_bar_bell +│ └─ tab-bar: TextTabBarView(추천 selected, 랭킹, 팔로잉) +└─ 추천 content scroll area + ├─ 라이브 가로 목록: lives + │ ├─ Figma instance: live, more + │ ├─ 재사용: v2.widget.livethumbnail.LiveThumbnailSimpleView + │ └─ 신규: 더보기 item 또는 기존 more UI 대응 adapter item 필요 + ├─ 배너 캐러셀: banners + │ ├─ Figma instance: banner + │ └─ 재사용: v2.widget.banner.BannerView + ├─ 최근 활동한 크리에이터: recentlyActiveCreators + │ ├─ Figma instance: section-title, list-act + │ ├─ 재사용: view_section_title + │ └─ 신규: list-act 형태의 최근 활동 카드 UI 필요 + ├─ 최근 데뷔한 크리에이터: recentDebutCreators + │ ├─ Figma instance: section-title, creater + │ ├─ 재사용: view_section_title + │ └─ 신규: 최근 데뷔 크리에이터 카드 UI + ├─ 첫 오디오 콘텐츠: firstAudioContents + │ ├─ Figma instance: contents, profile + │ ├─ 재사용: v2.widget.AudioContentCardView + │ └─ 신규: 카드 하단 profile 결합 adapter item 필요 여부 확인 + ├─ AI 캐릭터: aiCharacters + │ ├─ Figma instance: section-title, chat-thumbnail + │ ├─ 재사용: view_section_title + │ └─ 재사용: v2.widget.characterchatthumbnail.CharacterChatThumbnailView + ├─ 장르별 크리에이터: genreCreators + │ ├─ Figma instance: section-title, profile, button-capsule + │ ├─ 재사용: view_section_title + │ └─ 신규: 장르 그룹 카드, 2행 profile grid, 모두 팔로우 버튼 + ├─ 추천 필모그래피 + │ └─ 제외: 만들지 않음 + ├─ 최근 응원이 많은 크리에이터: cheerCreators + │ ├─ Figma instance: section-title, profile, button-capsule + │ ├─ 재사용: view_section_title + │ └─ 신규: profile grid, 모두 팔로우 버튼 + ├─ 또 다른 모습 + │ └─ 제외: 만들지 않음 + ├─ 인기 커뮤니티: popularCommunityPosts + │ ├─ Figma instance: section-title, feed + │ └─ 재사용/수정: v2.widget.feed.FeedCommunityView 또는 FeedAdapter Community variant + └─ 사업자 정보 + └─ 신규: 3줄 말줄임 + 더보기/접기 TextView 영역 +``` + +#### Section Mapping +| API field | Figma node range | 표시 섹션 | 재사용 후보 | 신규 필요 여부 | +| --- | --- | --- | --- | --- | +| `lives` | `24:5516` | 라이브 가로 목록 | `LiveThumbnailSimpleView` | `more` item 확인 필요 | +| `banners` | `24:5525` | 배너 캐러셀 | `BannerView` | 없음 | +| `recentlyActiveCreators` | `24:5529` | 방금 활동한 크리에이터 | `view_section_title` | `list-act` 신규 필요 | +| `recentDebutCreators` | `24:5534` | 최근 데뷔한 크리에이터 | `view_section_title` | 신규 카드 필요 | +| `firstAudioContents` | `24:5539` | 첫 오디오 콘텐츠 | `AudioContentCardView` | 태그 조건 매핑 확인 필요 | +| `aiCharacters` | `24:5551` | AI 캐릭터 | `view_section_title`, `CharacterChatThumbnailView` | DTO mapping 필요 | +| `genreCreators` | `24:5611`, `24:5636` 후보 | 장르별 크리에이터 | `view_section_title` | 그룹 카드, 모두 팔로우 버튼 신규 필요 | +| `cheerCreators` | `24:5636` 후보 | 최근 응원이 많은 크리에이터 | `view_section_title` | profile grid, 모두 팔로우 버튼 신규 필요 | +| `popularCommunityPosts` | `24:5645`, `309:19774`, `309:19775` | 인기 커뮤니티 | `view_section_title`, `FeedCommunityView` | 이미지/유료 상태 UI 수정 필요 | +| 사업자 정보 | `218:2058` | 사업자 정보 | 없음 | 접기/더보기 신규 필요 | + +### 제외 섹션 +- Figma `24:5557` 근처의 `추천 필모그래피`로 판단되는 카드형 시리즈/필모그래피 영역은 구현하지 않는다. +- Figma `24:5602` 근처의 `또 다른 모습`으로 판단되는 콘텐츠 grid 영역은 구현하지 않는다. +- 제외 섹션과 연결되는 서버 필드가 현재 응답에 없으므로 adapter item도 만들지 않는다. + +#### Edge Cases +- 모든 추천 리스트가 비어 있으면 홈 추천 영역은 비어 있는 섹션을 노출하지 않고, 기존 홈 화면 empty 정책을 따른다. +- 특정 섹션만 비어 있으면 해당 section-title과 목록 전체를 숨긴다. +- 긴 제목, 닉네임, 커뮤니티 본문은 각 widget의 말줄임 정책을 따른다. +- `HomeAiCharacterItem.profileImage`가 null이면 호출부 이미지 로딩 정책에 따라 placeholder 또는 빈 상태를 표시한다. +- `HomePopularCommunityPostItem.imageUrl` 또는 `audioUrl`이 null이어도 커뮤니티 feed는 본문/반응 수 중심으로 표시 가능해야 한다. +- 유료 미구매 포스트의 가격 capsule은 `price` 값을 사용하고, 유료 구매 완료 또는 무료 포스트에는 가격 capsule을 표시하지 않는다. +- `HomeBannerItem.type`별 이동 정책은 서버 정의와 기존 딥링크/이동 정책을 확인한 뒤 구현한다. + +--- + +## 8. UX / UI Expectations +- Figma `24:5514`의 세로 순서, 좌우 여백, 가로 스크롤 형태를 유지한다. +- `HomeMainFragment`의 추천 tab page로 표시하며, `TextTabBarView`에서 추천 선택 상태가 명확해야 한다. +- `ViewPager`나 swipe로 tab을 넘기는 동작은 없어야 하며, tab 글자 터치로만 전환한다. +- 세로 스크롤 중에도 `TextTabBarView`가 화면에 남아 있어야 하므로, 스크롤 컨테이너는 `TextTabBarView` 아래 content 영역에만 적용한다. +- 상단 라이브 목록, 배너, 추천 카드들은 어두운 홈 배경 위에서 기존 v2 widget 색상/typography/radius를 유지한다. +- 각 섹션 제목은 기존 `view_section_title`을 사용한다. +- `최근 응원이 많은 크리에이터`, `장르별 크리에이터`의 모두 팔로우 버튼은 success 전/후 상태가 명확히 구분되어야 한다. +- 인기 커뮤니티는 keyword 없이 본문, 선택적 이미지, 유료 잠금 상태, reaction row를 보여야 한다. +- 유료 미구매 커뮤니티 이미지는 내용을 바로 읽을 수 없도록 blur/lock overlay가 적용되어야 한다. +- 유료 구매 완료 또는 무료 커뮤니티 이미지는 overlay 없이 표시하고, `구매완료` 태그는 표시하지 않는다. +- 가로 목록은 화면 밖 다음 item이 일부 보이도록 Figma의 peek 느낌을 유지한다. +- 커뮤니티 본문과 사업자 정보는 긴 텍스트가 화면 폭을 밀어내지 않아야 한다. +- 사업자 정보는 기본 3줄만 보여 홈 탐색을 방해하지 않고, 사용자가 원할 때 전체 내용을 확인할 수 있어야 한다. +- 터치 동작은 UI 컴포넌트 내부에서 목적지를 결정하지 않고 화면/adapter callback으로 위임한다. + +--- + +## 9. Technical Constraints +- 현재 프로젝트는 Android XML Views + Kotlin + ViewBinding 기반이므로 XML layout, RecyclerView/adapter, custom view 패턴을 우선한다. +- UI는 기존 `kr.co.vividnext.sodalive.v2.main.HomeMainFragment`에 구현한다. +- `HomeMainFragment`는 이번 범위에서 추천 content만 직접 구성하며, `ViewPager2`, `FragmentStateAdapter`, tab별 신규 Fragment 3개 구조는 도입하지 않는다. +- `TextTabBarView` 아래 추천 content 영역만 `NestedScrollView` 또는 동등한 단일 세로 스크롤 컨테이너로 구성한다. +- 신규 API/Repository/ViewModel 및 그와 연결된 하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다. +- 기존 `kr.co.vividnext.sodalive.v2.widget.*` 컴포넌트를 우선 재사용한다. +- 기존 API 인터페이스에 메서드를 추가하지 않고, 홈 추천 전용 신규 API 인터페이스를 만든다. +- 이미지 로딩 라이브러리를 새로 추가하지 않고 기존 호출부의 이미지 로딩 방식을 따른다. +- 외부 라이브러리를 추가하지 않는다. +- API 모델은 기존 네트워크 스택과 JSON 파서 규칙을 따른다. +- string resource 기반 다국어 처리를 적용하며, UI 표시 문자열을 Kotlin 코드에 직접 하드코딩하지 않는다. +- Figma에 있는 기존 widget과 다른 신규 UI는 구현 계획에서 파일 경로와 테스트 범위를 명시한 뒤 최소 범위로 작성한다. +- 신규 순수 변환 로직(activity type mapper, count/time formatter 등)은 가능하면 local unit test로 검증한다. + +--- + +## 10. Metrics +- PRD와 구현 계획에 Figma 섹션 배치, 재사용 widget, 신규 필요 컴포넌트가 한눈에 확인 가능하게 정리된다. +- `HomeRecommendationResponse`의 각 필드가 노출/제외/확인필요 중 하나로 분류된다. +- `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY` 표시 문자열이 string resource 기반으로 매핑된다. +- `HomeMainFragment`에서 `view_title_bar_home`, `TextTabBarView`, `view_section_title` 재사용이 확인된다. +- `FeedCommunityView`에서 keyword 영역이 추천 페이지에 노출되지 않고, 이미지와 유료 미구매 overlay가 표시된다. +- `AudioContentCardView`에서 `ic_content_tag_point`, `무료`, `FIRST` 태그가 조건에 맞게 표시되고, `ic_content_tag_original`은 표시되지 않는다. +- 제외 대상인 `추천 필모그래피`, `또 다른 모습` 섹션이 화면에 생성되지 않는다. +- 모두 팔로우 API success 후 버튼이 `모두 팔로우 완료`와 `ic_new_following` 상태로 변경된다. +- 사업자 정보는 기본 3줄 말줄임, 더보기 후 전체 표시, 접기 후 3줄 복귀가 동작한다. +- 기존 v2 widget 재사용 범위가 구현 diff에서 확인된다. +- 관련 unit test, resource merge, `./gradlew :app:testDebugUnitTest` 또는 구현 범위에 맞는 단일 테스트가 성공한다. + +--- + +## 11. Open Questions +- 홈 추천 API endpoint는 `/api/v2/home/recommendations`로 확정됐다. HTTP method는 `GET`으로 가정하되, 구현 전 백엔드 계약 또는 기존 `HomeApi` 패턴에서 최종 확인한다. +- 모두 팔로우 API는 `POST /api/v2/home/recommendations/creators/follow`, request body는 `FollowRecommendedCreatorsRequest(creatorIds: List)`로 확정됐다. +- 인증 token 처리 방식은 아직 제공되지 않았다. 구현 전 기존 `HomeApi` 확장 지점과 공통 인증 interceptor 사용 여부를 확인해야 한다. +- `genreCreators`와 `cheerCreators`의 profile grid는 기존 profile widget이 저장소에 있는지 추가 확인 후 재사용/신규를 결정한다. +- 배너 `type`별 이동 정책(`eventId`, `creatorId`, `seriesId`, `link`)은 기존 딥링크/화면 이동 규칙과 백엔드 code 목록 확인이 필요하다. +- `activityAt`, `releaseDate`, `createdAt`, `beginDateTime`의 표시 포맷은 기존 시간 formatter 재사용 여부를 구현 계획에서 확인한다. + +--- + +## 12. 검증 기록 +- 2026-06-01: `docs/prd/sample-prd.md`와 `docs/agent-guides/work-plan-docs.md`를 확인해 신규 문서 위치와 PRD 작성 형식을 맞췄다. +- 2026-06-01: Figma `24:5514`의 최상위 구조를 확인해 `live`, `banner`, `section-title`, `contents`, `chat-thumbnail`, `profile`, `feed`, 사업자 정보 섹션의 배치와 제외 후보 섹션을 PRD에 반영했다. +- 2026-06-01: 저장소의 기존 v2 widget 패키지(`banner`, `livethumbnail`, `characterchatthumbnail`, `feed`, `AudioContentCardView`)와 관련 PRD를 확인해 재사용 후보를 문서화했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 홈 추천 API URL을 `GET /api/v2/home/recommendations`로 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 신규 API 인터페이스 방침, `HomeMainFragment` 구현 위치, `view_title_bar_home`, `TextTabBarView`, `view_section_title` 재사용, 모두 팔로우 API/버튼 상태, 변경된 `HomeFirstAudioContentItem`, `HomeAiCharacterItem` 응답 계약을 PRD에 반영했다. +- 2026-06-01: 저장소에서 `HomeMainFragment`, `view_title_bar_home`, `view_section_title`, `TextTabBarView`, `ic_bar_cash`, `ic_bar_search`, `ic_bar_bell`, `ic_new_follow`, `ic_new_following` 존재를 확인했다. +- 2026-06-01: Figma `24:9092`를 확인해 모두 팔로우 완료 버튼의 white capsule, `20dp` icon, `6dp` gap, `16sp` medium text 요구사항을 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 모두 팔로우 request body `FollowRecommendedCreatorsRequest(creatorIds: List?)`와 섹션별 `creatorIds` 전달 정책을 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 모두 팔로우 request body의 `creatorIds`를 nullable이 아닌 `List`으로 변경하고 null 의미 확인 항목을 제거했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 모두 팔로우 API HTTP method를 `POST`로 확정해 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 `HomeRecommendationResponse.popularCommunities`를 `popularCommunityPosts`로 변경하고, `HomePopularCommunityPostItem` 응답 계약을 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 추천 페이지의 `FeedCommunityView` 수정 요구사항(keyword 제거, 이미지 추가, 유료 미구매 overlay, 구매완료 태그 제외)과 `AudioContentCardView` 태그 표시 정책을 PRD에 반영했다. +- 2026-06-01: Figma `309:19774`, `309:19775`의 design context와 screenshot을 확인해 유료 미구매/구매 또는 무료 커뮤니티 feed의 이미지, blur/lock overlay, reaction row 요구사항을 문서화했다. +- 2026-06-01: 저장소에서 `AudioContentCardView`가 `AudioContentTag.Original`, `Point`, `First`, `Free`와 `ic_content_tag_original`, `ic_content_tag_point`를 이미 지원하는 것을 확인했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 `HomeFirstAudioContentItem`으로 표시하는 모든 아이템은 첫 콘텐츠로 간주해 `FIRST` 태그를 항상 표시하도록 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 `HomePopularCommunityPostItem.content` 아래에 `price: Int`를 추가하고, 유료 미구매 가격 capsule 표시값으로 사용하도록 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 `HomeFirstAudioContentItem.title` 아래에 `price: Int`를 추가하고, `price == 0`이면 무료 태그를 표시하도록 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 `HomeFirstAudioContentItem`에서는 오리지널 여부를 판단하지 않고, 오리지널 태그를 항상 숨기는 정책으로 PRD에 반영했다. +- 2026-06-01: 사용자 추가 제공 정보에 따라 `popularCommunityPosts.price`와 `existOrdered` 조합으로 유료 미구매/유료 구매 완료/무료 포스트 UI를 판정하도록 PRD에 반영했다. +- 2026-06-02: 사용자 추가 제공 정보에 따라 `ViewPager`/swipe tab 전환을 제외하고, tab 글자 터치 전환만 허용하며, `TextTabBarView` 아래 추천 content 영역만 세로 스크롤되도록 요구사항을 반영했다.