From fa4e41589b74cce3300455b142745196cd1557d8 Mon Sep 17 00:00:00 2001 From: klaus Date: Thu, 18 Jun 2026 15:22:17 +0900 Subject: [PATCH] =?UTF-8?q?docs(creator):=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20?= =?UTF-8?q?=ED=83=AD=20=ED=9B=84=EC=86=8D=20=EA=B2=80=EC=A6=9D=EC=9D=84=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 225 +++++++++++++++++- .../20260617_크리에이터_채널_라이브_탭/prd.md | 18 +- 2 files changed, 240 insertions(+), 3 deletions(-) diff --git a/docs/20260617_크리에이터_채널_라이브_탭/plan-task.md b/docs/20260617_크리에이터_채널_라이브_탭/plan-task.md index deaf8633..e2a2b74f 100644 --- a/docs/20260617_크리에이터_채널_라이브_탭/plan-task.md +++ b/docs/20260617_크리에이터_채널_라이브_탭/plan-task.md @@ -287,7 +287,7 @@ - 기대 결과: - ViewModel/pagination 테스트가 PASS한다. - 검증 기록: - - 2026-06-17: `CreatorChannelLiveViewModel`을 추가하고 `Loading`, `Empty`, `Error`, `Content` 상태를 정의했다. 초기 로드는 `page=0`, `size=10`, `sort=LATEST`로 요청하고, pagination은 마지막 성공 응답의 `page + 1`로 append하며 `isLoadingMore`로 중복 요청을 차단한다. + - 2026-06-17: `CreatorChannelLiveViewModel`을 추가하고 `Loading`, `Empty`, `Error`, `Content` 상태를 정의했다. 초기 로드는 `page=0`, `size=20`, `sort=LATEST`로 요청하고, pagination은 마지막 성공 응답의 `page + 1`로 append하며 `isLoadingMore`로 중복 요청을 차단한다. - 2026-06-17: 정렬 변경은 기존 목록/page를 초기화해 첫 페이지를 다시 요청하고, 같은 정렬 재선택은 API를 재호출하지 않는다. 최초 로드 전 정렬 변경은 기본 `LATEST` 최초 로드 계약을 바꾸지 않도록 no-op 처리했다. - 2026-06-17: 리뷰 게이트에서 정렬 변경 중 이전 첫 페이지/load-more 응답이 최신 상태를 덮어쓸 수 있다는 지적이 있어 RED 테스트를 추가했다. production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `이전 첫 페이지 응답은 이후 정렬 변경 결과를 덮어쓰지 않는다`, `이전 load-more 응답은 이후 정렬 변경 목록에 append되지 않는다` 2개 테스트가 실패함을 확인했다. - 2026-06-17: `requestGeneration` guard를 추가해 오래된 첫 페이지/load-more 성공 또는 실패 응답을 무시하도록 보정했다. 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL`로 통과했다. @@ -556,7 +556,7 @@ ### Phase 8: 최종 검증과 문서 기록 -- [ ] **Task 8.1: 자동 검증 실행** +- [x] **Task 8.1: 자동 검증 실행** - 실행 명령: - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` @@ -568,6 +568,12 @@ - 모든 명령이 `BUILD SUCCESSFUL`로 통과한다. - 검증 기록: - 실행 일시, 명령, 결과, 기존 경고 여부를 이 Task 아래에 누적 기록한다. + - 2026-06-18: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` 실행 결과 `BUILD SUCCESSFUL in 12s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + - 2026-06-18: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` 실행 결과 `BUILD SUCCESSFUL in 11s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + - 2026-06-18: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` 실행 결과 `BUILD SUCCESSFUL in 14s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + - 2026-06-18: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 1s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + - 2026-06-18: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 21s`로 통과했다. Gradle daemon 1개 busy로 신규 daemon이 시작됐고, Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + - 2026-06-18: `./gradlew :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 16s`로 통과했다. Gradle daemon 1개 busy로 신규 daemon이 시작됐고, Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. - [ ] **Task 8.2: 수동 확인** - 확인 항목: @@ -584,6 +590,11 @@ - CTA가 마지막 item을 가리지 않는다. - 검증 기록: - 실제 확인한 시나리오와 결과를 이 Task 아래에 한국어로 누적 기록한다. + - 2026-06-18: 연결 단말 확인을 위해 `adb devices`를 실행했고 `2cec640c34017ece device`가 연결되어 있음을 확인했다. `adb shell pm list packages | rg "sodalive"` 결과 `kr.co.vividnext.sodalive.debug`, `kr.co.vividnext.sodalive`가 설치되어 있음을 확인했다. + - 2026-06-18: `CreatorChannelActivity`를 `extra_creator_id=1`로 직접 실행해 Live 탭 실제 화면 검증을 시도했으나, Activity가 exported 대상이 아니어서 `Permission Denial ... not exported` 보안 예외로 차단됐다. + - 2026-06-18: `adb shell monkey -p kr.co.vividnext.sodalive.debug 1`로 디버그 앱 런처 실행은 성공했다. 실행 후 focus는 `kr.co.vividnext.sodalive.debug/com.gun0912.tedpermission.TedPermissionActivity`로 확인되어 권한 화면 진입까지만 실제 단말 표면으로 확인했다. + - 2026-06-18: Live 탭 상세 화면은 로그인/권한/앱 내부 이동 및 서버 데이터가 필요한 경로라 현재 shell 직접 실행으로는 Figma 대비 실제 화면 대조를 완료하지 못했다. 따라서 Task 8.2는 남은 실제 수동 QA로 유지한다. + - 2026-06-18: 자동 검증과 소스/테스트 근거로 `page=0`/`sort=LATEST` 최초 조회, 정렬 변경/동일 정렬 no-op, load-more append, `isOwned && isRented` 시 `소장중` 우선, `seriesName` 미표시, owner CTA visibility/inset/click 중복 방지, `ic_new_sort` 연결은 확인했다. 실제 화면의 Sort popup 표시/닫힘, current live 카드 렌더링, CTA가 마지막 item을 가리지 않는지는 후속 실제 단말 시나리오에서 확인이 필요하다. --- @@ -652,8 +663,211 @@ - 2026-06-17: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLivePaginationTest"` 최초 실행은 신규 테스트 fixture의 `CreatorChannelLiveResponse` 필드명 오기로 컴파일 실패했고, `coverImageUrl`로 수정 후 재실행 결과 `BUILD SUCCESSFUL`로 통과했다. - 2026-06-17: 추가 회귀 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck`를 실행했고 모두 `BUILD SUCCESSFUL`로 통과했다. `ktlintCheck`에서는 기존 `.editorconfig`의 `disabled_rules` deprecation 경고가 계속 출력되었다. + --- +### Phase 6 Follow-up: CTA Figma 고정 영역 보정 + +- [x] **Task RF6.1: 라이브 시작하기 CTA를 Figma `665:19371` 기준으로 보정** + - 수정: + - `app/src/main/res/layout/fragment_creator_channel_live.xml` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` + - 작업: + - CTA outer 영역은 화면 하단에 고정된 100dp black container로 유지한다. + - 내부 버튼은 좌우 14dp, top 14dp 위치의 `soda_400` capsule로 표시한다. + - CTA는 스크롤 컨텐츠가 아니라 Fragment root에 고정해 스크롤하지 않아도 보이게 한다. + - CTA 표시 시 리스트 하단 padding은 CTA 100dp 영역과 navigation bar inset을 반영해 마지막 item이 가려지지 않게 한다. + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` + - `./gradlew :app:mergeDebugResources` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + - 기대 결과: + - 본인 채널의 `라이브 시작하기` CTA가 Figma처럼 하단 고정 100dp 영역 안에 표시되고, 스크롤하지 않아도 보인다. + - 검증 기록: + - 2026-06-18: Figma `665:19359`, CTA layer `665:19371`을 재확인했다. CTA는 `bottom=0`, `height=100`, black 배경의 outer 영역이며, 내부 버튼은 `left/right=14`, `top=14`, `soda_400 #00BDF7`, 24dp icon, 18sp medium label 구조다. 기존 구현은 56dp capsule 자체를 parent 하단에 붙이는 구조라 Figma outer CTA 영역과 달라 보정한다. + - 2026-06-18: production 보정 전 `CreatorChannelLiveFragmentLayoutTest`를 실행해 Activity root overlay CTA 계약 미충족으로 실패함을 확인했다. 최초 RED 시도는 테스트 문자열 escaping 오류로 컴파일 실패했고, 테스트 문자열을 수정한 뒤 `CreatorChannelLiveFragmentLayoutTest > 라이브 owner CTA는 Activity root overlay로 Figma 고정 영역을 제공한다`가 assertion 실패로 RED가 됐다. + - 2026-06-18: CTA를 `fragment_creator_channel_live.xml`에서 제거하고 `activity_creator_channel.xml` root overlay로 이동했다. Activity overlay는 100dp black container, 내부 52dp `soda_400` capsule, 좌우/상단 14dp margin, `ic_new_create_live`, `creator_channel_live_start_button` 구조로 Figma `665:19371`을 맞춘다. + - 2026-06-18: `CreatorChannelActivity`가 Live 탭이면서 본인 채널일 때만 CTA를 표시하도록 `updateLiveOwnerCtaVisibility()`를 추가했다. navigation bar inset은 CTA container 높이와 `nestedScrollView` bottom padding에 반영해 CTA가 화면 하단에 고정되어도 컨텐츠가 가려지지 않도록 했다. + - 2026-06-18: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 33s`로 GREEN을 확인했다. + - 2026-06-18: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 1s`로 통과했다. + - 2026-06-18: `./gradlew :app:ktlintCheck` 최초 실행은 제가 추가한 빈 줄 1개로 실패했고, 빈 줄 제거 후 재실행 결과 `BUILD SUCCESSFUL in 31s`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 계속 출력되었다. + - 2026-06-18: 병렬 Gradle 실행 중 Kotlin incremental cache 경합으로 `compileDebugKotlin`이 실패/timeout 되었으나 `./gradlew --stop` 후 순차 재실행한 `./gradlew :app:compileDebugKotlin`은 `BUILD SUCCESSFUL in 2m 4s`로 통과했다. 기존 deprecation/annotation 경고는 변경 범위와 무관하다. + - 2026-06-18: 최종 확인으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`를 순차 재실행해 `BUILD SUCCESSFUL in 27s`로 통과했다. + +- [x] **Task RF6.2: 실제 기기 bottom 위치와 짧은 Live 상태 스크롤 보정** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` + - `app/src/main/res/layout/fragment_creator_channel_live.xml` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` + - 작업: + - `BaseActivity` root bottom padding과 `owner_fab_button`/`layout_creator_channel_live_owner_cta`의 navigation inset 중복 적용을 제거한다. + - Live 탭의 empty/error처럼 짧은 상태에서는 `ViewPager2` page가 viewport 높이로 강제 확장되지 않아 스크롤 없이 안내 문구와 retry 버튼이 보이도록 한다. + - CTA 표시 시 content bottom padding은 CTA 100dp 영역만 반영하고 navigation inset은 root padding에 맡긴다. + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` + - `./gradlew :app:mergeDebugResources` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + - 기대 결과: + - 실제 기기에서 owner FAB와 live CTA가 XML preview 기준 bottom 위치와 동일한 safe-area 기준으로 표시된다. + - Live 탭 error/empty 같은 짧은 상태에서 불필요한 긴 스크롤이 생기지 않는다. + - 검증 기록: + - 2026-06-18: 사용자 실제 기기 확인 결과 `owner_fab_button`과 `layout_creator_channel_live_owner_cta`가 XML preview보다 위에 표시되고, Live 탭 error/empty 짧은 상태에서도 Fragment 높이가 길어 스크롤이 발생함을 확인 요청받았다. 코드 확인 결과 `BaseActivity`가 root에 system bar bottom padding을 이미 적용하는데 `CreatorChannelActivity`가 동일 navigation inset을 FAB margin과 CTA height/padding에 추가로 반영하고 있어 bottom 위치가 중복 보정될 수 있음을 확인했다. + - 2026-06-18: RED 확인으로 `CreatorChannelActivitySourceTest`와 `CreatorChannelLiveFragmentLayoutTest`에 navigation inset 중복 제거, `currentPage.minimumHeight` 강제 제거, Live root `wrap_content` 계약을 추가한 뒤 실행했다. production 보정 전 `CreatorChannelActivitySourceTest > 라이브 탭 pagination과 높이 갱신은 NestedScrollView 소유 스크롤 경로에서 처리한다`, `CreatorChannelActivitySourceTest > 크리에이터 채널 하단 고정 UI는 BaseActivity root bottom padding과 navigation inset을 중복 적용하지 않는다`, `CreatorChannelLiveFragmentLayoutTest > 라이브 fragment layout은 sort current live list empty error owner CTA를 제공한다`, `CreatorChannelLiveFragmentLayoutTest > 라이브 fragment와 adapter source는 필수 drawable과 retry loadMore click 연결을 포함한다`가 실패함을 확인했다. + - 2026-06-18: `CreatorChannelActivity.setupOwnerFabInsets()`에서 FAB margin에 navigation inset을 더하던 listener를 제거하고, ViewPager content bottom padding만 유지했다. Live CTA도 Activity root overlay의 XML 100dp height를 유지하고, `nestedScrollView` bottom padding에는 CTA 100dp만 반영하도록 변경했다. navigation bar 대응은 `BaseActivity` root bottom padding에 맡긴다. + - 2026-06-18: 짧은 Live 상태에서 불필요하게 긴 스크롤이 생기지 않도록 `updateViewPagerHeight()`의 `currentPage.minimumHeight = calculateCreatorChannelTabViewportHeight()` 강제 설정을 제거하고, `fragment_creator_channel_live.xml` root를 `wrap_content`로 변경했다. empty/error 안내 영역은 parent bottom constraint를 제거해 실제 표시 컨텐츠 높이만 측정되도록 했다. + - 2026-06-18: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 30s`로 통과했다. + - 2026-06-18: `./gradlew :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 2s`로 통과했다. + - 2026-06-18: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 1s`로 통과했다. + - 2026-06-18: `./gradlew :app:ktlintCheck` 최초 실행은 테스트 source assertion 2줄의 line length/argument wrapping 위반으로 실패했고, 줄바꿈 보정 후 재실행 결과 `BUILD SUCCESSFUL in 6s`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 계속 출력되었다. + - 2026-06-18: 포맷 보정 후 최종 targeted 재검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`를 실행해 `BUILD SUCCESSFUL in 13s`로 통과했다. + +--- + +### Phase 7 Follow-up: 탭 전환 시 sticky tabbar anchor 보정 + +- [x] **Task RF7.1: 탭 전환 시 sticky 위치로 먼저 정렬한 뒤 컨텐츠 표시** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - 판단: + - 현재 구조는 `NestedScrollView`가 전체 스크롤을 소유하고, `ViewPager2` page 높이가 선택 탭 컨텐츠 높이에 맞춰 바뀐다. + - 짧은 탭으로 전환하면 parent scroll range가 줄면서 기존 sticky 상태가 풀려 tabbar가 header 아래로 갑자기 되돌아가 보일 수 있다. + - 탭을 선택한 순간 사용자의 목적은 header 재확인이 아니라 선택한 탭 컨텐츠 탐색이므로, tabbar를 sticky anchor로 고정한 뒤 컨텐츠를 보여주는 편이 전환감과 예측 가능성이 좋다. + - 따라서 sticky 상태가 아니더라도 다른 탭 선택 시 `NestedScrollView`를 sticky threshold까지 보정하는 방식을 채택한다. + - 작업: + - sticky threshold 계산은 기존 `updateScrollState()`와 같은 기준을 사용한다. + - `stickyTop = CreatorChannelScrollState.calculateStickyTop(statusBarHeight, baseTitleBarHeight)` + - `stickyScrollY = (binding.headerContainer.height - stickyTop).coerceAtLeast(0)` + - `ViewPager2.OnPageChangeCallback.onPageSelected(position)`에서 이전 탭과 다른 탭으로 전환될 때 sticky 보정을 수행한다. + - 현재 `nestedScrollView.scrollY`가 `stickyScrollY`보다 작은 경우에만 `scrollTo(0, stickyScrollY)` 또는 동등한 즉시 보정을 수행한다. + - 현재 `nestedScrollView.scrollY`가 이미 `stickyScrollY` 이상이면 사용자의 기존 scroll 위치를 낮추지 않는다. + - 같은 탭 재선택 또는 초기 adapter attach 과정에서 불필요한 scroll 보정이 발생하지 않도록 마지막 선택 tab index 또는 초기 선택 여부를 관리한다. + - Live 탭 lazy load, pagination bottom check, owner CTA visibility 갱신은 기존 흐름을 유지한다. + - RED 테스트: + - `CreatorChannelActivitySourceTest`에 탭 전환 시 sticky threshold helper와 `nestedScrollView.scrollTo(0, stickyScrollY)` 호출 계약을 추가한다. + - 같은 탭 재선택/초기 선택에서는 sticky 보정을 하지 않는 guard 계약을 추가한다. + - 이미 sticky 기준 이상으로 스크롤된 경우 scrollY를 낮추지 않는 guard 계약을 추가한다. + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + - 기대 결과: + - Home 또는 Live 등 어느 탭에서든 다른 탭을 선택하면 tabbar가 sticky 위치로 유지된 상태에서 선택 탭 컨텐츠가 표시된다. + - 선택 탭 컨텐츠가 empty/error처럼 짧아도 tabbar가 header 아래로 풀리는 전환이 보이지 않는다. + - 이미 sticky보다 아래로 스크롤한 상태에서 탭을 바꿔도 scrollY가 위로 되돌아가지 않는다. + - 검증 기록: + - 2026-06-18: 사용자 피드백에 따라 탭 전환 시 짧은 컨텐츠 때문에 sticky tabbar가 풀려 보이는 현상을 검토했다. 현재 `updateViewPagerHeight()`가 선택 page 높이를 컨텐츠에 맞추고 parent `NestedScrollView`가 스크롤을 소유하므로, 짧은 탭 전환 시 scroll range 변화로 tabbar가 갑자기 header 아래 위치로 보일 수 있다고 판단했다. UX상 탭 전환 시점에는 sticky tabbar를 anchor로 유지하는 편이 더 자연스럽다고 판단해 PRD와 plan-task에 후속 작업을 추가했다. 구현은 아직 진행하지 않았다. + - 2026-06-18: `CreatorChannelActivitySourceTest`에 탭 전환 시 sticky anchor 보정 source 계약을 추가했다. production 보정 전 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 `탭 전환은 sticky tabbar anchor 아래로 내려간 scroll 위치를 되돌리지 않고 부족할 때만 보정한다`가 assertion 실패로 RED가 됐다. + - 2026-06-18: `CreatorChannelActivity`에 `lastSelectedCreatorChannelTabPosition`, `adjustCreatorChannelStickyAnchorOnTabSelected(position)`, `calculateCreatorChannelStickyScrollY()`를 추가했다. 초기 선택 또는 같은 탭 재선택은 무시하고, 다른 탭으로 전환할 때 현재 `NestedScrollView.scrollY`가 sticky threshold보다 작으면 `scrollTo(0, stickyScrollY)`로 보정하며 이미 sticky 기준 이상이면 기존 scroll 위치를 낮추지 않는다. + - 2026-06-18: 보정 후 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 `BUILD SUCCESSFUL in 26s`로 GREEN을 확인했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + - 2026-06-18: `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 11s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + - 2026-06-18: `./gradlew :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 25s`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 계속 출력됐다. + - 2026-06-18: 리뷰 게이트에서 `TabLayoutMediator.attach()` 이후 `OnPageChangeCallback`을 등록하므로 초기 Home 선택 이벤트를 받지 못하면 첫 실제 탭 전환이 `previousPosition == null`로 처리되어 sticky 보정이 누락될 수 있다는 차단 이슈가 발견됐다. + - 2026-06-18: `CreatorChannelActivitySourceTest`에 `lastSelectedCreatorChannelTabPosition = binding.viewPager.currentItem` baseline 초기화 계약을 추가했다. production 보정 전 동일 테스트를 실행해 `탭 전환은 sticky tabbar anchor 아래로 내려간 scroll 위치를 되돌리지 않고 부족할 때만 보정한다`가 assertion 실패로 RED가 됐다. + - 2026-06-18: `setupTabsAndPager()`에서 `TabLayoutMediator.attach()` 직후, `OnPageChangeCallback` 등록 전 `lastSelectedCreatorChannelTabPosition = binding.viewPager.currentItem`를 초기화했다. 이로써 첫 실제 탭 전환도 이전 탭과 다른 선택으로 판단되어 sticky anchor 보정이 수행된다. + - 2026-06-18: 병렬/daemon Gradle 재검증 중 `processDebugResources`의 기본 resource not found, `packageDebugResources` 삭제 실패, `dataBindingGenBaseClassesDebug`의 `chk_fri` 누락, Kotlin incremental cache `lookups.tab` 중복 등록이 발생했다. 변경 코드 실패가 아니라 생성물/daemon cache 문제로 판단해 `app/build/intermediates`, `app/build/generated`, `app/build/kspCaches`, `app/build/tmp`, `app/build/kotlin` 생성물 캐시를 정리하고 Gradle/Kotlin daemon을 비활성화해 순차 재검증했다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 4m 19s`로 통과했다. 기존 Kotlin deprecation/annotation 경고는 변경 범위와 무관하게 출력됐다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 32s`로 통과했다. 기존 Gradle 9.0 호환성 deprecation 경고는 계속 출력됐다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:ktlintCheck` 실행 결과 `:app:ktlintMainSourceSetCheck`에서 실패했다. 보고서 확인 결과 `Agora.kt`, `SodaLiveApp.kt`, `audio_content` package-name 등 기존 main source 위반 574건이 출력됐고, `rg -n "CreatorChannelActivity|creator/channel|plan-task" app/build/reports/ktlint/ktlintMainSourceSetCheck/ktlintMainSourceSetCheck.txt` 결과 이번 RF7.1 변경 파일은 포함되지 않았다. + +--- + +### Phase 8 Follow-up: Live empty 안내와 다시듣기 item Figma 보정 + +- [x] **Task RF8.1: Live empty 안내 메시지와 다시듣기 item 간격/썸네일 radius 보정** + - 수정: + - `app/src/main/res/layout/fragment_creator_channel_live.xml` + - `app/src/main/res/layout/item_creator_channel_live_replay.xml` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/ui/CreatorChannelLiveReplayAdapter.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` + - 작업: + - Live empty 상태에서 `크리에이터가 라이브를 준비 중입니다.\n기대해 주세요!` 안내 문구가 실제 탭 영역 안에서 보이도록 empty 영역 높이와 중앙 정렬을 보장한다. + - 라이브 다시듣기 item 사이 간격을 Figma 기준 8dp로 적용한다. + - 라이브 다시듣기 썸네일 이미지는 Figma 기준 14dp rounded corner로 실제 이미지가 clipping되도록 적용한다. + - RED 테스트: + - `CreatorChannelLiveFragmentLayoutTest`에 empty 메시지 표시 영역이 0dp match-constraints가 아닌 viewport 높이를 갖는지 확인하는 source/layout 계약을 추가한다. + - `CreatorChannelLiveFragmentLayoutTest`에 replay item 8dp spacing과 thumbnail `radius_14` clipping 계약을 추가한다. + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` + - `./gradlew :app:mergeDebugResources` + - `./gradlew :app:compileDebugKotlin` + - 기대 결과: + - Live empty 상태에서 안내 메시지가 보인다. + - 라이브 다시듣기 item 사이 간격은 8dp이고 썸네일 이미지는 14dp rounded corner로 표시된다. + - 검증 기록: + - 2026-06-18: 현재 코드 확인 결과 Live empty 문구는 root `ConstraintLayout` top에 직접 붙은 `TextView`라 `wrap_content` page 높이/탭 전환 환경에서 보장된 표시 영역을 갖지 못할 수 있음을 확인했다. 라이브 다시듣기 item은 썸네일 background에 `radius_14`가 있으나 실제 이미지 clipping이 없고, item 간 8dp 간격도 적용되지 않았다. + - 2026-06-18: `CreatorChannelLiveFragmentLayoutTest`에 empty container 표시 영역과 replay item 8dp spacing/thumbnail clipping 계약을 추가했다. production 보정 전 실행한 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`는 `layout_creator_channel_live_empty` 미존재로 `:app:compileDebugUnitTestKotlin FAILED`가 발생해 RED를 확인했다. + - 2026-06-18: `fragment_creator_channel_live.xml`에 `layout_creator_channel_live_empty` `FrameLayout`을 추가해 `minHeight=360dp`, `gravity=center` 안에서 empty message를 중앙 표시하도록 변경했다. `CreatorChannelLiveFragment`는 empty/loading/error/content 상태에서 `layoutCreatorChannelLiveEmpty` visibility를 제어하도록 보정했다. + - 2026-06-18: `item_creator_channel_live_replay.xml` root에 `android:layout_marginBottom="@dimen/spacing_8"`을 추가하고, 썸네일 container에 `android:clipToOutline="true"`, `android:outlineProvider="background"`를 적용했다. `CreatorChannelLiveReplayAdapter.ViewHolder`에서도 `ViewOutlineProvider`를 설정해 실제 썸네일 이미지를 `radius_14`로 clipping하도록 보강했다. + - 2026-06-18: 보정 후 `CreatorChannelLiveFragmentLayoutTest` 최초 GREEN 시도는 테스트가 item root margin이 아니라 thumbnail margin을 확인해 실패했고, Figma 기준인 item 사이 간격을 source 계약으로 확인하도록 테스트를 바로잡았다. 재실행 결과 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`가 `BUILD SUCCESSFUL in 1m 7s`로 통과했다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 28s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 25s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. + +- [x] **Task RF8.2: Live empty 최소 높이를 sticky 가능한 탭 viewport 기준으로 보정** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` + - `app/src/main/res/layout/fragment_creator_channel_live.xml` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` + - 작업: + - 고정 `360dp` empty 높이를 제거한다. + - Activity가 sticky anchor 이후 실제 탭 viewport를 계산해 Live Fragment empty container의 최소 높이로 전달한다. + - Live empty 상태에서 `NestedScrollView`의 최대 scroll range가 sticky threshold보다 작아져 tabbar가 header 아래로 풀리는 상황을 방지한다. + - content/error 상태의 짧은 스크롤 정책은 기존 RF6.2 범위를 유지하고, empty 상태만 최소 높이를 보정한다. + - RED 테스트: + - `CreatorChannelActivitySourceTest`에 `calculateCreatorChannelLiveEmptyMinHeight()`와 `findLiveFragment()?.onCreatorChannelLiveViewportHeightChanged(...)` 호출 계약을 추가한다. + - `CreatorChannelLiveFragmentLayoutTest`에 고정 `android:minHeight="360dp"` 제거와 runtime `layoutCreatorChannelLiveEmpty.minimumHeight` 적용 계약을 추가한다. + - 검증 명령: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` + - `./gradlew :app:mergeDebugResources` + - `./gradlew :app:compileDebugKotlin` + - 기대 결과: + - Live empty 안내 문구가 실제 탭 viewport 중앙에 표시된다. + - Live empty 탭 전환 시 sticky tabbar anchor가 scroll range 부족으로 풀리지 않는다. + - 검증 기록: + - 2026-06-18: 리뷰 지적에 따라 RF8.1의 고정 `360dp` empty 높이가 실제 탭 viewport 중앙/Sticky 유지 요구를 보장하지 못할 수 있음을 확인했다. 탭 컨텐츠가 너무 짧으면 `NestedScrollView.scrollTo(stickyScrollY)`가 최대 scroll range에 clamp되어 tabbar가 sticky anchor에 도달하지 못할 수 있으므로 Activity 계산값을 empty 상태에만 전달하도록 보정한다. + - 2026-06-18: production 보정 전 `CreatorChannelActivitySourceTest`와 `CreatorChannelLiveFragmentLayoutTest`에 viewport min height 전달 계약과 고정 `360dp` 제거 계약을 추가했다. 실행 결과 `라이브 탭 pagination과 높이 갱신은 NestedScrollView 소유 스크롤 경로에서 처리한다`, `라이브 empty 최소 높이는 sticky anchor 이후 탭 viewport 기준으로 전달한다`, `라이브 fragment layout은 sort current live list empty error owner CTA를 제공한다`, `라이브 empty container 최소 높이는 Activity가 전달한 viewport 높이를 사용한다` 4개 테스트가 실패해 RED를 확인했다. + - 2026-06-18: `fragment_creator_channel_live.xml`에서 `android:minHeight="360dp"`를 제거했다. `CreatorChannelLiveFragment`에는 `emptyMinHeight`, `onCreatorChannelLiveViewportHeightChanged(minHeight)`, `applyEmptyMinHeight()`를 추가해 empty 상태에서 Activity가 전달한 최소 높이를 `layoutCreatorChannelLiveEmpty.minimumHeight`에 적용하도록 했다. + - 2026-06-18: `CreatorChannelActivity`는 Live 탭 선택/Live content 변경/ViewPager height 갱신 시 `updateCreatorChannelLiveViewportHeight()`를 호출한다. 최소 높이는 `visibleTabViewportHeight = nestedScrollView.height - tabLayout.height`와 `scrollRangeRequiredHeight = nestedScrollView.height + stickyScrollY - headerContainer.height` 중 큰 값으로 계산해, empty page가 실제 탭 viewport를 채우면서 sticky scroll range도 만족하도록 했다. + - 2026-06-18: 보정 후 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 50s`로 통과했다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 44s`로 통과했다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 56s`로 통과했다. + +- [x] **Task RF8.3: Live owner CTA 표시 시 다시듣기 마지막 item cut-off 보정** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivity.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragment.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelActivitySourceTest.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/live/CreatorChannelLiveFragmentLayoutTest.kt` + - 작업: + - Activity overlay CTA 표시 여부를 Live Fragment에 전달한다. + - CTA가 표시될 때 마지막 다시듣기 item이 하단 CTA 영역에 가려지지 않도록 `RecyclerView` 하단 padding을 CTA 높이만큼 추가한다. + - CTA 미표시 상태에서는 기존 기본 list bottom padding만 유지한다. + - RED 테스트: + - `CreatorChannelLiveFragmentLayoutTest`에 CTA visibility callback과 replay list bottom padding 계약을 추가한다. + - `CreatorChannelActivitySourceTest`에 owner CTA visibility 변경 시 Live Fragment로 CTA 표시 여부를 전달하는 계약을 추가한다. + - 검증 명령: + - `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` + - `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources` + - `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` + - 기대 결과: + - 본인 채널 Live 탭에서 하단 CTA가 표시되어도 다시듣기 마지막 item 전체가 CTA 위로 스크롤되어 보인다. + - 검증 기록: + - 실제 원인 확인과 검증 결과를 이 Task 아래에 한국어로 누적 기록한다. + - 2026-06-18: 원인 확인 결과, Phase 6 Follow-up에서 `라이브 시작하기` CTA를 Activity root overlay로 이동하면서 기존 Fragment 내부 `RecyclerView` 하단 padding 보정이 제거됐고, Activity의 `NestedScrollView` bottom padding에만 의존하게 됐다. Phase 8 Follow-up에서 item 간격과 empty 높이를 보정한 뒤에도 실제 마지막 다시듣기 item을 소유한 `RecyclerView`에는 CTA 높이만큼의 list bottom padding이 없어 owner CTA 아래에서 마지막 item이 잘릴 수 있음을 확인했다. + - 2026-06-18: RED 확인으로 `CreatorChannelActivitySourceTest`와 `CreatorChannelLiveFragmentLayoutTest`에 owner CTA visibility를 Live Fragment로 전달하고, Live Fragment가 replay list bottom padding을 기본 32dp 또는 CTA 표시 시 132dp로 적용하는 계약을 추가했다. production 보정 전 실행한 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"`는 `CreatorChannelActivitySourceTest` 2건, `CreatorChannelLiveFragmentLayoutTest` 1건이 실패해 RED를 확인했다. + - 2026-06-18: `CreatorChannelActivity.updateLiveOwnerCtaVisibility()`가 `findLiveFragment()?.onCreatorChannelLiveOwnerCtaVisibilityChanged(shouldShowLiveOwnerCta)`를 호출하도록 변경하고, 바깥 `NestedScrollView` bottom padding 조정은 제거했다. `CreatorChannelLiveFragment`에는 `onCreatorChannelLiveOwnerCtaVisibilityChanged(isVisible)`를 추가해 `rvCreatorChannelLiveReplays` bottom padding을 CTA 미표시 32dp, CTA 표시 132dp로 갱신하도록 했다. + - 2026-06-18: 보정 후 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` 실행 결과 `BUILD SUCCESSFUL in 1m 11s`로 통과했다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:mergeDebugResources` 실행 결과 `BUILD SUCCESSFUL in 14s`로 통과했다. + - 2026-06-18: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 17s`로 통과했다. + - 2026-06-18: 추가 검증으로 `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process :app:ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 14s`로 통과했다. 기존 `.editorconfig`의 `disabled_rules` deprecation 경고는 계속 출력됐다. + ## Verification Log - 2026-06-17: Phase 1 진행. `CreatorChannelHomeApi`/`CreatorChannelHomeRepository`를 `CreatorChannelApi`/`CreatorChannelRepository`로 rename하고 기존 홈 endpoint/repository method 동작은 유지했다. - 2026-06-17: `./gradlew :app:compileDebugKotlin` PASS. 최초 병렬 실행은 KSP incremental cache 손상으로 실패했으나 `app/build/kspCaches/debug` 생성물 캐시 삭제 후 순차 재실행에서 통과했다. @@ -707,3 +921,10 @@ - 2026-06-17: Phase 5 코드 리뷰에서 수정 필요 결함은 발견하지 못했다. `CreatorChannelLiveSortPopup`은 외부 터치 dismiss, 같은 정렬 재선택 dismiss-only, 새 정렬 선택 시 `viewModel.changeSort(sort)` 전달, 우측 화면 밖 보정, `onDestroyView()` dismiss 경로를 갖는다. - 2026-06-17: Phase 5 검증으로 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveFragmentLayoutTest"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"` PASS, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"` PASS, `./gradlew :app:mergeDebugResources` PASS, `./gradlew :app:compileDebugKotlin` PASS, `./gradlew :app:ktlintCheck` PASS를 확인했다. - 2026-06-17: 추가 상위 회귀 검증 `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"` 최초 실행은 drawable 3개 추가 후 resource id 캐시가 맞지 않아 `CreatorChannelHomeMapperTest`, `CreatorChannelTitleBarStateTest`의 drawable id assertion이 3씩 밀려 실패했다. 해당 클래스들을 `--rerun-tasks`로 강제 재컴파일하자 통과했고, 이후 동일 채널 전체 회귀 명령을 순차 재실행해 `BUILD SUCCESSFUL`로 통과했다. 병렬 `--rerun-tasks` 중 Kotlin incremental cache 경합 로그가 출력됐으나 최종 순차 검증은 통과했다. + +- 2026-06-18: Phase 8 자동 검증 실행. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*Live*"`, `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.*"`, `./gradlew :app:mergeDebugResources`, `./gradlew :app:compileDebugKotlin`, `./gradlew :app:ktlintCheck` 모두 `BUILD SUCCESSFUL`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. +- 2026-06-18: Phase 8 수동 검증 시도. 연결 단말과 설치 패키지는 확인했으나 `CreatorChannelActivity` 직접 실행은 exported 제한으로 차단됐고, 런처 실행은 TedPermissionActivity 권한 화면까지만 확인했다. Live 탭 실제 화면/Figma 대조는 후속 로그인/권한/서버 데이터가 준비된 단말 시나리오에서 계속 확인해야 한다. +- 2026-06-18: Phase 6 Follow-up 진행. `라이브 시작하기` CTA를 Fragment 내부 스크롤 컨텐츠에서 Activity root overlay로 이동해 Figma `665:19371` 기준 100dp black 하단 고정 영역과 14dp inset cyan capsule 버튼 구조로 보정했다. RED/GREEN 확인으로 `CreatorChannelLiveFragmentLayoutTest` 실패 후 통과, `mergeDebugResources` PASS, `compileDebugKotlin` PASS, `ktlintCheck` PASS를 확인했다. 병렬 Gradle 실행 중 Kotlin incremental cache 경합은 daemon stop 후 순차 재실행으로 해소했다. +- 2026-06-18: Phase 6 Follow-up RF6.2 진행. `BaseActivity` root bottom padding과 Activity 하단 UI navigation inset의 중복 적용을 제거해 실제 기기에서 preview보다 위로 뜨는 문제를 보정했다. Live 탭 empty/error 짧은 상태는 `ViewPager2` page minimum height 강제와 `fragment_creator_channel_live.xml` root `match_parent`를 제거해 불필요한 긴 스크롤이 생기지 않도록 했다. targeted RED/GREEN 테스트, `mergeDebugResources`, `compileDebugKotlin`, `ktlintCheck`가 최종 통과했다. +- 2026-06-18: Live API 기본 `size`를 20으로 변경한 뒤 연결 지점을 점검했다. `CreatorChannelLiveViewModel.DEFAULT_PAGE_SIZE`를 참조하는 테스트 bytecode가 기존 `const val` 값 10을 inline해 mock stub이 production 호출 `size=20`과 불일치하는 문제를 확인했고, 기본값을 runtime `val`로 바꿔 stale inline 재발 가능성을 낮췄다. 문서의 초기 로드 기록도 `page=0`, `size=20`, `sort=LATEST`로 보정했다. `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLiveViewModelTest" --tests "kr.co.vividnext.sodalive.v2.creator.channel.live.CreatorChannelLivePaginationTest"` 실행 결과 `BUILD SUCCESSFUL in 27s`로 통과했다. +- 2026-06-18: Live API 기본 `size=20` 보정 후 `./gradlew :app:compileDebugKotlin` 실행 결과 `BUILD SUCCESSFUL in 1s`로 통과했다. Gradle 9.0 호환성 deprecation 경고는 기존 빌드 설정 경고로 출력됐다. diff --git a/docs/20260617_크리에이터_채널_라이브_탭/prd.md b/docs/20260617_크리에이터_채널_라이브_탭/prd.md index 9361aac3..0833db79 100644 --- a/docs/20260617_크리에이터_채널_라이브_탭/prd.md +++ b/docs/20260617_크리에이터_채널_라이브_탭/prd.md @@ -205,7 +205,7 @@ Sort-bar는 라이브 다시듣기 총 개수와 현재 정렬 상태를 표시 - 로그인 사용자가 현재 크리에이터 채널의 본인이면 하단 고정 CTA 영역을 표시한다. - 로그인 사용자가 현재 크리에이터 채널의 본인이 아니면 하단 고정 CTA 영역을 표시하지 않는다. - CTA 영역은 화면 하단에 고정하고, 목록 스크롤과 함께 움직이지 않는다. -- CTA 영역 배경은 black이며, 내부 버튼은 Figma 기준 cyan capsule 형태를 따른다. +- CTA 영역은 Figma `665:19371` 기준으로 화면 하단에 붙은 100dp 높이의 black 영역이며, 내부 버튼은 좌우 14dp, 상단 14dp 여백을 둔 cyan capsule 형태를 따른다. - 버튼 label은 `라이브 시작하기`이며 다국어 문자열 리소스로 관리한다. - 버튼 icon은 `ic_new_create_live` drawable 리소스를 사용한다. - 버튼 터치 시 기존 라이브 시작/생성 진입 플로우로 이동한다. @@ -232,6 +232,22 @@ Sort-bar는 라이브 다시듣기 총 개수와 현재 정렬 상태를 표시 - 빠른 스크롤로 load-more trigger가 반복 발생해도 page가 중복 append되지 않아야 한다. - Fragment/View 재생성 후 현재 목록, 정렬, page 상태는 ViewModel 상태 보존 정책에 따라 유지되어야 한다. +### Creator Channel Tab Switching Sticky Behavior +크리에이터 채널의 공통 tabbar는 탭 전환 시 컨텐츠 높이 변화와 무관하게 안정적인 탐색 anchor로 동작해야 한다. + +#### Requirements +- 사용자가 다른 탭을 선택하면, 현재 tabbar가 sticky 상태가 아니더라도 tabbar를 sticky 위치로 전환한 뒤 선택된 탭 내용을 표시한다. +- 탭 전환 중 선택된 탭 컨텐츠가 empty/error처럼 짧아도 tabbar가 sticky 상태에서 풀려 header 아래 위치로 되돌아가는 듯한 시각적 전환을 만들지 않는다. +- sticky 전환 기준은 기존 `CreatorChannelActivity.updateScrollState()`가 사용하는 `CreatorChannelScrollState.calculateStickyTop(statusBarHeight, baseTitleBarHeight)`와 `headerContainer.height` 계산을 재사용한다. +- 같은 탭을 다시 선택하는 경우에는 불필요한 scroll 보정을 수행하지 않는다. +- 사용자가 탭 전환 전에 이미 sticky 기준보다 아래로 스크롤한 상태라면 기존 scrollY를 낮추지 않는다. +- sticky 보정은 `NestedScrollView`가 scroll owner인 현재 구조를 유지하며, 각 탭 Fragment 내부에 별도 scroll listener를 추가하지 않는다. + +#### UX Decision +- 짧은 탭으로 전환할 때 header 일부가 갑자기 다시 보이는 현상은 사용자가 선택한 탭 컨텐츠가 밀려 보이는 느낌을 만든다. +- 탭 전환 시점에는 사용자의 초점이 header 탐색이 아니라 탭 컨텐츠 탐색에 있으므로, tabbar를 sticky anchor로 고정한 뒤 내용을 보여주는 편이 더 예측 가능하다. +- 따라서 탭 전환 시 sticky 상태를 보장하는 방식을 채택하고, 짧은 탭의 불필요한 내부 스크롤을 없애는 RF6.2 정책은 유지한다. + --- ## 8. UX / UI Expectations