From f74f5a534d488886cc560d726ebf708d48c45dcf Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 30 Jun 2026 23:54:21 +0900 Subject: [PATCH] =?UTF-8?q?docs(home):=20=ED=8C=94=EB=A1=9C=EC=9E=89=20?= =?UTF-8?q?=EC=B5=9C=EA=B7=BC=20=EC=86=8C=EC=8B=9D=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20=EA=B8=B0=EB=A1=9D=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/20260625_메인_홈_팔로잉_탭/plan-task.md | 94 +++++++++++++++++++- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/docs/20260625_메인_홈_팔로잉_탭/plan-task.md b/docs/20260625_메인_홈_팔로잉_탭/plan-task.md index ec68c47f..f4f10c50 100644 --- a/docs/20260625_메인_홈_팔로잉_탭/plan-task.md +++ b/docs/20260625_메인_홈_팔로잉_탭/plan-task.md @@ -20,7 +20,7 @@ - 로그인 유도 화면의 실제 UI, 문구, CTA, 로그인 완료 후 복귀 정책은 별도 확정 후 구현한다. - `recentNews` 시간은 `visibleFromAtUtc`를 디바이스 타임존 기준으로 상대 시간 표시한다. - `PHOTO_CONTENT` label은 우선 `화보`로 표시한다. -- ranking news의 `rank`가 null이면 해당 news item은 표시하지 않는다. +- `recentNews`는 `type`에 대응하는 nested payload가 null이면 해당 news item을 표시하지 않는다. - `monthlySchedules`는 서버가 이번 달 범위로 정렬해서 내려주며 앱은 재정렬하지 않는다. - 섹션 title chevron은 터치 콜백까지만 연결하고 실제 이동 목적지는 만들지 않는다. - 구현 완료 후 최소 다음 명령을 실행한다. @@ -43,14 +43,16 @@ - 팔로잉 크리에이터, On Air, 최근 대화, 이달의 스케줄, 최근 소식 섹션 배치와 spacing은 Figma `24:5682`를 기준으로 확인한다. - Phase 5: 필수 참조 - 최종 수동 화면 검증은 PRD의 포함/제외 항목과 실제 화면을 대조한다. +- Phase 9: Figma 참조 불필요 + - 최근 소식 Response 계약 변경은 DTO/mapper/test fixture 중심으로 진행하고, 기존 Feed 위젯 표시 정책과 레이아웃은 유지한다. --- ## 파일 구조 - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingApi.kt` - `GET /api/v2/home/following` Retrofit endpoint를 정의한다. -- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt` - - `HomeFollowingTabResponse`, `FollowingCreatorResponse`, `FollowingLiveResponse`, `FollowingScheduleResponse`, `FollowingNewsResponse`, `FollowingNewsType` DTO를 정의한다. +- Create/Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt` + - `HomeFollowingTabResponse`, `FollowingCreatorResponse`, `FollowingLiveResponse`, `FollowingScheduleResponse`, `FollowingNewsResponse`, `FollowingCreatorRankingNewsResponse`, `FollowingContentNewsResponse`, `FollowingContentRankingNewsResponse`, `FollowingCommunityPostNewsResponse`, `FollowingNewsType` DTO를 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingRepository.kt` - API 호출을 repository method로 감싼다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingUiState.kt` @@ -58,7 +60,7 @@ - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingUiModels.kt` - 팔로잉 크리에이터, live, chat, schedule, news section/item UI model을 정의한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingMappers.kt` - - DTO를 UI model/state로 변환하고, `rank == null` news 숨김, `PHOTO_CONTENT` label, `visibleFromAtUtc` 시간 기준을 적용한다. + - DTO를 UI model/state로 변환하고, matching nested payload null news 숨김, `PHOTO_CONTENT` label, `visibleFromAtUtc` 시간 기준을 적용한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingAuthHeader.kt` - blank token이면 `null`, 값이 있으면 `Bearer {token}`을 반환한다. - Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingViewModel.kt` @@ -634,6 +636,88 @@ --- +### Phase 9: 최근 소식 Response nested payload 계약 반영 + +- [x] **Task 9.1: 최근 소식 새 응답 계약 RED 테스트 추가** + - 수정: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingMapperTest.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt` + - 작업: + - `FollowingNewsResponse` fixture를 새 nested payload 구조로 교체한다. + - `CREATOR_RANKING`은 `creatorRanking.rank`, `creatorRanking.creatorId`, `creatorRanking.nickname`, `creatorRanking.profileImageUrl`로 ranking UI item을 만드는지 검증한다. + - `CONTENT_RANKING`은 `contentRanking.rank`, `contentRanking.contentId`, `contentRanking.contentImageUrl`, `contentRanking.title`로 ranking UI item을 만드는지 검증한다. + - `AUDIO_CONTENT`는 `audioContent`, `PHOTO_CONTENT`는 `photoContent` payload로 content UI item을 만드는지 검증한다. + - `COMMUNITY_POST`는 `communityPost.postId`, `creatorProfileImage`, `creatorNickname`, `imageUrl`, `content`, `createdAt`, `likeCount`, `commentCount`를 UI item에 반영하는지 검증한다. + - `type`에 대응하는 payload가 null인 최근 소식은 `recentNews` section에서 제외되는지 검증한다. + - `visibleFromAtUtc`는 기존처럼 `UtcRelativeTimeTextFormatter`에 전달되는지 검증한다. + - source test로 `HomeFollowingModels.kt`에 기존 평면 필드(`creatorProfileImageUrl`, `creatorNickname`, `title`, `body`, `thumbnailImageUrl`, `targetId`, `occurredAtUtc`, `rank`)가 `FollowingNewsResponse` 직접 필드로 남지 않는지 확인한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"` + - Expected: DTO/UI model/mapper 변경 전 RED 실패. + - Result: RED 확인. `FollowingCommunityPostNewsResponse`, `FollowingContentNewsResponse`, `FollowingContentRankingNewsResponse`, `FollowingCreatorRankingNewsResponse` 미정의와 `FollowingNewsResponse` nested parameter 미지원, 신규 UI item 필드 부재로 `compileDebugUnitTestKotlin`이 실패했다. + +- [x] **Task 9.2: DTO와 UI model을 nested payload 기준으로 갱신** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/data/HomeFollowingModels.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingUiModels.kt` + - `app/src/main/res/values/strings.xml` + - `app/src/main/res/values-en/strings.xml` + - `app/src/main/res/values-ja/strings.xml` + - 작업: + - `FollowingNewsResponse` 직접 필드는 `newsId`, `type`, `visibleFromAtUtc`, `creatorRanking`, `audioContent`, `photoContent`, `contentRanking`, `communityPost`만 유지한다. + - `FollowingCreatorRankingNewsResponse`, `FollowingContentNewsResponse`, `FollowingContentRankingNewsResponse`, `FollowingCommunityPostNewsResponse` DTO를 추가하고 모두 `@Keep`, `@SerializedName`을 적용한다. + - `HomeFollowingNewsUiItem`은 새 payload 차이를 표현할 수 있도록 ranking/content/community variant를 분리하거나 기존 variant를 최소 확장한다. + - ranking UI item은 `rank`, target id, 표시 이미지 URL, 표시 제목/이름, `visibleFromAtUtc`, `visibleFromText`를 가진다. + - content UI item은 `contentId`, `contentImageUrl`, `title`, `creatorProfileImageUrl`, `creatorNickname`, label resource id, `visibleFromAtUtc`, `visibleFromText`를 가진다. + - community UI item은 `postId`, `creatorProfileImageUrl`, `creatorNickname`, `imageUrl`, `content`, `createdAt`, `likeCount`, `commentCount`, `visibleFromAtUtc`, `visibleFromText`를 가진다. + - `CONTENT_RANKING` 문장형 message가 필요하면 `screen_home_following_content_ranking_news_message`를 3개 locale string resource에 추가한다. + - 검증: + - Run: `./gradlew :app:mergeDebugResources` + - Expected: string resource가 중복 없이 merge된다. + - Run: `./gradlew :app:compileDebugKotlin` + - Expected: DTO/UI model 변경 후 컴파일 오류가 mapper/adapter 미반영 지점으로만 제한된다. + - Result: PASS. `FollowingNewsResponse` 직접 필드를 nested payload 계약으로 교체하고, ranking/content/community UI model variant 및 3개 locale content ranking message string을 추가한 뒤 최종 `mergeDebugResources`, `compileDebugKotlin`이 통과했다. + +- [x] **Task 9.3: mapper와 최근 소식 adapter 바인딩 갱신** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeFollowingMappers.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeFollowingNewsAdapter.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeFollowingFragmentSourceTest.kt` + - 작업: + - mapper는 `type`별 matching payload만 읽고, payload가 null이면 `null`을 반환해 해당 item을 제외한다. + - `CREATOR_RANKING` adapter binding은 기존 `screen_home_following_creator_ranking_news_message`와 `FeedRankHighlight` 정책을 유지하되 `creatorRanking.nickname`, `creatorRanking.rank`, `creatorRanking.profileImageUrl`을 사용한다. + - `CONTENT_RANKING` adapter binding은 content title, rank, content image로 `FeedRankView`를 구성하고 순위 텍스트만 highlight한다. + - `AUDIO_CONTENT`, `PHOTO_CONTENT` adapter binding은 `FollowingContentNewsResponse` payload의 content/creator 필드를 `FeedContentView`에 전달한다. + - `COMMUNITY_POST` adapter binding은 `FeedCommunityView`에 `postId`, creator profile/nickname, content, image, `likeCount`, `commentCount`를 전달한다. + - 최근 소식 item click target id는 `creatorId`, `contentId`, `postId` 중 payload에 맞는 값을 사용한다. + - `visibleFromAtUtc` 기준 상대 시간 표시 정책은 유지한다. + - 코드 리뷰에서 확인한 `contentImageUrl` nullable 계약을 DTO/UI model에 반영하고, Feed 위젯에는 빈 문자열 fallback으로 전달한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"` + - Expected: PASS. + - Result: PASS. 최초 병렬 Gradle 실행 중 Kotlin incremental cache 충돌과 source-level test 기대 문자열 불일치 2건을 확인했다. Gradle을 `--no-daemon` 순차 실행으로 전환하고 source test 기대 문자열을 새 `HomeFollowingNewsUiItem.Community` 분기 구조에 맞춘 뒤 focused test가 PASS했다. + - 2026-06-30 코드 리뷰 후 보완 검증: `contentImageUrl = null`인 `CONTENT_RANKING`, `AUDIO_CONTENT` 최근 소식이 유지되는 mapper test를 추가했다. RED: DTO `contentImageUrl`이 non-null `String`이라 `compileDebugUnitTestKotlin` type mismatch 실패. GREEN: DTO/UI model을 `String?`로 변경하고 adapter에서 `.orEmpty()` fallback을 적용한 뒤 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest"` PASS. + +- [x] **Task 9.4: 최근 소식 Response 변경 통합 검증** + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"` + - Expected: 팔로잉 관련 local unit/source test가 모두 PASS. + - Run: `./gradlew :app:mergeDebugResources` + - Expected: layout/resource merge PASS. + - Run: `./gradlew :app:compileDebugKotlin` + - Expected: Kotlin compile PASS. + - Run: `./gradlew :app:ktlintCheck` + - Expected: ktlint PASS 또는 기존 unrelated failure만 발생. + - Run: `git diff --check` + - Expected: whitespace error 없음. + - 검증 기록: + - 2026-06-30 Phase 9 focused 검증: `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingMapperTest" --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"` PASS. + - 2026-06-30 Phase 9 통합 검증: `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` 모두 PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. + - 2026-06-30 Phase 9 검증 참고: 최초 병렬 Gradle 검증은 Kotlin incremental cache `Storage ... is already registered` 충돌과 timeout으로 중단되어, 이후 모든 Gradle 검증은 `--no-daemon` 순차 실행으로 재검증했다. + - 2026-06-30 Phase 9 코드 리뷰 후 재검증: `contentImageUrl` nullable 계약을 보완한 뒤 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. + +--- + ## Verification Log - 2026-06-25 Phase 1-3 구현 검증: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` 모두 PASS. - 2026-06-25 Phase 1-3 코드 리뷰 및 재검증: blocking finding 없음. `./gradlew :app:mergeDebugResources`는 최초 sandbox lock 권한 오류 후 승인 실행으로 PASS했고, 나머지 검증 명령도 PASS. @@ -648,3 +732,5 @@ - 2026-06-30 content feed clipping 위치 수정 검증: `docs/agent-guides/code-style.md`의 XML `android:clipToOutline` 금지 규칙에 맞춰 `view_feed_content.xml` 속성을 제거하고 `FeedContentView` Kotlin 코드에서 root `clipToOutline`/`outlineProvider`를 설정했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.feed.FeedViewTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `git diff --check` PASS. - 2026-06-30 팔로잉 크리에이터 `전체` 버튼 후속 검증: 팔로잉 크리에이터 RecyclerView 마지막에 `전체` item을 추가하고 `FollowingCreatorActivity` 이동을 연결했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"` RED/GREEN, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` PASS. `mergeDebugResources`는 최초 sandbox lock 권한 오류 후 승인 실행으로 PASS했고, `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. - 2026-06-30 팔로잉 크리에이터 `전체` 버튼 높이 보정 후 재검증: `전체` item root는 `wrap_content` 높이로 두고 invisible nickname spacer로 creator item 전체 높이를 맞췄다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeFollowingFragmentSourceTest"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`, `git diff --check` PASS. `mergeDebugResources`는 최초 sandbox lock 권한 오류 후 승인 실행으로 PASS했고, `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. +- 2026-06-30 Phase 9 최근 소식 Response nested payload 계약 반영 검증: `FollowingNewsResponse`를 `newsId`, `type`, `visibleFromAtUtc`, type별 nested payload만 갖는 구조로 갱신하고 mapper/adapter를 matching payload 기준으로 변경했다. RED: 새 nested DTO/UI 필드 미구현으로 `compileDebugUnitTestKotlin` 실패. GREEN: focused test PASS. 통합 검증 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` 모두 PASS. `ktlintCheck`에서는 기존 `.editorconfig disabled_rules` deprecation warning만 출력됐다. +- 2026-06-30 Phase 9 코드 리뷰 후 nullable 계약 보완 검증: PRD의 `contentImageUrl: String?` 계약과 구현의 non-null DTO/UI model 불일치를 수정했다. RED: null image fixture 추가 시 `compileDebugUnitTestKotlin` type mismatch 실패. GREEN: `contentImageUrl` nullable DTO/UI model과 adapter `.orEmpty()` fallback 적용 후 focused mapper test PASS. 통합 재검증 `./gradlew --no-daemon :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*Following*"`, `./gradlew --no-daemon :app:mergeDebugResources`, `./gradlew --no-daemon :app:compileDebugKotlin`, `./gradlew --no-daemon :app:ktlintCheck`, `git diff --check` PASS.