diff --git a/docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md b/docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md index d2063a5a..165afd5a 100644 --- a/docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md +++ b/docs/20260601_메인_홈_추천_UI와_API_연동/plan-task.md @@ -432,6 +432,57 @@ - 확정되지 않은 목적지는 임시 주석으로 남기지 않고 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`로 회귀를 확인한다. + --- ### Phase 10: 최종 검증과 문서 기록 @@ -468,8 +519,7 @@ ## 구현 중 확인 필요 - 배너 `type`별 이동 정책과 서버 code 목록. -- `HomeLiveItem`, `HomeBannerItem`, `HomeActiveCreatorItem`, `HomeCreatorItem`, `HomeGenreCreatorGroupItem`의 최종 서버 필드명. -- 시간 표시 포맷: `activityAt`, `releaseDate`, `createdAt`, `beginDateTime`에 기존 formatter를 재사용할 수 있는지 확인. +- 시간 표시 포맷: `activityAt`, `createdAt`에 기존 formatter를 재사용할 수 있는지 확인. - 모두 팔로우 API success response의 `data` 형태. `ApiResponse.success == true`만으로 완료 처리 가능한지 백엔드 계약 확인. - 사업자 정보 텍스트를 `strings.xml`로 둘지 서버/설정값으로 받을지 운영 정책 확인. @@ -547,3 +597,13 @@ - 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 실패는 없었다.