# Phase 9 최근 소식 nested payload 리뷰 ## 1. 리뷰 정보 | 항목 | 내용 | |---|---| | 리뷰 대상 | Phase 9 / Task 9.1~9.7 | | 기준 commit 또는 working tree | `f600d10e8b63b6fdc812b18a874b077dfe072ca8` + 2026-07-31 working tree | | 리뷰 일자 | 2026-07-31 | | 리뷰어 | Codex | | 기준 문서 | 대상 `prd.md`, `plan-task.md` | | 리뷰 상태 | 수정 검증 완료 | ## 2. 리뷰 목적과 범위 ### 목적 - type별 nested payload DTO, mapper 필터, nullable image, adapter binding과 target ID 보존을 확인한다. - Task 9.7의 미사용 `FeedAdapter`·`FeedImageViews` 제거가 실제 호출자 0건과 일치하는지 확인한다. ### 포함 범위 - `HomeFollowingModels.kt`, `HomeFollowingUiModels.kt`, `HomeFollowingMappers.kt`, `HomeFollowingNewsAdapter.kt` - 삭제된 `v2/widget/feed/FeedAdapter.kt`와 현재 재사용 중인 `FeedItem`, `FeedRankView`, `FeedContentView`, `FeedCommunityView` - mapper/source tests와 content ranking string resource ### 제외 범위 - 실제 서버 응답 샘플과 Phase 11/13 터치 목적지. ## 3. 판정 기준 - PRD의 nested payload 필드, matching payload null 제외, `contentImageUrl: String?`, `visibleFromAtUtc` 정책을 기준으로 판정했다. ## 4. 검토한 근거 - `HomeFollowingModels.kt:47-111` - `HomeFollowingMappers.kt:64-149` - `HomeFollowingUiModels.kt:55-103` - `HomeFollowingNewsAdapter.kt:68-167` - `HomeFollowingMapperTest.kt:88-182` ### 실행한 검증 | 명령 | 결과 | 핵심 증거 | |---|---|---| | mapper + Fragment source focused test | 성공 | mapper 10, source 16 테스트 실패 0 | | 팔로잉 전체 회귀 | 성공 | 39개 테스트 실패 0 | | resource/compile/ktlint/diff | 성공 | 모두 PASS | | nullable image binding·Coil 2.7.0 source·저장소 `dispose()` 사용례 대조 | 문제 확인 | blank 분기가 drawable만 비우고 View 요청을 해제하지 않음 | | DTO→mapper→Feed item의 ID 의미 대조 | 문제 확인 | content/post ID가 creator ID 필드에 대입됨 | | `rg -n "FeedAdapter|FeedImageViews" app/src/main/java app/src/test/java docs/20260625_메인_홈_팔로잉_탭 docs/agent-guides` | 성공 | production/test 호출자 0건, Task 9.7 이력만 남음 | ## 5. 발견 사항 요약 | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-P9-001` | Medium | 수정 완료 | nullable 최근 소식 이미지 바인딩이 이전 Coil 요청을 해제하지 않음 | Task 9.5 | `P9-R1` 완료 | | `REV-P9-002` | Low | 수정 완료 | API에 없는 creator ID를 content/post ID로 합성해 Feed model에 전달함 | Task 9.6 | `P9-R2` 완료 | ## 6. 발견 사항 상세 matching payload가 null이면 `mapNotNull`로 제외되고, content image null은 UI model까지 보존된다. `visibleFromAtUtc` 기준 상대 시간과 payload별 target ID도 현재 계약과 일치한다. Task 9.7에서 삭제한 `FeedAdapter`와 `FeedImageViews`는 production/test 호출자가 없고, 최근 소식은 현재 `FeedItem`과 타입별 Feed view를 직접 재사용하므로 삭제에 따른 확정 발견 사항은 없다. ### REV-P9-001 — nullable 최근 소식 이미지 바인딩이 이전 Coil 요청을 해제하지 않음 - **심각도:** Medium - **상태:** 수정 완료 - **관련 요구사항:** PRD `contentImageUrl`·`imageUrl` nullable 계약, Task 9.3 Feed image fallback - **소유 Task:** Task 9.5 **관찰 내용** `HomeFollowingNewsAdapter.bindImage()`는 blank URL에서 `setImageDrawable(null)`만 호출한다. 같은 ViewHolder가 이미지가 있는 item에서 nullable image item으로 재사용될 때 이전 Coil 요청이 진행 중이면, 그 요청이 완료되면서 현재 item에 이전 이미지가 표시될 수 있다. **근거** - 코드: `HomeFollowingNewsAdapter.kt:187-203`의 blank 분기에 `dispose()`가 없음 - 계약: `FollowingContentNewsResponse.contentImageUrl`, `FollowingContentRankingNewsResponse.contentImageUrl`, `FollowingCommunityPostNewsResponse.imageUrl`은 nullable - 테스트: `HomeFollowingMapperTest`는 null 보존만 확인하고 adapter 재사용 시 이미지 요청 상태는 검증하지 않음 - 저장소 정상 사용례: `HomePopularCommunityAdapter.kt:67-70`은 blank image에서 `imageView.dispose()`와 drawable 초기화를 함께 수행함 - Coil 2.7.0 로컬 source: `ImageView.dispose()`는 View의 현재 요청을 취소하고 분리함 **재현 또는 검증 절차** 1. 최근 소식 ViewHolder의 image view에 item A의 완료 전 Coil 요청을 연결한다. 2. 같은 holder에 `contentImageUrl = null` 또는 `imageUrl = null`인 item B를 바인딩한다. 3. 현재 blank 분기는 drawable만 비우므로 요청 A가 남는다. 4. 요청 A가 완료되면 B에 A의 이미지가 표시될 수 있다. 요구 결과는 blank 바인딩 시 요청을 해제하고 빈 이미지 상태를 유지하는 것이다. **영향** 빠른 스크롤이나 느린 이미지 응답 조건에서 ranking/content/community 최근 소식 카드에 다른 item의 이미지가 노출될 수 있다. **권장 조치** 공유되는 `bindImage()` blank 분기에서 `imageView.dispose()` 후 drawable을 비운다. 한 대표 nullable content item의 ViewHolder 재사용을 Robolectric test로 고정하고 mapper/Feed 회귀를 실행한다. **판정 기록** - 2026-07-30 — nullable DTO·adapter binding·Coil request lifecycle·저장소 내 정상 해제 패턴을 대조해 확정. - 2026-07-30 — 구현 전 Phase 9 Task 9.5 / `P9-R1`로 전환. - 2026-07-30 — Task 9.5에서 `HomeFollowingNewsAdapter.bindImage()` blank 분기에 `dispose()`를 추가하고, Robolectric adapter binding test와 팔로잉 회귀 검증을 완료. ### REV-P9-002 — API에 없는 creator ID를 다른 target ID로 합성함 - **심각도:** Low - **상태:** 수정 완료 - **관련 요구사항:** Task 6.4의 API 미제공 값 비합성 정책, Phase 9 nested payload 계약 - **소유 Task:** 신규 Task 9.6 **관찰 내용** `FollowingContentNewsResponse`와 `FollowingCommunityPostNewsResponse`에는 `creatorId`가 없지만 adapter가 `FeedItem.Content.creatorId`에는 `content.targetId`, `FeedItem.Community.creatorId`에는 `community.targetId`를 문자열로 넣는다. 두 target은 각각 `contentId`, `postId`이므로 creator ID가 아니다. **근거** - DTO: `HomeFollowingModels.kt`의 content/community nested payload에는 `creatorId` 없음 - mapper: content `targetId = contentId`, community `targetId = postId` - adapter: `HomeFollowingNewsAdapter.kt`에서 두 `FeedItem.creatorId`에 각각 위 target ID를 대입 - 현재 클릭 경로: adapter callback은 원본 `HomeFollowingNewsUiItem`을 캡처하므로 현행 상세 이동에는 잘못된 `creatorId`가 사용되지 않음 **재현 또는 검증 절차** 1. content/community nested payload와 UI model의 target ID 의미를 대조한다. 2. 두 Feed item binding의 `creatorId` 대입값을 확인한다. 3. 실제 결과는 content/post ID가 creator ID 필드에 들어가며, 요구 결과는 서버가 제공하지 않은 ID를 합성하지 않는 것이다. **영향** 현재 화면 클릭에는 직접 영향이 없지만 Feed model의 의미가 잘못되고, 공통 Feed 위젯이 추후 creator ID를 사용하면 잘못된 크리에이터 이동이나 분석 값으로 이어질 수 있다. **권장 조치** 서버 계약을 추정해 확장하지 말고 두 Feed item의 `creatorId`를 빈 값으로 전달한다. 실제 `contentId`, `postId`와 기존 원본 UI model click callback은 유지한다. **판정 기록** - 2026-07-31 — DTO→mapper→adapter→click callback을 추적해 확정하고 Phase 9 Task 9.6 / `P9-R2`로 전환. - 2026-07-31 — Task 9.6에서 `FeedItem.Content`와 `FeedItem.Community`의 `creatorId`를 빈 값으로 전달하도록 수정하고 focused/팔로잉 회귀 검증을 완료. ## 7. 확정 항목의 plan·goal 전환 - Phase 9에 Task 9.5 / `P9-R1`을 추가했으며 구현·검증을 완료했다. - 최종 재점검에서 Task 9.6 / `P9-R2`를 추가했으며 creator ID 합성 제거를 완료했다. - Task 9.7 / `P9-R3`의 미사용 `FeedAdapter` 제거와 문서 정리를 현재 호출자 검색·컴파일·회귀 결과로 확인했다. ## 8. 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | 리뷰 범위 전체 확인 | 충족 | DTO→mapper→adapter 추적과 Task 9.7 미사용 호출자 검색 | | 후보 항목 판정 완료 | 충족 | REV-P9-001~002 판정 완료 | | 확정 항목 plan 반영 | 충족 | Task 9.5, Task 9.6 추가 | | 검증 명령과 결과 기록 | 충족 | focused/통합 PASS | **최종 결론:** 확정 발견 사항 모두 수정 완료 **남은 항목:** 없음 ## 9. 수정 후 검증 기록 - 2026-07-30: `REV-P9-001` 보완으로 `HomeFollowingNewsAdapter.bindImage()`의 blank image 분기에서 기존 Coil 요청을 `dispose()`로 해제한 뒤 drawable을 비우도록 변경했다. 구현 전 `HomeFollowingNewsAdapterTest` RED, 구현 후 focused GREEN, 팔로잉 전체 회귀와 신규 프로필 테스트, `:app:mergeDebugResources`, `:app:compileDebugKotlin`, `:app:ktlintCheck`, `git diff --check` PASS. - 2026-07-31 재점검: nested payload mapper·nullable image 회귀를 포함한 Phase 1~13 focused 113개 test, resource merge, Kotlin compile, ktlint PASS. 추가 확정 발견 사항 없음. - 2026-07-31 최종 재점검: API에 없는 creator ID를 content/post ID로 대신 채우는 binding을 `REV-P9-002`로 확정하고, production/test 코드를 변경하기 전에 Task 9.6 / `P9-R2`로 전환했다. - 2026-07-31 최종 검증: nested payload·adapter 회귀를 포함한 통합 20개 suite·306개 test와 resource merge, Kotlin compile, ktlint가 PASS했다. 자동 검증 성공과 별개로 ID 의미 계약 불일치 `REV-P9-002`는 Task 9.6의 수정 항목으로 유지했다. - 2026-07-31: `REV-P9-002` 보완으로 `HomeFollowingNewsAdapter`의 content/community Feed model에서 API가 제공하지 않는 `creatorId`를 content/post ID로 합성하지 않고 빈 값으로 전달하도록 변경했다. RED focused test 실패를 확인한 뒤 GREEN으로 전환했고, focused test, 팔로잉 전체 회귀, `:app:compileDebugKotlin`, `:app:ktlintCheck` PASS. - 2026-07-31 이번 리뷰 재검증: nested payload·nullable image·ID 의미와 Task 9.7의 미사용 `FeedAdapter` 삭제 상태를 다시 대조했다. production/test `FeedAdapter|FeedImageViews` 호출자는 0건이며, 통합 16개 suite·120개 test, resource merge, Kotlin compile, ktlint가 PASS해 추가 확정 발견 사항이 없다.