# 메인 홈 추천 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과 화면 상태 구성 - [x] **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에 직접 노출하지 않는다. - [x] **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를 노출한다. - [x] **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 구현 - [x] **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만 스크롤된다. - [x] **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 바인딩 전 제거하거나 실제 상태 바인딩으로 대체한다. - [x] **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`. - [x] **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`도 `라이브`로 표시. - [x] **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 조건과 일치. - [x] **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 호출하지 않고 버튼 상태 유지. - [x] **Task 6.5: 방금 활동한 크리에이터 Figma 정합 수정** - 기준: Figma `24:5529` - 수정: `app/src/main/res/layout/item_home_recent_activity_creator.xml` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomePhase6Adapters.kt` - 생성: `app/src/main/res/drawable/bg_home_recent_activity_card.xml` - 생성: `app/src/main/res/drawable/bg_home_recent_activity_type.xml` - 구현 내용: - 세로 프로필형 카드를 `244dp x 76dp` 가로 capsule 카드로 변경 - profile image를 `52dp x 52dp`로 변경 - 닉네임은 `18sp` bold, activity type은 `gray_700` tag, 보조 텍스트는 `activityAt` 표시 - 검증: `HomeMainFragmentLayoutTest`에 Figma capsule 치수 회귀 테스트를 추가한다. - [x] **Task 6.6: 홈 추천 섹션 adapter 파일 분리** - 삭제: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomePhase6Adapters.kt` - 생성: `HomeLiveAdapter.kt`, `HomeBannerBinder.kt`, `HomeRecentActivityCreatorAdapter.kt`, `HomeRecentDebutCreatorAdapter.kt`, `HomeFirstAudioAdapter.kt`, `HomeAiCharacterAdapter.kt`, `HomeCreatorProfileAdapter.kt`, `HomeGenreCreatorAdapter.kt`, `HomeCheerCreatorAdapter.kt`, `HomeFollowAllButtonBinder.kt`, `HomeRecyclerItemLayoutParams.kt` - 구현 내용: - 한 파일에 모여 있던 섹션별 adapter/binder를 섹션 단위 파일로 분리 - 공통 `RecyclerView.LayoutParams` 생성 로직은 package-private helper로 분리 - UI 동작과 바인딩 계약은 변경하지 않음 - 검증: `./gradlew :app:compileDebugKotlin`, `./gradlew :app:testDebugUnitTest --tests kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest`, `./gradlew :app:ktlintCheck`를 실행한다. - [x] **Task 6.7: 라이브 섹션 Figma 정합 수정** - 기준: Figma `24:5516` - 수정: `app/src/main/res/layout/fragment_v2_main_home.xml` - 수정: `app/src/main/res/values/dimens.xml` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeLiveAdapter.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 구현 내용: - `rv_home_lives` 높이는 아이템 높이에 따라가도록 `wrap_content` 유지 - `rv_home_lives` 시작 padding `20dp` 적용 - 라이브 아이템 간격을 Figma 기준 `14dp`로 적용 - 라이브는 최대 20개까지 표시하고, 20개 초과 시 `HomeLiveAdapter` 마지막 item으로 `58dp x 102dp` 검은 배경 `전체` item 추가 - 검증: `HomeMainFragmentLayoutTest`에 live row `wrap_content`, live adapter item gap, 20개 초과 시 `전체` item 추가 회귀 테스트를 추가한다. - [x] **Task 6.8: 최근 데뷔한 크리에이터 Figma 정합 수정** - 기준: Figma `24:5534` - 수정: `app/src/main/res/layout/item_home_recent_debut_creator.xml` - 수정: `app/src/main/res/layout/fragment_v2_main_home.xml` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeRecentDebutCreatorAdapter.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 생성: `app/src/main/res/drawable/bg_home_recent_debut_card.xml` - 생성: `app/src/main/res/drawable/bg_home_recent_debut_dim_gradient.xml` - 구현 내용: - 기존 원형 profile item을 `205dp x 259dp` 이미지 카드로 변경 - 카드 radius `14dp`, 하단 dim gradient, 닉네임 `24sp` bold 중앙 정렬 적용 - `rv_home_recent_debut_creators` 시작 padding `14dp`, item gap `4dp` 적용 - 검증: `HomeMainFragmentLayoutTest`에 recent debut 카드 치수와 목록 간격 회귀 테스트를 추가한다. - [x] **Task 6.9: 처음부터 함께 성장! Figma 정합 수정** - 기준: Figma `24:5539` - 생성: `app/src/main/res/layout/item_home_first_audio_content.xml` - 수정: `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/HomeRecyclerItemLayoutParams.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 구현 내용: - `AudioContentCardView` 재사용 대신 첫 오디오 전용 item layout을 사용한다. - 썸네일을 `185dp x 185dp`, radius `14dp` 카드로 구성한다. - creator profile을 `42dp x 42dp`, creator nickname `14sp`로 구성하고 상세 시간은 표시하지 않는다. - `rv_home_first_audio_contents` 시작 padding `14dp`, item gap `4dp`를 적용한다. - `First`, `Point`, `Free` tag 표시 조건은 기존 UI model의 `tags`를 그대로 사용한다. - 검증: `HomeMainFragmentLayoutTest`에 first audio 카드 치수, 목록 간격, tag visibility 회귀 테스트를 추가한다. - [x] **Task 6.10: AI 캐릭터 섹션 Figma 정합 수정** - 기준: Figma `24:5551` - 확인: `app/src/main/res/layout/view_character_chat_thumbnail.xml` - 수정: `app/src/main/res/layout/fragment_v2_main_home.xml` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeAiCharacterAdapter.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 구현 내용: - `크리에이터와 이야기를 나눠요!` 목록 시작/끝 padding을 `14dp`로 적용한다. - AI 캐릭터 item gap을 `4dp`로 적용한다. - adapter에서 `view_character_chat_thumbnail.xml`의 root `185dp x wrap_content` 치수를 generic layout params로 덮어쓰지 않는다. - 검증: `HomeMainFragmentLayoutTest`에 AI 캐릭터 목록 spacing과 adapter item dimension 회귀 테스트를 추가한다. - [x] **Task 6.11: 장르 크리에이터 모두 팔로우 버튼 Figma 정합 수정** - 기준: Figma `24:5611`, 완료 버튼 `24:9092` - 수정: `app/src/main/res/layout/view_home_follow_all_button.xml` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowAllButtonBinder.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 구현 내용: - 기본 상태는 `모두 팔로우하기`, `ic_new_follow`, 투명 배경과 흰색 30% stroke, 흰색 텍스트를 적용한다. - 완료 상태는 `모두 팔로우 완료`, `ic_new_following`, 흰색 배경, 검은색 텍스트를 적용한다. - 완료 상태에서는 터치해도 callback을 호출하지 않는다. - 검증: `HomeMainFragmentLayoutTest`에 모두 팔로우 기본/완료 상태와 완료 상태 no-op 회귀 테스트를 추가한다. - [x] **Task 6.12: 장르의 크리에이터 Figma 정합 추가 수정** - 기준: Figma `24:5611` - 수정: `app/src/main/res/layout/fragment_v2_main_home.xml` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeGenreCreatorAdapter.kt` - 생성 후보: `app/src/main/res/layout/item_home_genre_creator_group.xml` - 생성 후보: `app/src/main/res/layout/item_home_genre_creator_profile.xml` - 생성 후보: `app/src/main/res/drawable/bg_home_genre_creator_group.xml` - 구현 내용: - `genreCreators: List`를 그룹 단위 page로 표시한다. - 각 장르 page는 `374dp` 폭, `14dp` padding/radius/gap, dark-blue gradient 배경을 적용한다. - 각 page 내부에 장르 제목, 8명 creator profile grid, 모두 팔로우 버튼을 포함한다. - 전체 장르 page 목록은 free scroll이 아니라 `PagerSnapHelper` 기반 banner snapping으로 이동한다. - 빈 creator group은 숨기고 최대 5개 장르 group만 표시한다. - 검증: `HomeMainFragmentLayoutTest`에 genre page 치수/gradient/grid/snap/helper/filtering 회귀 테스트를 추가한다. - [x] **Task 6.13: 최근 응원이 많은 크리에이터 Figma 정합 추가 수정** - 기준: Figma `24:5636` - 수정: `app/src/main/res/layout/fragment_v2_main_home.xml` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCheerCreatorAdapter.kt` - 생성 후보: `app/src/main/res/layout/item_home_cheer_creator_group.xml` - 생성 후보: `app/src/main/res/drawable/bg_home_cheer_creator_group.xml` - 구현 내용: - 섹션은 특정 width로 고정하지 않고 화면 폭을 채우며 horizontal padding으로 card 폭을 조정한다. - `최근 응원이 많은 크리에이터` card는 `14dp` padding/radius/gap, `gray_900` 배경을 적용한다. - creator profile은 장르 크리에이터와 유사한 simple profile grid로 표시하고 최대 8명만 노출한다. - 모두 팔로우 버튼은 card 내부 하단에 포함하고 기존 기본/완료 상태를 유지한다. - 검증: `HomeMainFragmentLayoutTest`에 cheer card/grid/width/follow callback 회귀 테스트를 추가한다. --- ### Phase 7: FeedCommunityView 추천 페이지 확장 - [x] **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` - [x] **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를 숨긴다. - [x] **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`. - [x] **Task 7.4: 실기기 확인용 인기 커뮤니티 샘플 데이터 주입** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` - 구현 내용: - 기존 `phase6SampleContent()`의 `popularCommunityPosts` 빈 목록을 무료/유료 미구매/구매 완료 샘플 3건으로 교체 - 추천용 커뮤니티 샘플은 `showKeyword = false`로 유지 - 유료 미구매 샘플은 lock overlay와 가격 capsule 확인용으로 구성 - 주의: Phase 9 실제 API/ViewModel 바인딩 시 임시 샘플 데이터는 실제 상태 바인딩으로 교체한다. - 검증: 실제 기기에서 인기 커뮤니티 섹션과 카드 상태 3종을 육안 확인할 수 있다. - [x] **Task 7.5: 커뮤니티 이미지 고정 크기 제거와 동적 비율 적용** - 수정: `app/src/main/res/layout/view_feed_community.xml` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedCommunityView.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/feed/FeedViewTest.kt` - 구현 내용: - `FeedCommunityView` root와 이미지 container의 고정 폭을 제거하고 `match_parent`로 변경 - 이미지 container 높이는 실제 card content width 기준으로 Figma `346:236` 비율을 유지해 계산 - Kotlin `clipToOutline`/`ViewOutlineProvider.setRoundRect(...)` clipping 계약 유지 - 검증: 인기 커뮤니티 이미지 영역이 화면 폭에 맞춰 계산되어 고정 폭 때문에 잘리지 않는다. --- ### Phase 8: 사업자 정보 접기/더보기 구현 - [x] **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 정책 연결 - [x] **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 처리. - [x] **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 컴포넌트 내부에서 목적지를 결정하지 않는다. - [x] **Task 9.3: 홈 추천 응답 스키마 변경 대응** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeRecommendationModels.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationMappers.kt` - 수정: `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` - 수정: `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/HomeFirstAudioAdapter.kt` - 수정: `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` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 구현 내용: - 변경된 `HomeLiveItem(roomId, creatorNickname, creatorProfileImage)`을 라이브 UI model과 `LiveThumbnailSimpleView` 바인딩에 반영한다. - 변경된 `HomeBannerItem(imageUrl, eventItem, creatorId, seriesId, link)`을 배너 UI model과 `BannerView` 바인딩에 반영한다. - 변경된 `HomeActiveCreatorItem(creatorNickname, creatorProfileImage, activityType, activityAt, targetId)`을 최근 활동 UI model/mapper에 반영하고, 응답에서 제거된 `creatorId` 기반 프로필 이동은 제거한다. - 변경된 `HomeCreatorItem(creatorNickname, creatorProfileImage)`, `HomeFirstAudioContentItem`의 `releaseDate` 제거, `HomeGenreCreatorGroupItem.genreName`을 mapper/UI model에 반영한다. - 실제 UI에서 사용하지 않거나 응답에서 더 이상 제공하지 않는 nullable placeholder 값은 UI model에 추가하지 않는다. - 검증: - `HomeMainFragmentLayoutTest`에 변경된 응답 필드 기반 mapper 회귀 테스트를 추가한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home recommendation mapper uses changed response fields"`로 RED/GREEN을 확인한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`로 회귀를 확인한다. - [x] **Task 9.4: 홈 추천 이미지 캐시 ANR 대응** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/common/ImageLoaderProvider.kt` - 생성: `app/src/test/java/kr/co/vividnext/sodalive/common/ImageLoaderProviderTest.kt` - 구현 내용: - 홈 추천 API 바인딩 후 다수 이미지 로딩 시 `unexpected journal header` 경고와 ANR이 발생하는 현상을 `ImageLoaderProvider`의 OkHttp cache journal 충돌 가능성으로 분리한다. - 기존 `cacheDir/image_cache`를 legacy OkHttp image cache로 보고 앱 시작 시 삭제한다. - 신규 이미지 loader OkHttp cache는 `cacheDir/coil_image_cache`로 분리해 손상된 기존 journal을 다시 읽지 않도록 한다. - 검증: - `ImageLoaderProviderTest`에 legacy cache directory와 신규 cache directory가 분리되는 회귀 테스트를 추가한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.common.ImageLoaderProviderTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`로 회귀를 확인한다. - [x] **Task 9.5: BannerView 홈 추천 배너 모델 직접 바인딩** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerAdapter.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerItem.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeBannerBinder.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerViewTest.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 구현 내용: - `BannerItem`을 `HomeRecommendationBannerUiModel`과 동일한 배너 payload(`imageUrl`, `eventItem`, `creatorId`, `seriesId`, `link`)를 담는 위젯 전용 모델로 변경한다. - `BannerView`/`BannerAdapter`는 공용 위젯 독립성을 유지하기 위해 `BannerItem`만 사용하고, `HomeBannerBinder`에서 홈 추천 배너 UI model과 `BannerItem`을 변환한다. - `HomeBannerBinder`의 synthetic id 매칭 로직을 제거하고 클릭 시 `BannerItem` payload를 다시 `HomeRecommendationBannerUiModel`로 전달한다. - 2개 이상 배너에서 `BannerAdapter.itemCount == Int.MAX_VALUE`가 되더라도 `submitItems()`와 size/preview 갱신이 `notifyItemRangeInserted(0, Int.MAX_VALUE)` 또는 `notifyItemRangeChanged(0, Int.MAX_VALUE)` 같은 huge range notify를 호출하지 않도록 한다. - 단일 배너 상태에서는 `notifyItemInserted`/`notifyItemChanged`/`notifyItemRemoved`를 사용하고, `Int.MAX_VALUE` 가상 adapter 상태에서만 `NotifyDataSetChanged` lint를 좁은 helper에 제한해 전체 갱신을 수행한다. - 검증: - `BannerViewTest`에 carousel 설정 시 max range insert/change notify를 호출하지 않는 회귀 테스트와 단일 배너 specific notify 회귀 테스트를 추가한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`로 회귀를 확인한다. - [x] **Task 9.6: 홈 추천 프로필 이미지 원형 처리와 상대 시간 표시 적용** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/explorer/profile/creator_community/GetCommunityPostListResponse.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationMappers.kt` - 수정: `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/HomeGenreCreatorAdapter.kt` - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeCheerCreatorAdapter.kt` - 수정: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt` - 구현 내용: - 기존 `GetCommunityPostListResponse.relativeTimeText(context)`가 사용하는 UTC 서버 시간 파싱과 상대 시간 문자열 계산을 DTO 전용 확장 함수에서 재사용 가능한 일반 함수 또는 공통 확장 함수로 분리한다. - `HomeActiveCreatorItem.activityAt`은 UTC 값을 디바이스 기본 타임존 기준으로 해석한 뒤 time-ago timestamp 문자열로 변환해 `HomeRecommendationRecentlyActiveCreatorUiModel.activityAt`에 매핑한다. - `HomePopularCommunityPostItem.createdAt`도 같은 상대 시간 변환 함수를 사용해 `FeedItem.Community.createdAtText`에 매핑한다. - `방금 활동한 크리에이터`, `[장르] 크리에이터`, `최근 응원이 많은 크리에이터`의 creator profile image는 기존 Coil image loading 패턴에 `CircleCropTransformation` 또는 동일한 원형 clipping 방식을 적용한다. - 상대 시간 변환 실패 시 크래시 없이 기존 커뮤니티 포스트 정책과 동일하게 `character_comment_time_just_now` 문자열을 표시한다. - 검증: - `HomeMainFragmentLayoutTest`에 최근 활동 `activityAt`과 인기 커뮤니티 `createdAt`이 UTC 입력에서 상대 시간 문자열로 매핑되는 회귀 테스트를 추가한다. - `HomeMainFragmentLayoutTest`에 최근 활동/장르/응원 creator profile image가 원형 처리 계약을 적용하는 회귀 테스트를 추가한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`로 회귀를 확인한다. --- ### Phase 10: 배너 이동 정책 연결 - [x] **Task 10.1: 홈 추천 배너 클릭 라우팅 구현** - 수정: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt` - 확인: 기존 `EventDetailActivity`, `UserProfileActivity`, `SeriesDetailActivity` 라우팅 extra 계약 - 구현 내용: - 배너 응답에서 `type` 값은 제거되었으므로 사용하지 않는다. - `eventItem != null`이면 `EventDetailActivity`로 이동한다. - `creatorId != null`이면 `UserProfileActivity`로 이동한다. - `seriesId != null`이면 `SeriesDetailActivity`로 이동한다. - `link != null && link가 웹 URL`이면 `Intent.ACTION_VIEW`를 사용해 외부 웹 URL로 이동한다. - `link != null && link가 내부 딥링크`이면 기존 딥링크 실행 정책에 따라 앱 내부 딥링크를 실행한다. - 여러 이동 대상 값이 함께 내려오면 위 순서대로 우선 처리한다. - 웹 URL도 내부 딥링크도 아닌 `link`는 이동하지 않는다. - 검증: - `HomeMainFragmentLayoutTest` 또는 라우팅 단위 테스트에 배너 목적지별 Intent 계약 회귀 테스트를 추가한다. - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`로 회귀를 확인한다. --- ### Phase 11: 최종 검증과 문서 기록 - [ ] **Task 11.1: 단위 테스트와 빌드 검증** - 실행: `./gradlew :app:testDebugUnitTest` - 실행: `./gradlew :app:compileDebugKotlin` - 실행: `./gradlew :app:mergeDebugResources` - 실행: `./gradlew :app:ktlintCheck` - 기대 결과: 모두 성공. - [ ] **Task 11.2: 수동 확인** - 확인 항목: - 추천 tab 선택 상태 - tab 전환은 글자 터치로만 동작하고 swipe 전환은 없음 - 세로 스크롤 시 title-bar와 `TextTabBarView`는 유지되고 `TextTabBarView` 아래 추천 content만 스크롤됨 - 섹션 순서가 PRD 도식과 일치 - 제외 섹션 미노출 - 빈 리스트 섹션 숨김 - 첫 오디오 태그 조건 - activity type 다국어 문자열 - 모두 팔로우 success 후 완료 상태 - 배너 목적지별 클릭 이동 - 인기 커뮤니티 유료/무료/구매 완료 이미지 상태 - 사업자 정보 더보기/접기 - 기대 결과: PRD Metrics 항목 충족. - [ ] **Task 11.3: 검증 기록 누적** - 수정: `docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md` - 수정: `docs/20260601_메인_홈_추천_UI와_API_연동/prd.md` - 구현 내용: 실행 명령, 결과, 실패 시 원인과 후속 조치를 문서 하단 `검증 기록`에 누적한다. - 주의: 기존 검증 기록은 삭제하거나 덮어쓰지 않는다. --- ## 확인 완료 사항 - 배너 이동은 `type` 없이 `eventItem`, `creatorId`, `seriesId`, `link` 값 기준으로 처리하며, `link`는 웹 URL이면 외부 브라우저, 내부 딥링크이면 앱 내부 딥링크로 실행한다. 세부 구현은 Phase 10에서 진행한다. - 시간 표시는 `RelativeTimeFormatter`에 재사용 가능한 함수가 있으므로 `activityAt`, `createdAt` 변환에 해당 formatter를 사용한다. - 모두 팔로우 API는 success response의 `data` 형태에 의존하지 않고 `ApiResponse.success == true`만으로 완료 처리한다. - 사업자 정보 텍스트는 서버/설정값이 아니라 `strings.xml`에 정의된 문자열을 사용한다. --- ## 검증 기록 - 2026-06-05: Task 10.1 구현으로 홈 추천 배너 클릭 라우팅을 연결했다. `eventItem`은 `EventDetailActivity` + `Constants.EXTRA_EVENT`, `creatorId`는 `UserProfileActivity` + `Constants.EXTRA_USER_ID`, `seriesId`는 `SeriesDetailActivity` + `Constants.EXTRA_SERIES_ID`, `link`는 `Intent.ACTION_VIEW`로 실행한다. 여러 대상이 함께 내려오면 `eventItem` → `creatorId` → `seriesId` → `link` 순서로 우선 처리한다. RED 검증은 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home banner route uses event creator series link priority"`에서 `HomeRecommendationBannerRoute`, `toHomeRecommendationBannerRoute()` 미정의 컴파일 실패로 확인했다. 구현 후 동일 테스트는 최초 `:app:kspDebugKotlin`의 `java.io.EOFException`으로 중단되었고, `--rerun-tasks` 재실행에서 BUILD SUCCESSFUL을 확인했다. 이후 내부 딥링크 판별 누락 지적에 따라 `BuildConfig.APPSCHEME` custom scheme과 `https://${BuildConfig.APPSCHEME}.onelink.me` app link만 내부 딥링크로 인정하고, `mailto:` 같은 외부 custom scheme은 이동하지 않도록 보강했다. RED 검증은 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home banner route maps creator series web and internal deeplink" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home banner route ignores blank malformed and scheme-less links"`에서 2개 테스트 실패로 확인했고, 수정 후 동일 명령은 BUILD SUCCESSFUL을 확인했다. - 2026-06-05: Task 10.1 최종 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` 속성 deprecation warning만 출력되었고 신규 실패는 없었다. - 2026-06-05: Task 10.1 리뷰 반려 대응으로 라우팅 결정 테스트에 더해 실제 `Intent` 생성 계약 테스트를 추가했다. `HomeRecommendationBannerRoute.toHomeRecommendationBannerIntent(context)`를 추가해 `EventDetailActivity`/`UserProfileActivity`/`SeriesDetailActivity` component와 `Constants.EXTRA_EVENT`/`Constants.EXTRA_USER_ID`/`Constants.EXTRA_SERIES_ID` extra, `Intent.ACTION_VIEW` link data를 검증하도록 했다. 또한 `creatorId + seriesId + link`, `seriesId + link` 조합 우선순위 테스트를 추가했다. RED 검증은 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home banner route creates activity and link intents with expected extras"`에서 `toHomeRecommendationBannerIntent()` 미정의 컴파일 실패로 확인했고, 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home banner route creates activity and link intents with expected extras" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home banner route uses creator over series and series over link priority"`는 BUILD SUCCESSFUL을 확인했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 재실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-05: 사용자 추가 요청에 따라 배너 `link` 값이 내부 딥링크이면 기존 딥링크 실행 정책에 따라 앱 내부 딥링크를 실행하도록 `Task 10.1`과 확인 완료 사항에 반영했다. 문서 변경만 수행했으므로 Gradle 검증은 실행하지 않고 문서 재읽기로 확인한다. - 2026-06-05: 사용자 요청에 따라 `구현 중 확인 필요` 섹션을 `확인 완료 사항`으로 정리하고, 배너 `type` 제거 후 `eventItem`/`creatorId`/`seriesId`/웹 URL `link` 기준 이동 정책을 새 `Phase 10: 배너 이동 정책 연결`로 추가했다. 기존 `Phase 10: 최종 검증과 문서 기록`은 `Phase 11`로 변경하고 task 번호와 수동 확인 항목에 배너 목적지별 클릭 이동 확인을 반영했다. 문서 변경만 수행했으므로 Gradle 검증은 실행하지 않았고, `plan-task.md` 재읽기로 Phase 10/11 및 확인 완료 사항 반영을 확인했다. - 2026-06-05: 사용자 요청에 따라 Phase 9에 `Task 9.6: 홈 추천 프로필 이미지 원형 처리와 상대 시간 표시 적용`을 추가했다. 기존 `GetCommunityPostListResponse.relativeTimeText(context)`가 `dateUtc`를 UTC로 파싱해 디바이스 타임존 기준 상대 시간 문자열을 만들고, 홈 추천 mapper가 현재 `activityAt`/`createdAt` 원문을 그대로 전달하는 것을 확인해 공통 상대 시간 함수 분리와 최근 활동/인기 커뮤니티 재사용 항목으로 기록했다. 문서 변경만 수행했으므로 Gradle 검증은 실행하지 않고 문서 재읽기로 확인한다. - 2026-06-05: Task 9.6 구현으로 `RelativeTimeFormatter`를 추가해 UTC timestamp를 디바이스 기본 타임존 기준 상대 시간 문자열로 변환하고, 홈 추천 응답 매핑에서 `HomeActiveCreatorItem.activityAt`과 `HomePopularCommunityPostItem.createdAt`에 적용했다. 최근 활동/장르/응원 creator profile image는 `CircleCropTransformation` 기반 공통 helper로 원형 로딩하도록 변경했다. RED 검증은 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`에서 `homeCreatorProfileImageTransformations`, `toUiModel(context)` 미정의 컴파일 실패로 확인했고, 구현 후 동일 targeted test와 `./gradlew :app:compileDebugKotlin`은 BUILD SUCCESSFUL을 확인했다. `./gradlew :app:ktlintCheck`는 최종 diff에 없는 기존 `GetCommunityPostListResponse.kt`의 `creator_community` package-name 위반으로 실패해 별도 기존 이슈로 분리했다. - 2026-06-05: Task 9.6 후속 수정으로 `HomeRecommendationViewModel`에 `Context`를 주입해 보관하던 구조를 제거했다. `HomeRecommendationViewModel`은 다시 `HomeRecommendationViewModel(get())`로 Repository만 주입받고, UTC 상대 시간 문자열 변환은 `HomeMainFragment`의 UI 바인딩 경계에서 `requireContext()`로 처리하도록 이동했다. RED 검증은 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home recommendation viewmodel does not keep android context"`에서 ViewModel source에 `android.content.Context`가 남아 실패하는 것으로 확인했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`가 모두 BUILD SUCCESSFUL임을 확인했다. - 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 실제 상태 바인딩 전 제거하거나 대체해야 한다. - 2026-06-02: Phase 4.1로 `HomeRecommendationUiState`, `HomeRecommendationUiModels`, `HomeRecommendationResponse.toContent()`와 섹션별 mapper를 추가했다. DTO를 Fragment/ViewHolder 계약으로 직접 노출하지 않고, 첫 오디오 태그(`First`/`Point`/`Free`), AI character original title 표시 여부, 인기 커뮤니티 유료 상태 모델을 UI model로 변환하도록 구성했다. - 2026-06-02: Phase 4.2로 `HomeRecommendationViewModel`을 추가해 `recommendationStateLiveData`, `toastLiveData`, `isLoading`, `loadRecommendations()`, `followCreators(sectionKey, creatorIds)`를 구현했다. 추천 API 성공 시 Content/Empty 상태로 분기하고, 실패/data null/Throwable은 Error 상태와 unknown error toast를 노출하도록 했다. 빈 creatorIds에서는 모두 팔로우 API를 호출하지 않는다. - 2026-06-02: Phase 4.3으로 `AppDI.kt`에 `HomeRecommendationViewModel` import와 `viewModel { HomeRecommendationViewModel(get()) }` 등록을 추가했다. `lsp_diagnostics`는 `kotlin-lsp` 미설치로 실행되지 않아 Gradle 검증으로 보완할 예정이다. - 2026-06-02: Phase 4 검증으로 `./gradlew :app:compileDebugKotlin --rerun-tasks`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*"`, `./gradlew :app:testDebugUnitTest`, `./gradlew :app:ktlintCheck`, `./gradlew :app:compileDebugKotlin`을 실행했고 모두 BUILD SUCCESSFUL을 확인했다. 최초 증분 컴파일에서 `recentContentModule`, `chatTalkRoomModule` unresolved가 발생했으나 해당 파일이 존재하고 tracked 상태임을 확인했으며, `--rerun-tasks` 재실행 후 성공해 증분 캐시 문제로 분리했다. ktlint 최초 실행에서는 `HomeRecommendationMappers.kt`의 긴 줄 2건이 실패해 줄바꿈 수정 후 재실행으로 성공했다. - 2026-06-02: Phase 5 RED 검증으로 `HomeMainFragmentLayoutTest`를 추가하고 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`를 실행했다. 구현 전 `view_home_title_bar`, `text_tab_bar_home`, `nsv_home_recommendation_content`, 섹션/RecyclerView/title icon ID가 없어 컴파일 실패하는 RED 상태를 확인했다. - 2026-06-02: Phase 5.1로 `fragment_v2_main_home.xml`을 `ConstraintLayout` 기반으로 구성해 `view_title_bar_home`, `view_text_tab_bar`, `NestedScrollView` 추천 content shell을 배치했다. title bar와 tab bar는 scroll container 밖에 두고, 추천 content 내부에 섹션별 container와 가로 `RecyclerView`, 사업자 정보 영역만 추가했다. `ViewPager2`, `FragmentStateAdapter`, 랭킹/팔로잉 전용 content는 추가하지 않았다. - 2026-06-02: Phase 5.2로 `view_title_bar_home.xml`의 right icon을 `ic_bar_cash`, `ic_bar_search`, `ic_bar_bell` 순서로 확장하고, `HomeMainFragment`에서 `TextTabBarView` 메뉴를 추천/랭킹/팔로잉 순서와 selected index 0으로 바인딩했다. 좌우 swipe 전환은 추가하지 않았다. - 2026-06-02: Phase 5 GREEN/검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `lsp_diagnostics`는 `kotlin-lsp` 미설치로 실행되지 않아 Gradle compile/test/ktlint 결과로 보완했다. - 2026-06-02: 홈 상단 `TextTabBarView`가 추천 화면 전용이 아니라 홈 탭 공용 탭(추천/랭킹/팔로잉)임을 반영해 string resource 이름을 `home_recommendation_tab_*`에서 `screen_home_tab_*`으로 변경했다. 구현 전 `HomeMainFragment` 참조만 먼저 바꾼 `./gradlew :app:compileDebugKotlin`에서 새 string ID 미정의로 RED 실패를 확인했다. - 2026-06-02: 배너 아래 섹션 제목 요구사항을 반영해 `최근 데뷔한 크리에이터`, `처음부터 함께 성장!`, `크리에이터와 이야기를 나눠요!`에는 `view_section_title`의 더보기 chevron을 표시하고, `방금 활동한 크리에이터`, `최근 응원이 많은 크리에이터`, `인기 커뮤니티`에는 chevron을 숨기도록 `HomeMainFragment`에서 초기화했다. 장르 섹션은 백엔드 장르명 바인딩을 위해 `view_section_title`을 쓰지 않고 별도 title row로 분리했으며 장르명 색상은 `@color/color_3bb9f1`로 지정했다. 구현 전 `HomeMainFragmentLayoutTest`에서 새 section title ID 미정의로 RED 실패를 확인했고, 이후 `./gradlew :app:compileDebugKotlin --rerun-tasks`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest" --rerun-tasks`, `./gradlew :app:mergeDebugResources`가 BUILD SUCCESSFUL임을 확인했다. 최초 병렬 Gradle 실행 중 KSP incremental cache 오류가 있었으나 순차 재실행으로 성공해 캐시 경합 문제로 분리했다. - 2026-06-02: Phase 6.1-6.4로 라이브/배너/최근 활동/최근 데뷔/첫 오디오/AI 캐릭터/장르 크리에이터/응원 크리에이터 adapter와 binder를 추가하고, `HomeMainFragment`에서 빈 리스트 섹션을 `GONE` 처리하도록 바인딩했다. 배너는 `BannerView`, 라이브는 `LiveThumbnailSimpleView`, 첫 오디오는 `AudioContentCardView`, AI 캐릭터는 `CharacterChatThumbnailView`를 재사용했고, 임시 샘플 데이터는 `phase6SampleContent()`에 격리해 Phase 9에서 실제 상태 바인딩으로 교체할 수 있게 했다. 모두 팔로우 버튼은 creatorId가 비어 있으면 callback을 호출하지 않도록 연결했다. - 2026-06-02: Phase 6 검증으로 `./gradlew :app:compileDebugKotlin`, `./gradlew :app:testDebugUnitTest --tests kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:ktlintCheck`, `./gradlew :app:installDebug`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. 최초 병렬 Gradle 실행은 Kotlin 출력 디렉터리 잠금 경합으로 실패해 `./gradlew --stop` 후 순차 재실행했다. `lsp_diagnostics`는 `kotlin-lsp` 미설치와 XML LSP 미구성으로 실행되지 않았다. 연결 기기에서 런처 실행은 성공했지만 `MainV2Activity` 직접 실행은 `exported=false`로 차단되었고, 런처 경로는 splash의 검은 화면에 머물러 홈 추천 UI 실기기 육안 확인은 완료하지 못했다. - 2026-06-02: Phase 6 리뷰에서 Phase 7 전 빈 `popularCommunityPosts` 상태에도 `인기 커뮤니티` 섹션 container가 기본 노출될 수 있음을 확인했다. `HomeMainFragmentLayoutTest`에 `popular community section is hidden until phase7 binding is implemented` 회귀 테스트를 추가해 수정 전 실패(AssertionError)를 확인했고, `fragment_v2_main_home.xml` 기본 visibility와 `HomeMainFragment.bindPopularCommunitySection()`을 추가한 뒤 동일 테스트가 BUILD SUCCESSFUL로 통과함을 확인했다. - 2026-06-02: Phase 6 수정 후 `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 재실행했고 모두 BUILD SUCCESSFUL을 확인했다. 재리뷰에서 이전 `인기 커뮤니티` 빈 섹션 노출 지적이 수정됐고, Phase 6.1-6.4 범위의 남은 승인 차단 이슈가 없다는 무조건 승인을 받았다. - 2026-06-02: Figma `24:5529` 기준 `방금 활동한 크리에이터` 섹션을 확인해 기존 세로 프로필형 카드가 디자인과 다름을 확인했다. `HomeMainFragmentLayoutTest`에 `recent activity creator item matches figma capsule dimensions` 테스트를 먼저 추가했고, 기존 구현에서 실패하는 RED 상태를 확인했다. 이후 `item_home_recent_activity_creator.xml`을 `244dp x 76dp` 가로 capsule 구조로 변경하고 `HomePhase6Adapters.kt`에서 보조 텍스트를 `activityAt`으로 바인딩하도록 수정했으며, 동일 테스트가 BUILD SUCCESSFUL로 통과함을 확인했다. - 2026-06-02: `HomePhase6Adapters.kt`에 모든 섹션 adapter/binder가 모여 있어 섹션별 유지보수가 어렵다는 지적에 따라 adapter/binder를 섹션 단위 파일로 분리했다. `HomePhase6Adapters.kt`는 삭제하고, 공통 item margin 생성 로직은 `HomeRecyclerItemLayoutParams.kt`로 분리했다. 동작 변경 없이 파일 구조만 정리하는 리팩터링이다. - 2026-06-02: Figma `24:5516` 기준 `ll_home_live_section`을 확인해 라이브 행 높이, `전체` item, 아이템 간격이 디자인과 다름을 확인했다. `HomeMainFragmentLayoutTest`에 live row 치수, live adapter item gap, 20개 초과 시 `전체` item 추가 회귀 테스트를 먼저 추가했고, 구현 전 adapter `전체` view type 미지원과 20개 cap 미지원으로 RED 실패를 확인했다. 이후 `fragment_v2_main_home.xml`에서 별도 `tv_home_live_more` overlay를 제거하고, `HomeLiveAdapter`가 live를 최대 20개까지 표시한 뒤 20개 초과 시 마지막 item으로 `58dp x 102dp` 검은 배경 `전체`를 추가하도록 수정했으며, 동일 테스트가 BUILD SUCCESSFUL로 통과함을 확인했다. - 2026-06-02: 라이브 섹션 `전체` item 방식 변경 후 `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `lsp_diagnostics`는 Kotlin/XML LSP 미구성으로 실행하지 못해 Gradle compile/test/ktlint로 보완했다. - 2026-06-02: Figma `24:5534` 기준 `최근 데뷔한 크리에이터` 섹션을 확인해 기존 `112dp` 세로 원형 profile item이 디자인과 다름을 확인했다. `HomeMainFragmentLayoutTest`에 recent debut 카드 치수와 목록 간격 테스트를 먼저 추가했고, 기존 구현에서 `recent debut creator item matches figma card dimensions`, `home recent debut section matches figma list spacing` 두 테스트가 실패하는 RED 상태를 확인했다. - 2026-06-02: `item_home_recent_debut_creator.xml`을 `205dp x 259dp` 이미지 카드 구조로 변경하고, radius `14dp` 배경/하단 dim gradient/`24sp` bold 중앙 닉네임을 적용했다. `HomeRecentDebutCreatorAdapter`에는 최근 데뷔 전용 `4dp` item gap을 적용했고, `rv_home_recent_debut_creators` 시작 padding을 Figma 기준 `14dp`로 맞췄다. 동일 targeted 테스트 재실행 결과 BUILD SUCCESSFUL을 확인했다. - 2026-06-02: 최근 데뷔 수정 후 `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`는 BUILD SUCCESSFUL을 확인했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`는 이번 최근 데뷔 테스트는 통과했지만 기존 `home live section matches figma row dimensions`가 `rv_home_lives` 높이 기대값(`102dp`)과 현재 XML `wrap_content`가 맞지 않아 실패했다. 이후 사용자 확인에 따라 `rv_home_lives` 높이는 아이템 높이를 따라가야 하므로 `wrap_content`가 올바른 계약으로 정정했다. - 2026-06-02: Figma `24:5539` 기준 `처음부터 함께 성장!` 섹션을 확인해 기존 `AudioContentCardView` 기반 item이 전용 profile row 구조와 item gap을 재현하기 어렵다는 점을 확인했다. `HomeMainFragmentLayoutTest`에 first audio 전용 item 치수, 목록 간격, tag visibility 테스트를 먼저 추가했고, 구현 전 `item_home_first_audio_content`와 관련 view ID 미존재로 RED 컴파일 실패를 확인했다. - 2026-06-02: `item_home_first_audio_content.xml`을 추가하고 `HomeFirstAudioAdapter`가 `AudioContentCardView` 대신 전용 item을 inflate하도록 변경했다. 썸네일은 `185dp x 185dp`, creator profile은 `42dp x 42dp`, item gap은 `4dp`로 맞췄고, `First`/`Point`/`Free` tag visibility는 기존 `tags` 모델로 바인딩했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"` 재실행 결과 BUILD SUCCESSFUL을 확인했다. - 2026-06-02: 리뷰에서 first audio 썸네일 이미지 clipping과 `Free` tag visibility 테스트 누락이 차단 이슈로 지적되어 `item_home_first_audio_content.xml`에 `clipToOutline="true"`를 추가하고 `HomeMainFragmentLayoutTest`에서 `First`/`Point`/`Free` 개별 tag visibility를 모두 검증하도록 보강했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-02: first audio adapter에서 nullable image URL이 재사용된 ViewHolder의 이전 이미지를 남길 수 있는 케이스를 확인했다. `first audio adapter clears nullable images` 테스트를 추가해 수정 전 실패를 확인한 뒤, `HomeFirstAudioAdapter`에서 `coverImage`/`creatorProfileImage`가 null이면 drawable을 명시적으로 비우도록 수정했다. 이후 해당 테스트, `HomeMainFragmentLayoutTest` 전체, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-02: first audio 썸네일은 이미지뿐 아니라 `FIRST`/`Point`/`Free` overlay까지 같은 radius로 잘려야 한다는 사용자 확인에 따라 Coil transformation 방식이 아니라 썸네일 parent `FrameLayout` 전체에 Kotlin `clipToOutline = true`와 `ViewOutlineProvider.setRoundRect(..., 14dp)`를 적용했다. XML `android:clipToOutline`은 lint 경고를 피하기 위해 사용하지 않았다. `first audio adapter clips thumbnail container` 테스트를 추가했고, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행해 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-02: `AudioContentCardView`의 이미지 표시 영역도 first audio 전용 item과 동일하게 이미지와 overlay tag가 함께 roundRect clipping되도록 확인하고, `First`/`Point`/`Free` tag 속성을 `item_home_first_audio_content.xml`과 맞췄다. `HomeMainFragmentLayoutTest`에 `audio content card clips image area and overlay tags together`, `audio content card tag attributes match first audio item` 회귀 테스트를 추가했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-02: `AudioContentCardView`와 `item_home_first_audio_content.xml`의 badge별 제어를 위해 `Original`/`First`/`Point`/`Free` badge에 안정적인 id를 추가했다. `AudioContentCardView`의 runtime 생성 badge에도 동일 id를 부여하고, `First`와 `Free` badge 높이를 `24dp`로 유지하도록 맞췄다. `HomeMainFragmentLayoutTest`에서 badge id/높이 계약을 검증하도록 보강했으며 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행해 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-02: Figma `24:5551` 기준 `크리에이터와 이야기를 나눠요!` 섹션을 확인해 기존 목록 padding `20dp`, adapter item gap `12dp`, generic `RecyclerView.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)` 적용이 `view_character_chat_thumbnail.xml`의 root `185dp x wrap_content` 계약을 보존하지 못함을 확인했다. `HomeMainFragmentLayoutTest`에 character thumbnail root 치수, AI 캐릭터 목록 spacing, adapter item dimension 회귀 테스트를 먼저 추가했고, 수정 전 targeted test에서 새 테스트 2건 실패로 RED를 확인했다. - 2026-06-02: `rv_home_ai_characters` padding을 `14dp`로 맞추고, `HomeAiCharacterAdapter`에서 inflate된 `view_character_chat_thumbnail.xml` root layout params를 유지한 채 `marginEnd`만 `4dp`로 적용했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `lsp_diagnostics`는 Kotlin/XML LSP 미구성으로 실행하지 못해 Gradle compile/test/ktlint로 보완했다. - 2026-06-04: Figma `24:5611`, 완료 버튼 `24:9092` 기준 장르 크리에이터 모두 팔로우 버튼을 확인해 기본 상태는 `ic_new_follow`, 투명 배경과 흰색 30% stroke, 흰색 텍스트이고 완료 상태는 `ic_new_following`, 흰색 배경, 검은색 텍스트여야 함을 확인했다. `HomeMainFragmentLayoutTest`에 모두 팔로우 기본/완료 상태와 완료 상태 no-op 회귀 테스트를 먼저 추가했고, 구현 전 `bg_home_follow_all_button` 리소스 미존재로 RED 컴파일 실패를 확인했다. - 2026-06-04: `bg_home_follow_all_button.xml`을 추가하고 `view_home_follow_all_button.xml`의 기본 상태를 Figma 활성 상태로 변경했다. `HomeFollowAllButtonBinder`는 완료 상태에서 `bg_round_corner_999_white`, 검은 텍스트, `ic_new_following`, `모두 팔로우 완료`를 적용하고 터치 시 callback을 호출하지 않도록 수정했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `./gradlew :app:installDebug`는 연결된 기기가 없어 `No connected devices!`로 실패해 실기기 설치 확인은 수행하지 못했다. - 2026-06-04: 사용자 추가 요청에 따라 Figma `24:5611` 기준 `장르의 크리에이터` 섹션을 그룹 단위 snapping card 구조로 수정했다. 구현 전 `HomeMainFragmentLayoutTest`에 genre group card/profile/snapping/filtering/follow callback 회귀 테스트를 추가했고, 기존 구현에서 `item_home_genre_creator_group`, `bg_home_genre_creator_group`, `submitGroups` 등 새 계약 미존재로 RED 컴파일 실패를 확인했다. 이후 `HomeGenreCreatorAdapter`를 group page adapter로 변경하고, `HomePagerSnapRecyclerView`, `item_home_genre_creator_group.xml`, `item_home_genre_creator_profile.xml`, `bg_home_genre_creator_group.xml`을 추가해 각 page에 장르 제목, 8명 grid, 모두 팔로우 버튼, dark-blue gradient, `PagerSnapHelper` snapping을 적용했다. targeted 테스트 최초 재실행에서는 null image가 Coil `ImageLoaderProvider`를 요구해 실패했고, nullable image는 drawable을 명시적으로 비우도록 수정한 뒤 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`가 BUILD SUCCESSFUL임을 확인했다. 추가 검증으로 `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. ktlint 최초 실행은 테스트 파일 긴 줄 2곳으로 실패했고 줄바꿈 수정 후 재실행으로 성공했다. `lsp_diagnostics`는 Kotlin/XML LSP 미구성으로 실행하지 못해 Gradle compile/test/ktlint로 보완했다. 실기기 수동 확인은 연결 기기가 없어 수행하지 못했다. - 2026-06-04: 사용자 추가 요청에 따라 Figma `24:5636` 기준 `최근 응원이 많은 크리에이터` 섹션을 card 내부 profile grid 구조로 수정했다. 구현 전 `HomeMainFragmentLayoutTest`에 cheer card/grid/width/follow callback 회귀 테스트를 추가했고, 기존 구현에서 `item_home_cheer_creator_group`, `bg_home_cheer_creator_group`, `HomeCheerCreatorAdapter(onFollowAllClick)`, `submitSection` 등 새 계약 미존재로 RED 컴파일 실패를 확인했다. 이후 `HomeCheerCreatorAdapter`를 단일 card adapter로 변경하고, `item_home_cheer_creator_group.xml`, `bg_home_cheer_creator_group.xml`을 추가해 `14dp` horizontal padding 기반 full-width card, 최대 8명 4열 grid, card 내부 모두 팔로우 버튼을 적용했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`, `./gradlew :app:installDebug`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `adb shell monkey -p kr.co.vividnext.sodalive.debug -c android.intent.category.LAUNCHER 1`로 설치된 debug 앱 실행까지 확인했다. 리뷰 게이트에서 무조건 승인도 확인했다. `lsp_diagnostics`는 Kotlin/XML LSP 미구성으로 실행하지 못해 Gradle compile/test/ktlint로 보완했다. - 2026-06-04: 리뷰에서 모든 genre group의 creators가 비어 있을 때 adapter는 빈 목록이 되지만 `ll_home_genre_creator_section`이 `VISIBLE`로 남을 수 있다는 차단 이슈를 확인했다. `HomeMainFragmentLayoutTest`에 `visibleHomeGenreCreatorGroups()` all-empty 회귀 테스트를 추가했고, helper 미구현으로 RED 컴파일 실패를 확인했다. 이후 `HomeRecommendationUiModels.kt`에 visible group helper를 추가하고 `HomeMainFragment.bindGenreCreatorSection()`의 visibility와 submit 기준을 같은 filtered/capped 목록으로 맞췄으며, adapter 자체의 empty/max 5 방어도 유지했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 재실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-04: 장르 크리에이터 group card 폭을 고정 `374dp`가 아니라 현재 화면 가로 크기에서 좌우 여백 `28dp`를 뺀 값으로 계산하도록 수정했다. 구현 전 `HomeMainFragmentLayoutTest`의 group layout 계약을 `match_parent`로 바꾸고 adapter item width가 parent width `402dp - 28dp = 374dp`가 되는 회귀 테스트를 추가했으며, 기존 구현에서 XML `374dp` 고정 및 adapter 폭 미계산으로 RED 실패를 확인했다. 이후 `item_home_genre_creator_group.xml` root width를 `match_parent`로 바꾸고 `HomeGenreCreatorAdapter`에서 parent width 기반 `RecyclerView.LayoutParams`를 설정하도록 변경했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-04: 작은 해상도에서 장르 크리에이터 grid의 profile item/image `75dp` 고정값 때문에 4열 끝부분이 잘릴 수 있어, group card 내부 grid 가용 폭에 맞춰 profile cell 크기를 계산하도록 수정했다. 구현 전 `HomeMainFragmentLayoutTest`에 작은 parent width `360dp` 기준 cell size가 `(360 - 28 - 28 - 14*3) / 4`로 계산되는 회귀 테스트를 추가했고, 기존 `75dp` 고정 구현에서 RED 실패를 확인했다. 이후 `item_home_genre_creator_group.xml`의 grid width를 `match_parent`로 변경하고, `HomeGenreCreatorAdapter`에서 card width, padding, column gap을 기준으로 profile item/image width/height를 동적으로 설정하도록 수정했다. `home_genre_creator_profile_size` dimen은 더 이상 사용하지 않아 제거했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-04: `item_home_genre_creator_profile.xml` 자체에도 profile root와 image가 `75dp`로 고정되어 있어 XML 계약을 `ConstraintLayout` 기반의 가변 cell + 1:1 image ratio로 변경했다. 구현 전 `HomeMainFragmentLayoutTest`의 profile XML 테스트를 root `match_parent`, image `0dp x 0dp`, `layout_constraintDimensionRatio="1:1"` 계약으로 바꿨고 기존 `LinearLayout`/`75dp` XML에서 RED 실패를 확인했다. 이후 root를 `ConstraintLayout`으로 전환하고 image는 parent width를 따르는 1:1 ratio로, nickname은 image 하단에 constraint 되도록 수정했다. adapter에서는 image width/height 직접 덮어쓰기를 제거하고 root cell width만 grid layout params로 설정하게 정리했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-04: 실제 화면에서 장르 크리에이터 grid 우측 끝 이미지가 절반 정도 잘리는 문제를 수정했다. 원인은 profile size 계산 시 group item width가 아직 `MATCH_PARENT`이거나 미측정일 때 display width fallback을 사용해 cell이 과대 계산될 수 있는 점이었다. 구현 전 `HomeMainFragmentLayoutTest`에 group width가 미확정인 상황에서도 parent RecyclerView width `360dp` 기준으로 cell size가 계산되는 회귀 테스트를 추가했고, 기존 display width fallback에서 RED 실패를 확인했다. 이후 `HomeGenreCreatorAdapter.GenreCreatorGroupViewHolder`가 parent RecyclerView를 보관하고, card width가 확정되지 않은 경우 `parent.width - 28dp`를 사용해 profile cell 크기를 계산하도록 수정했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-04: 한국어/영어 장르 suffix의 선행 공백이 실제 기기에서 보이지 않고, 우측 끝 profile image가 일부 잘리는 문제를 추가 수정했다. 일본어에는 공백을 넣으면 안 되므로 title row margin 방식은 사용하지 않고 한국어/영어 string에 `\u0020`를 사용해 선행 공백을 보존했다. `xml:space="preserve"` 시도는 Android resource merge에서 실패해 제거했고, stale incremental resource 산출물(`app/build/intermediates/incremental/debug/packageDebugResources`, `mergeDebugResources`)을 삭제한 뒤 재검증했다. profile cell 크기는 itemView 측정 폭이 parent RecyclerView content 폭보다 크게 잡히는 경우를 방지하도록 parent padding을 제외한 content width로 cap 했다. 구현 전 `HomeMainFragmentLayoutTest`에 parent padding이 있는 상태에서 measured group width가 과대 측정되어도 cell size가 `RecyclerView content width` 기준으로 계산되는 회귀 테스트와 한국어/영어는 선행 공백, 일본어는 선행 공백 없음 테스트를 추가해 RED 실패를 확인했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-04: 우측 profile image 잘림 범위가 group card/RecyclerView가 아니라 `item_home_genre_creator_group.xml` 내부 `GridLayout`임을 확인해, profile cell 계산 기준을 `GridLayout`의 실제 측정 width 우선으로 수정했다. 구현 전 `HomeMainFragmentLayoutTest`에 GridLayout width가 `300dp`로 측정된 경우 cell size가 `(300 - 14*3) / 4`로 계산되어야 하는 회귀 테스트를 추가했고, 기존 card width 기준 계산에서 RED 실패를 확인했다. 이후 `HomeGenreCreatorAdapter`의 `calculateProfileSize()`가 `creatorGrid.width`를 우선 사용하고, layout 이후 width가 확정되는 경우 `creatorGrid.post { ... }`에서 다시 바인딩해 재계산하도록 변경했다. 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-05: Figma `24:5645` 기준 Phase 7 인기 커뮤니티 섹션을 구현했다. 구현 전 `FeedViewTest`와 `HomeMainFragmentLayoutTest`에 기존 Community keyword 기본 노출, 추천용 keyword 숨김, 이미지 null 시 이미지 영역 `GONE`, 유료 미구매 overlay/가격 capsule, 구매 완료 overlay 숨김, 인기 커뮤니티 최대 10개 노출 테스트를 추가했고, 기존 구현에서 새 필드/뷰/adapter/helper 미존재로 RED 컴파일 실패를 확인했다. 이후 `FeedItem.Community`에 `imageUrl`, `price`, `existOrdered`, `showKeyword` 기본값 필드를 추가하고, `FeedCommunityView`/`FeedAdapter`가 커뮤니티 primary image와 유료 overlay를 지원하도록 확장했으며, `HomePopularCommunityAdapter`와 `HomeMainFragment` 바인딩을 추가했다. `popularCommunityPosts`는 최대 10개만 표시한다. - 2026-06-05: Phase 7 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:testDebugUnitTest`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. 최초 병렬 Gradle 실행은 KSP cache 경합으로 실패해 `./gradlew --stop` 후 순차 재실행했다. ktlint 최초 실행은 새 긴 줄 위반으로 실패했고 줄바꿈 수정 후 재실행으로 성공했다. `lsp_diagnostics`는 Kotlin/XML LSP 미구성으로 실행하지 못해 Gradle compile/test/ktlint로 보완했다. - 2026-06-05: Phase 7 리뷰 게이트에서 `audioUrl` 상세 이동 데이터 유실과 유료 미구매 커뮤니티 원본 이미지 로드 가능성이 차단 이슈로 지적됐다. `HomeMainFragmentLayoutTest`에 `popular community mapper preserves audio url for detail data`, `home popular community adapter does not load original image for locked paid post` 회귀 테스트를 추가했고, 구현 전 `audioUrl` 필드 미존재로 RED 컴파일 실패를 확인했다. 이후 `FeedItem.Community.audioUrl` 기본값 필드와 mapper 전달을 추가하고, `HomePopularCommunityAdapter`가 `price > 0 && existOrdered == false`인 item의 원본 `imageUrl`을 로드하지 않도록 수정했다. - 2026-06-05: Phase 7 차단 이슈 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:ktlintCheck`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-05: 보안 재리뷰에서 유료 미구매 item으로 재바인딩할 때 기존 Coil 비동기 요청이 늦게 완료될 가능성이 차단 이슈로 지적됐다. `HomePopularCommunityAdapter.bindImage()`의 null/blank 경로에서 `imageView.dispose()`로 기존 Coil 요청을 취소한 뒤 drawable을 비우도록 보완했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-05: Phase 7 인기 커뮤니티 UI를 실제 기기에서 확인할 수 있도록 `HomeMainFragment.phase6SampleContent()`의 `popularCommunityPosts`에 무료/유료 미구매/구매 완료 샘플 3건을 임시 주입했다. 샘플은 추천용 계약에 맞춰 keyword를 숨기고, 유료 미구매 샘플은 lock overlay와 가격 capsule을 확인할 수 있게 구성했다. `./gradlew :app:compileDebugKotlin`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:ktlintCheck`, `./gradlew :app:mergeDebugResources`는 모두 BUILD SUCCESSFUL을 확인했다. `./gradlew :app:installDebug`는 연결 기기가 없어 `No connected devices!`로 실패해 실기기 설치/육안 확인은 수행하지 못했다. - 2026-06-05: 사용자 확인 중 커뮤니티 게시물 이미지가 잘리는 문제가 있어 `view_feed_community.xml`의 root `374dp`, 이미지 container `346dp x 236dp` 고정 크기를 제거했다. root와 이미지 container는 `match_parent`로 두고, `FeedCommunityView.onMeasure()`에서 실제 card content width 기준 Figma `346:236` 비율로 이미지 높이를 계산하도록 변경했다. Kotlin `clipToOutline`/`ViewOutlineProvider.setRoundRect(...)` 계약은 유지했다. `FeedViewTest`에 동적 이미지 폭/높이 회귀 테스트를 추가했으며, 최초 테스트는 이미지 URL 미바인딩으로 container가 `GONE`이라 실패했고 테스트 조건을 실제 표시 상태로 수정한 뒤 성공했다. 이후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:ktlintCheck`가 모두 BUILD SUCCESSFUL임을 확인했다. - 2026-06-05: Phase 8 사업자 정보 UI 구현으로 `fragment_v2_main_home.xml`에 기본 `maxLines=3`/`ellipsize=end`와 `tv_home_business_info_toggle`을 추가하고, `HomeBusinessInfoBinder` 및 `HomeMainFragment.setUpBusinessInfo()`에서 더보기/접기 토글과 `TextView.post { lineCount }` 기반 3줄 이하 toggle 숨김 처리를 연결했다. RED 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`를 먼저 실행해 `HomeBusinessInfoBinder`, `tv_home_business_info_toggle` 미존재 컴파일 실패를 확인했고, 구현 후 동일 targeted test와 `./gradlew :app:mergeDebugResources`가 BUILD SUCCESSFUL임을 확인했다. - 2026-06-05: 사용자 확인에 따라 `home_recommendation_business_info`와 기존 `company_info` 중복을 제거했다. `fragment_v2_main_home.xml`은 공용 `@string/company_info`를 참조하도록 변경하고, 한국어/영어/일본어 `home_recommendation_business_info` 리소스는 삭제했다. 리뷰 게이트에서 지적된 접힘 상태 lineCount 측정 문제는 전체 줄 수 측정 시 일시적으로 `maxLines=Int.MAX_VALUE`/`ellipsize=null`로 측정한 뒤 다시 접힘 상태로 되돌리도록 보완했고, 4줄 이상일 때 toggle이 노출되는 회귀 테스트를 추가했다. - 2026-06-05: `HomeBusinessInfoBinder`의 const naming warning을 해결하기 위해 `CollapsedMaxLines`를 `COLLAPSED_MAX_LINES`로 변경했다. 또한 긴 텍스트 줄 수 측정은 접힘 상태의 기존 layout을 읽지 않도록 `HomeMainFragment`에서 전체 줄 표시 상태를 적용한 뒤 다음 `post`에서 `lineCount`를 읽고 다시 접힘 상태로 되돌리도록 보완했다. - 2026-06-05: 사용자 추가 요청에 따라 사업자 정보 `더보기/접기`를 별도 하단 버튼이 아니라 본문 말줄임표 우측에 붙는 inline text action으로 변경하는 후속 작업을 진행한다. - 2026-06-05: 사업자 정보 `더보기/접기`를 하단 별도 버튼에서 본문 inline action으로 변경했다. `tv_home_business_info_toggle` View를 제거하고 `HomeBusinessInfoBinder`에서 접힘 상태는 `… 더보기`, 펼침 상태는 본문 뒤 `접기`를 clickable span으로 붙이도록 구현했다. `HomeMainFragmentLayoutTest`에 별도 toggle View 제거, inline 더보기 suffix, clickable span 확장/접기, 3줄 이하 action 미노출 테스트를 추가했다. 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-05: 사업자 정보 inline `더보기`가 말줄임표와 너무 붙어 보여 `… 더보기`에서 `… 더보기`로 간격을 한 칸 늘렸다. 관련 inline suffix 테스트 기대값도 함께 갱신한다. - 2026-06-05: 펼침 상태의 `접기` action도 본문에 붙어 보이지 않도록 action 앞에 공백 2칸을 추가했다. 접힘 상태의 `더보기` 간격과 동일한 시각적 여백을 유지하도록 테스트 기대값을 갱신했다. - 2026-06-05: Phase 9 RED 검증으로 `HomeMainFragmentLayoutTest`에 `home main fragment phase9 replaces sample content with viewmodel state binding`, `home main fragment phase9 connects loading toast and follow all callbacks` 소스 계약 테스트를 추가했다. 최초 실행은 테스트 source 경로 기준 문제로 `FileNotFoundException`이 발생해 경로를 보정했고, 이후 `phase6SampleContent`가 남아 있어 `AssertionError`로 RED 실패하는 것을 확인했다. - 2026-06-05: Phase 9.1로 `HomeMainFragment`에서 임시 `phase6SampleContent()`와 sample helper를 제거하고 `HomeRecommendationViewModel`을 주입해 `recommendationStateLiveData`, `isLoading`, `toastLiveData` observe 및 `loadRecommendations()` 호출을 연결했다. `Content`는 섹션 bind, `Empty`/`Error`는 빈 content bind로 섹션을 숨기고, loading은 `LoadingDialog`, toast는 `BaseFragment.showToast()`로 처리한다. - 2026-06-05: Phase 9.2로 live/banner/popular community callback을 Fragment에 연결하고, 기존 라우팅 extra가 확인된 creator/audio/AI/community만 최소 Intent 이동으로 연결했다. 목적지가 확정되지 않은 live/banner는 adapter callback을 Fragment로 위임하되 no-op으로 유지했다. `HomeRecentActivityCreatorAdapter`, `HomeRecentDebutCreatorAdapter`, `HomeFirstAudioAdapter`, `HomeAiCharacterAdapter`, `HomeGenreCreatorAdapter`, `HomeCheerCreatorAdapter`에는 기존 호출부 영향이 없도록 기본값 있는 click callback을 추가했다. - 2026-06-05: Phase 9 GREEN/검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home main fragment phase9*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:ktlintCheck`, `./gradlew :app:testDebugUnitTest`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. 최초 병렬 `compileDebugKotlin`은 Kotlin output directory 경합으로 실패했으나 순차 재실행에서 성공해 캐시/경합 문제로 분리했다. `lsp_diagnostics`는 Kotlin/XML LSP 미구성으로 실행하지 못해 Gradle compile/test/ktlint로 보완했다. - 2026-06-05: Phase 9.3으로 변경된 홈 추천 응답 스키마를 반영했다. `HomeLiveItem`은 `roomId`, `creatorNickname`, `creatorProfileImage`만 사용하고, `HomeBannerItem`은 `imageUrl`, `eventItem`, `creatorId`, `seriesId`, `link`로 매핑한다. `HomeActiveCreatorItem`은 `creatorNickname`, `creatorProfileImage`, `activityType`, `activityAt`, `targetId`로 매핑하고 응답에서 제거된 `creatorId` 기반 최근 활동 프로필 이동은 제거했다. `HomeCreatorItem`의 `creatorNickname`/`creatorProfileImage`, `HomeFirstAudioContentItem`의 `releaseDate` 제거, `HomeGenreCreatorGroupItem.genreName`도 mapper/UI model에 반영했다. 실제 UI에서 쓰지 않거나 응답에서 제공하지 않는 nullable placeholder 값은 추가하지 않았다. - 2026-06-05: Phase 9.3 RED 검증으로 `HomeMainFragmentLayoutTest`에 `home recommendation mapper uses changed response fields`를 추가하고 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home recommendation mapper uses changed response fields"`를 실행했다. 구현 전 `HomeRecommendationMappers.kt`가 제거된 `liveId`, `creatorId`, `imageUrl`, `title`, `beginDateTime`, `bannerId`, `type`, `linkUrl`, `targetId`, `releaseDate`, `genre` 필드를 참조해 `:app:compileDebugKotlin` 컴파일 실패하는 RED 상태를 확인했다. mapper/UI model/adapter 수정 후 동일 테스트를 재실행해 BUILD SUCCESSFUL을 확인했다. - 2026-06-05: Phase 9.3 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. 전체 홈 테스트 최초 실행에서는 non-null profile image helper가 Coil `ImageLoaderProvider` 초기화를 요구해 실패했고, 빈 문자열 이미지를 이미지 없음으로 처리하도록 `HomeFirstAudioAdapter`, `HomeGenreCreatorAdapter`, `HomeCheerCreatorAdapter`를 보완한 뒤 재실행해 성공했다. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐고 lint 실패는 없었다. - 2026-06-05: Phase 9.3 리뷰에서 `HomeBannerBinder`가 `imageUrl`을 click source key로 쓰면 중복 이미지 URL 배너에서 첫 번째 항목으로 잘못 매칭될 수 있다는 차단 이슈를 확인했다. `HomeBannerBinder`를 position 기반 synthetic id 매칭으로 수정하고, `HomeMainFragmentLayoutTest`에 `home banner binder matches duplicate image banners by position` 회귀 테스트를 추가했다. - 2026-06-05: Phase 9.3 리뷰 지적 수정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest.home banner binder matches duplicate image banners by position"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 재실행했고 모두 BUILD SUCCESSFUL을 확인했다. 배너 회귀 테스트 최초 실행은 `HomeRecommendationBannerSection`, `HomeRecommendationBannerUiModel` import 누락으로 컴파일 실패했고, import 보정 후 성공했다. - 2026-06-05: Phase 9.4로 홈 추천 API 응답 바인딩 후 `java.io.IOException: unexpected journal header: [libcore.io.DiskLruCache, 1, 2, ]`, `allocator 3.x is not supported` 경고와 함께 앱이 멈추는 문제를 이미지 캐시 journal 충돌 가능성으로 분리했다. `ImageLoaderProvider`에서 기존 `cacheDir/image_cache`를 앱 시작 시 삭제하고, 신규 OkHttp image cache directory를 `cacheDir/coil_image_cache`로 분리했다. 구현 전 `ImageLoaderProviderTest`는 cache directory 상수 미정의로 RED 컴파일 실패했고, 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.common.ImageLoaderProviderTest"`가 BUILD SUCCESSFUL임을 확인했다. - 2026-06-05: Phase 9.4 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐고 lint 실패는 없었다. - 2026-06-05: Phase 9.4 리뷰에서 `ImageLoaderProviderTest`가 상수명 차이만 검증해 `init()`의 legacy cache 삭제와 신규 cache 생성 계약을 충분히 고정하지 못한다는 차단 이슈를 확인했다. 테스트를 보강해 Robolectric 환경에서 `cacheDir/image_cache/journal`을 만든 뒤 `ImageLoaderProvider.init(context)` 호출 후 legacy directory 삭제와 `coil_image_cache` 생성까지 검증하도록 수정했다. 보강 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.common.ImageLoaderProviderTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 재실행했고 모두 BUILD SUCCESSFUL을 확인했다. - 2026-06-05: Phase 9.5로 `BannerView.setItems()` 직후 앱이 멈추는 문제를 배너 모델 payload 누락과 `BannerAdapter`의 huge range notify 가능성으로 분리했다. `BannerItem`을 삭제하지 않고 `HomeRecommendationBannerUiModel`과 동일한 payload(`imageUrl`, `eventItem`, `creatorId`, `seriesId`, `link`)를 담도록 확장해 공용 위젯 독립성을 유지했으며, `HomeBannerBinder`는 synthetic id 변환 없이 홈 배너 UI model과 `BannerItem`을 변환하도록 단순화했다. 또한 carousel 목록에서 `itemCount == Int.MAX_VALUE`여도 `notifyItemRangeInserted(0, Int.MAX_VALUE)`와 `notifyItemRangeChanged(0, Int.MAX_VALUE)`를 호출하지 않도록 했고, 단일 배너는 specific notify를 쓰되 `Int.MAX_VALUE` 가상 adapter 상태에서만 `NotifyDataSetChanged` lint를 좁은 helper에 제한했다. 구현 전 `BannerViewTest`는 기존 `BannerItem`이 변경된 홈 배너 payload를 담지 못해 클릭 데이터 보존을 검증할 수 없는 RED 상태였고, 구현 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.banner.BannerViewTest"`가 BUILD SUCCESSFUL임을 확인했다. - 2026-06-05: Phase 9.5 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentLayoutTest"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 추가 실행했고 모두 BUILD SUCCESSFUL을 확인했다. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐고 lint 실패는 없었다.