Files
sodalive-android/docs/20260625_메인_콘텐츠_탭_내부_전체_탭/plan-task.md

55 KiB

메인 콘텐츠 탭 내부 전체 탭 구현 계획/TASK

For agentic workers: REQUIRED SUB-SKILL: 구현 시 superpowers:subagent-driven-development 또는 superpowers:executing-plans를 사용해 task 단위로 진행한다. 각 단계는 체크박스(- [ ])로 추적하고, 완료 즉시 - [x]로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.

Goal: GET /api/v2/audio/contents 응답을 기반으로 메인 콘텐츠 탭 내부 전체 탭에 타입별 오디오/시리즈 콘텐츠 목록, 시리즈 요일 필터, 정렬, 스크롤 페이징을 제공한다.

Architecture: 기존 ContentMainFragment추천, 랭킹, 전체 Text Tab 구조는 유지하고, 전체 선택 시 전용 Capsule type tab, optional day-of-week filter, sort bar, paged grid RecyclerView를 노출한다. 신규 API/Repository/DTO/UI state/mapper/ViewModel/adapter는 kr.co.vividnext.sodalive.v2.main.content 하위에 두고, 공통 카드 위젯인 AudioContentCardView, SeriesContentCardView, CapsuleTabBarView를 재사용한다. 레거시 파일은 직접 수정하지 않고 SeriesPublishedDaysOfWeek enum만 참조한다.

Tech Stack: Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test.


전제와 성공 기준

  • PRD: docs/20260625_메인_콘텐츠_탭_내부_전체_탭/prd.md
  • API endpoint는 GET /api/v2/audio/contents이다.
  • query parameter 기본값은 page=0, size=20, sort=LATEST, type=AUDIO다.
  • dayOfWeektype=SERIES일 때만 전송한다.
  • 기본 dayOfWeek는 디바이스 현재 요일을 SeriesPublishedDaysOfWeek로 변환한다.
  • UI 타입 탭은 오디오, 시리즈, 오리지널, 무료, 포인트만 구현한다.
  • Figma에 보이는 전체, 연재 콘텐츠 타입 칩은 이번 범위에서 구현하지 않는다.
  • SERIES, ORIGINAL은 응답의 series 목록을 표시한다.
  • AUDIO, FREE, POINT는 응답의 audios 목록을 표시한다.
  • 이번 범위의 ORIGINAL은 오리지널 시리즈 목록만 의미한다.
  • RANDOM 요일 라벨은 한국어 기타, 일본어 その他, 영어 OTHER로 표시한다.
  • 정렬 옵션은 기존 ContentSort.entries 전체를 사용하고, UI는 CreatorChannelSortPopup/ContentSort.toLabelResId() 패턴을 따른다.
  • SeriesContentCardViewisAdult 성인 배지를 지원해야 한다.
    • SeriesContentCardSize.Large: ic_new_shield_large
    • SeriesContentCardSize.Small: ic_new_shield_small
    • badge background: bg_creator_channel_live_adult_badge
  • 구현 완료 후 최소 다음 명령을 실행한다.
    • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • ./gradlew :app:mergeDebugResources
    • ./gradlew :app:compileDebugKotlin
    • ./gradlew :app:ktlintCheck
    • git diff --check

Figma 참조 필요 Phase

  • Phase 1: 제한 참조
    • 기존 콘텐츠 추천/랭킹 구조, DI, sort popup, 카드 위젯 구조 확인 중심으로 진행한다.
  • Phase 2: Figma 참조 불필요
    • API/DTO/Repository와 enum/요일 변환은 PRD 서버 계약과 기존 V2 data layer 패턴을 따른다.
  • Phase 3: 필수 참조
    • SeriesContentCardView 성인 배지는 Figma 567:18346, 567:18347과 기존 오디오 배지 구조를 대조한다.
  • Phase 4: Figma 참조 불필요
    • ViewModel 페이징/정렬/요일 상태는 기존 크리에이터 채널 pagination 패턴을 따른다.
  • Phase 5: 필수 참조
    • 전체 탭 type tab, 요일 필터, 정렬 바, 3열 그리드 위치는 Figma 35:5857, 24:6909, 24:9105를 기준으로 확인한다.
  • Phase 6: 필수 참조
    • 최종 수동 화면 검증은 PRD의 모든 포함/제외 항목과 실제 화면을 대조한다.

파일 구조

  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabApi.kt
    • GET /api/v2/audio/contents Retrofit endpoint를 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabModels.kt
    • MainContentAllTabResponse, MainContentAllType, MainContentAudioResponse, MainContentSeriesResponse DTO를 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabRepository.kt
    • API 호출을 repository method로 감싼다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiState.kt
    • Loading, Content, Empty, Error 상태와 paging/loading-more 상태를 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiModels.kt
    • 오디오/시리즈/요일/type tab/sort UI model을 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabMappers.kt
    • DTO를 UI model로 변환하고 타입별 audios/series 선택 규칙을 구현한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllDayOfWeekMapper.kt
    • 디바이스 현재 요일과 UI 라벨을 SeriesPublishedDaysOfWeek로 매핑한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModel.kt
    • type/sort/day/page/hasNext/loading-more 상태와 API 호출을 관리한다.
  • Modify: app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
    • MainContentAllTabApi, MainContentAllTabRepository, ContentAllTabViewModel을 Koin에 등록한다.
  • Modify: app/src/main/res/layout/view_series_content_card.xml
    • 성인 배지 ImageView를 썸네일 우측 상단에 추가한다.
  • Modify: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt
    • setAdultVisible(isVisible: Boolean)과 size별 shield icon 적용을 추가한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardViewTest.kt
    • 성인 배지 visibility, background, size별 icon source를 검증한다.
  • Create: app/src/main/res/layout/item_content_all_series_card.xml
    • SeriesContentCardView 기반 전체 탭 시리즈 카드 item이다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.kt
    • SERIES, ORIGINAL grid item을 바인딩한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.kt
    • AUDIO, FREE, POINT grid item을 바인딩한다.
  • Modify: app/src/main/res/layout/fragment_v2_main_content.xml
    • 전체 탭 전용 type tab, day filter, sort bar, grid RecyclerView, loading-more/error surface를 추가한다.
  • Modify: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
    • 전체 Text Tab 선택 시 전체 탭 surface를 표시하고 ViewModel/adapter/sort popup/day filter/paging/routing을 연결한다.
  • Modify: app/src/main/res/values/strings.xml
    • 전체 탭 type 라벨, 요일 기타, 정렬/빈 목록/오류 표시 문구를 추가한다.
  • Modify: app/src/main/res/values-en/strings.xml
    • 전체 탭 type 라벨, 요일 OTHER, 정렬/빈 목록/오류 표시 문구를 추가한다.
  • Modify: app/src/main/res/values-ja/strings.xml
    • 전체 탭 type 라벨, 요일 その他, 정렬/빈 목록/오류 표시 문구를 추가한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/MainContentAllTabMapperTest.kt
    • 타입별 audios/series 선택, tag/adult/original/free/point mapping을 검증한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/MainContentAllDayOfWeekMapperTest.kt
    • current day mapping과 라벨 리소스 mapping을 검증한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModelTest.kt
    • 첫 페이지, type/sort/day 변경, load-more, stale response 방지를 검증한다.
  • Modify: app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt
    • 전체 탭 layout id, adapter/ViewModel 연결, 전체/연재 칩 제외, routing source를 검증한다.

Phase 1: 기존 구조 확인과 작업 경계 고정

  • Task 1.1: 기존 콘텐츠 탭 구조와 전체 탭 삽입 지점 확인

    • 확인:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
      • app/src/main/res/layout/fragment_v2_main_content.xml
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainViewModel.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentRankingViewModel.kt
    • 작업:
      • 기존 추천, 랭킹, 전체 Text Tab bar는 유지한다.
      • showContentTab(CONTENT_TAB_ALL) 분기에서 전체 탭 surface를 보이도록 확장할 위치를 확인한다.
      • 추천/랭킹 API와 ViewModel은 리팩터링하지 않는다.
    • 검증:
      • Run: rg -n "CONTENT_TAB_ALL|hideContentSurfaces|showRecommendationContent|showRankingContent|textTabBarContent" app/src/main/java/kr/co/vividnext/sodalive/v2/main/content app/src/main/res/layout/fragment_v2_main_content.xml
      • Expected: Text Tab 전환과 기존 surface visibility 지점이 확인된다.
      • 2026-06-25: PASS. ContentMainFragment.kt에서 CONTENT_TAB_ALL 분기와 hideContentSurfaces(), 추천/랭킹 surface visibility 제어 지점을 확인했다.
  • Task 1.2: 재사용 위젯과 신규 adapter 경계 확정

    • 확인:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelSortPopup.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelSortModels.kt
    • 작업:
      • 타입 칩은 CapsuleTabBarView 재사용으로 고정한다.
      • 정렬 팝업은 CreatorChannelSortPopupContentSort.toLabelResId() 재사용으로 고정한다.
      • 전체 탭 grid는 pagination과 type별 모델 분리를 위해 ContentAllAudioCardAdapter, ContentAllSeriesCardAdapter 신규 생성으로 고정한다.
    • 검증:
      • Run: rg -n "class CapsuleTabBarView|class CreatorChannelSortPopup|fun ContentSort.toLabelResId|class AudioContentCardView|class SeriesContentCardView" app/src/main/java/kr/co/vividnext/sodalive/v2
      • Expected: 재사용 후보 클래스와 함수가 확인된다.
      • 2026-06-25: PASS. CapsuleTabBarView, CreatorChannelSortPopup, ContentSort.toLabelResId(), AudioContentCardView, SeriesContentCardView 위치를 확인했다.
  • Task 1.3: 제외 범위 확인

    • 확인:
      • docs/20260625_메인_콘텐츠_탭_내부_전체_탭/prd.md
    • 제외:
      • Figma type chip 전체, 연재 구현
      • 추천/랭킹 탭 기존 기능 변경
      • 레거시 API/화면 파일 직접 수정
      • 오리지널 오디오 별도 표시
      • 오프라인 캐시/로컬 DB 저장
    • 검증:
      • Run: rg -n '전체|연재|Non-Goals|오리지널 오디오|레거시|Open Questions' docs/20260625_메인_콘텐츠_탭_내부_전체_탭/prd.md
      • Expected: 제외 범위와 Open Questions 없음 상태가 확인된다.
      • 2026-06-25: PASS. PRD에서 레거시 직접 수정 제외, 오리지널 오디오 제외, Figma 전체/연재 칩 제외, Open Questions 섹션을 확인했다.

Phase 2: API, DTO, Repository, enum/요일 mapping 추가

  • Task 2.1: API/DTO/Repository 계약 추가

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabApi.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabModels.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabRepository.kt
    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
    • 작업:
      • Retrofit endpoint는 @GET("/api/v2/audio/contents")로 정의한다.
      • query parameter는 type, sort, page, size, dayOfWeek를 정의한다.
      • dayOfWeek parameter는 nullable로 두고 repository/ViewModel에서 SERIES일 때만 값을 전달한다.
      • DTO는 PRD response class 필드를 모두 포함하되 Gson @SerializedName을 사용한다.
      • MainContentAllTypeAUDIO, SERIES, ORIGINAL, FREE, POINT만 정의한다.
      • ContentSortSeriesPublishedDaysOfWeek는 기존 타입을 import한다.
      • Koin networkModule, repositoryModule에 신규 API/Repository를 등록한다.
    • 검증:
      • Run: ./gradlew :app:compileDebugKotlin
      • Expected: 신규 data layer와 DI 등록이 컴파일된다.
      • 2026-06-25: PASS. MainContentAllTabApi, DTO, Repository, AppDI.kt API/Repository 등록 후 ./gradlew :app:compileDebugKotlin 성공.
  • Task 2.2: 타입 라벨과 요일 라벨 리소스 추가

    • 수정:
      • app/src/main/res/values/strings.xml
      • app/src/main/res/values-en/strings.xml
      • app/src/main/res/values-ja/strings.xml
    • 작업:
      • type tab 라벨 문자열을 추가한다.
        • screen_content_all_type_audio
        • screen_content_all_type_series
        • screen_content_all_type_original
        • screen_content_all_type_free
        • screen_content_all_type_point
      • 전체 탭 요일 RANDOM 전용 문자열을 추가한다.
        • screen_content_all_day_other
        • values: 기타
        • values-en: OTHER
        • values-ja: その他
      • 기존 전역 day_random은 레거시 사용처 영향 방지를 위해 수정하지 않는다.
      • 빈 목록/페이징 오류 표시를 위해 전체 탭 전용 문자열을 추가한다.
        • screen_content_all_empty
        • screen_content_all_pagination_error
    • 검증:
      • Run: ./gradlew :app:mergeDebugResources
      • Expected: 3개 locale string resource가 중복 없이 merge된다.
      • 2026-06-25: PASS. values, values-en, values-ja에 전체 탭 type/day/empty/pagination 문자열 추가 후 ./gradlew :app:mergeDebugResources 성공.
  • Task 2.3: 요일 mapping 테스트 작성

    • 생성:
      • app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/MainContentAllDayOfWeekMapperTest.kt
    • 테스트 케이스:
      • Calendar.MONDAYSeriesPublishedDaysOfWeek.MON
      • Calendar.TUESDAYSeriesPublishedDaysOfWeek.TUE
      • Calendar.WEDNESDAYSeriesPublishedDaysOfWeek.WED
      • Calendar.THURSDAYSeriesPublishedDaysOfWeek.THU
      • Calendar.FRIDAYSeriesPublishedDaysOfWeek.FRI
      • Calendar.SATURDAYSeriesPublishedDaysOfWeek.SAT
      • Calendar.SUNDAYSeriesPublishedDaysOfWeek.SUN
      • SeriesPublishedDaysOfWeek.RANDOM 라벨은 screen_content_all_day_other
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.MainContentAllDayOfWeekMapperTest"
      • Expected: mapper 구현 전 RED 실패.
      • 2026-06-25: RED 확인. mapper 구현 전 테스트를 먼저 추가하고 실행했다. 병렬 Gradle 실행 중 incremental resource merge cache 오류로 실패가 발생해 이후 검증은 순차 실행으로 전환했다.
  • Task 2.4: 요일 mapping 구현

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllDayOfWeekMapper.kt
    • 작업:
      • fun currentDeviceDayOfWeek(calendar: Calendar = Calendar.getInstance()): SeriesPublishedDaysOfWeek를 추가한다.
      • fun SeriesPublishedDaysOfWeek.toContentAllDayLabelResId(): Int를 추가한다.
      • RANDOMR.string.screen_content_all_day_other로 매핑한다.
      • 요일 UI 표시 순서는 MON, TUE, WED, THU, FRI, SAT, SUN, RANDOM으로 고정한다.
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.MainContentAllDayOfWeekMapperTest"
      • Expected: PASS.
      • 2026-06-25: PASS. currentDeviceDayOfWeek(), toContentAllDayLabelResId(), contentAllDayOfWeekOptions 구현 후 대상 테스트 성공.

Phase 3: UI model, mapper, SeriesContentCardView 성인 배지

  • Task 3.1: 전체 탭 mapper RED 테스트 작성

    • 생성:
      • app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/MainContentAllTabMapperTest.kt
    • 테스트 케이스:
      • AUDIO, FREE, POINT는 response audios만 UI 목록으로 사용한다.
      • SERIES, ORIGINAL은 response series만 UI 목록으로 사용한다.
      • ORIGINAL은 시리즈 카드 타입으로 매핑한다.
      • isAdult는 오디오/시리즈 UI model의 showAdultBadge로 매핑된다.
      • 오디오 isPointAvailable=trueAudioContentTag.Point로 매핑된다.
      • 오디오 isFirstContent=trueAudioContentTag.First로 매핑된다.
      • 오디오 isOriginalSeries=trueAudioContentTag.Original로 매핑된다.
      • 오디오 price == 0AudioContentTag.Free로 매핑된다.
      • hasNext, page, size, totalCount, sort, dayOfWeek는 UI state에 보존된다.
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.MainContentAllTabMapperTest"
      • Expected: UI model/mapper 구현 전 RED 실패.
      • 2026-06-25: RED 확인. UI model/mapper 구현 전 테스트를 먼저 추가하고 실행했다. 병렬 Gradle 실행 중 일부 명령이 compile 단계에서 timeout/cache 오류로 실패해 이후 검증은 순차 실행으로 전환했다.
  • Task 3.2: 전체 탭 UI model과 mapper 구현

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiState.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiModels.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabMappers.kt
    • 작업:
      • MainContentAllAudioUiModelaudioContentId, title, imageUrl, price, creatorNickname, tags, showAdultBadge를 가진다.
      • MainContentAllSeriesUiModelseriesId, title, coverImageUrl, creatorNickname, showOriginalTag, showAdultBadge를 가진다.
      • MainContentAllTabUiState.ContentselectedType, selectedSort, selectedDayOfWeek, totalCount, audioItems, seriesItems, page, size, hasNext, isLoadingMore, paginationErrorMessage를 가진다.
      • MainContentAllType.usesSeriesItems()SERIES, ORIGINAL에서 true를 반환한다.
      • MainContentAllType.usesDayOfWeekQuery()SERIES에서만 true를 반환한다.
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.MainContentAllTabMapperTest"
      • Expected: PASS.
      • 2026-06-25: PASS. 전체 탭 UI state/model/mapper 구현 후 타입별 audios/series 선택, tag/adult/original/paging metadata 대상 테스트 성공.
  • Task 3.3: SeriesContentCardView 성인 배지 테스트 작성

    • 생성:
      • app/src/test/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardViewTest.kt
    • 테스트 케이스:
      • inflate 직후 adult badge는 GONE
      • setAdultVisible(true) 호출 시 adult badge는 VISIBLE
      • setAdultVisible(false) 호출 시 adult badge는 GONE
      • setSize(SeriesContentCardSize.Large) 호출 시 adult badge는 24dp 컨테이너와 ic_new_shield_large를 사용한다.
      • setSize(SeriesContentCardSize.Small) 호출 시 adult badge는 18dp 컨테이너와 ic_new_shield_small을 사용한다.
      • adult badge background는 bg_creator_channel_live_adult_badge다.
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
      • Expected: adult badge 미구현으로 RED 실패.
      • 2026-06-25: RED 확인. adult badge 구현 전 테스트를 먼저 추가하고 실행했다. 병렬 Gradle 실행 중 incremental resource merge cache 오류로 실패가 발생해 이후 검증은 순차 실행으로 전환했다.
  • Task 3.4: SeriesContentCardView 성인 배지 구현

    • 수정:
      • app/src/main/res/layout/view_series_content_card.xml
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt
    • 작업:
      • view_series_content_card.xmlFrameLayout 안에 ImageView @+id/iv_series_content_adult_badge를 추가한다.
      • layout gravity는 top|end, 기본 visibility는 gone으로 둔다.
      • background는 @drawable/bg_creator_channel_live_adult_badge로 둔다.
      • SeriesContentCardView에서 adultBadge를 findViewById로 보관한다.
      • setAdultVisible(isVisible: Boolean)을 추가한다.
      • setSize(size)에서 size별 adult badge layout params와 icon을 갱신한다.
        • Large: 24dp, marginTop/marginEnd 8dp, padding 4dp, ic_new_shield_large
        • Small: 18dp, marginTop/marginEnd 6dp, padding 2dp, ic_new_shield_small
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
      • Expected: PASS.
      • 2026-06-25: PASS. view_series_content_card.xml에 adult badge 추가 및 SeriesContentCardView.setAdultVisible()/size별 icon-layout 갱신 구현 후 대상 테스트 성공.

Phase 4: ViewModel 페이징, 타입, 정렬, 요일 상태 구현

  • Task 4.1: ViewModel RED 테스트 작성

    • 생성:
      • app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModelTest.kt
    • 테스트 케이스:
      • 최초 로드는 type=AUDIO, sort=LATEST, page=0, size=20, dayOfWeek=null로 요청한다.
      • SERIES 선택 시 현재 디바이스 요일을 포함해 page=0으로 요청한다.
      • AUDIO, FREE, POINT, ORIGINAL 선택 시 dayOfWeek=null로 요청한다.
      • SERIES 상태에서 요일 변경 시 type=SERIES, 변경된 dayOfWeek, page=0으로 요청한다.
      • 정렬 변경 시 현재 type/day 조건을 유지하고 page=0으로 요청한다.
      • hasNext=true이면 loadMore()가 다음 page를 요청하고 기존 목록 뒤에 append한다.
      • loading-more 중복 요청은 1회로 제한한다.
      • load-more 실패 시 기존 목록을 유지하고 paginationErrorMessage를 설정한다.
      • type/sort/day 변경 후 도착한 이전 응답은 현재 목록에 append하지 않는다.
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentAllTabViewModelTest"
      • Expected: ViewModel 미구현으로 RED 실패.
      • 2026-06-25: RED 확인. ContentAllTabViewModel 미구현 상태에서 unresolved reference 실패를 확인했다.
  • Task 4.2: ContentAllTabViewModel 구현

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModel.kt
    • 작업:
      • DEFAULT_PAGE_SIZE = 20, FIRST_PAGE = 0을 정의한다.
      • allTabStateLiveData, isLoading, toastLiveData를 노출한다.
      • loadInitial(), changeType(type), changeSort(sort), changeDayOfWeek(dayOfWeek), loadMore(), retry(), consumePaginationErrorMessage()를 구현한다.
      • requestGeneration 방식으로 stale response를 무시한다.
      • authToken()은 기존 ViewModel과 동일하게 Bearer ${SharedPreferenceManager.token} 형태를 사용한다.
      • API success + 표시 대상 list empty이면 Empty 상태로 둔다.
      • first page error는 Error 상태와 R.string.common_error_unknown toast로 처리한다.
      • load-more error는 기존 content state를 유지하고 paginationErrorMessage만 설정한다.
    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentAllTabViewModelTest"
      • Expected: PASS.
      • 2026-06-25: PASS. ContentAllTabViewModelAppDI.kt 등록 구현 후 ContentAllTabViewModelTest가 성공했다.

Phase 5: Layout, adapter, Fragment 연결

  • Task 5.1: 전체 탭 layout과 source test 추가

    • 수정:
      • app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt
    • 테스트 케이스:
      • fragment_v2_main_content.xmlview_content_all_type_tabs가 있다.
      • fragment_v2_main_content.xmllayout_content_all_day_filter가 있다.
      • fragment_v2_main_content.xmllayout_content_all_sort_bar가 있다.
      • fragment_v2_main_content.xmlrv_content_all_items가 있다.
      • source에 ContentAllTabViewModel by viewModel()이 있다.
      • source에 ContentAllAudioCardAdapterContentAllSeriesCardAdapter가 있다.
      • source에 MainContentAllType.AUDIO, SERIES, ORIGINAL, FREE, POINT는 있지만 type tab menu에 전체, 연재 매핑은 없다.
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
      • Expected: layout/Fragment 연결 전 RED 실패.
      • 2026-06-25: RED 확인. layout/Fragment 연결 전 @+id/layout_content_all_surface 누락으로 source test 실패를 확인했다.
  • Task 5.2: 전체 탭 item layout과 adapter 구현

    • 생성:
      • app/src/main/res/layout/item_content_all_series_card.xml
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.kt
    • 작업:
      • 오디오 adapter는 AudioContentCardViewAudioContentCardSize.Small을 적용한다.
      • 오디오 adapter는 title/creator/image/tags/adult badge를 바인딩하고 클릭 시 audioContentId를 전달한다.
      • 시리즈 adapter는 SeriesContentCardViewSeriesContentCardSize.Small을 적용한다.
      • 시리즈 adapter는 title/creator/cover/original/adult badge를 바인딩하고 클릭 시 seriesId를 전달한다.
      • adapter는 submitItems(items)로 전체 list를 교체하며, ViewModel이 append된 list를 전달한다.
    • 검증:
      • Run: ./gradlew :app:mergeDebugResources
      • Expected: 신규 item layout binding class가 생성된다.
      • Run: ./gradlew :app:compileDebugKotlin
      • Expected: 신규 adapter가 컴파일된다.
      • 2026-06-25: PASS. adapter와 item layout 구현 후 mergeDebugResources, compileDebugKotlin이 성공했다.
  • Task 5.3: fragment_v2_main_content.xml에 전체 탭 surface 추가

    • 수정:
      • app/src/main/res/layout/fragment_v2_main_content.xml
    • 작업:
      • text_tab_bar_content 아래에 view_content_all_type_tabs include를 추가한다.
      • layout_content_all_day_filter를 추가하고 기본 visibility는 gone으로 둔다.
      • layout_content_all_sort_bar를 추가하고 total count와 sort label view id를 둔다.
        • tv_content_all_total_count
        • layout_content_all_sort_button
        • tv_content_all_sort_label
      • rv_content_all_items RecyclerView를 추가하고 기본 visibility는 gone으로 둔다.
      • 기존 추천/랭킹 surface와 겹치지 않도록 showContentTab()에서 visibility를 제어할 수 있는 root id를 둔다.
        • layout_content_all_surface
    • 검증:
      • Run: ./gradlew :app:mergeDebugResources
      • Expected: layout resource merge가 성공한다.
      • 2026-06-25: PASS. 전체 탭 surface XML 추가 후 mergeDebugResources가 성공했다.
  • Task 5.4: ContentMainFragment 전체 탭 연결

    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
    • 작업:
      • private val contentAllTabViewModel: ContentAllTabViewModel by viewModel()을 추가한다.
      • type tab은 AUDIO, SERIES, ORIGINAL, FREE, POINT 순서로 구성한다.
      • CONTENT_TAB_ALL 선택 시 추천/랭킹 surface를 숨기고 전체 탭 surface를 표시한다.
      • 전체 탭 최초 선택 시 contentAllTabViewModel.loadInitial()을 한 번 호출한다.
      • type 변경 시 contentAllTabViewModel.changeType(...)을 호출한다.
      • SERIES 상태에서만 day filter를 보이고, 다른 type에서는 숨긴다.
      • day filter 클릭 시 changeDayOfWeek(...)를 호출한다.
      • sort button 클릭 시 CreatorChannelSortPopup을 띄우고 changeSort(...)를 호출한다.
      • grid는 GridLayoutManager(spanCount = 3)로 구성한다.
      • 현재 state가 오디오 계열이면 audio adapter를, 시리즈 계열이면 series adapter를 연결한다.
      • RecyclerView 하단 접근 시 loadMore()를 호출한다.
      • paginationErrorMessage는 toast로 표시하고 consumePaginationErrorMessage()를 호출한다.
      • 오디오 클릭은 기존 openAudioContentDetail(audioContentId)를 재사용한다.
      • 시리즈 클릭은 기존 openSeriesDetail(seriesId)를 재사용한다.
    • 검증:
      • Run: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
      • Expected: PASS.
      • Run: ./gradlew :app:compileDebugKotlin
      • Expected: Fragment/ViewBinding/adapter 연결이 컴파일된다.
      • 2026-06-25: PASS. Fragment 전체 탭 연결 후 ContentMainFragmentSourceTestcompileDebugKotlin이 성공했다.

Phase 6: 통합 검증과 수동 확인

  • Task 6.1: 단위 테스트 실행

    • 실행:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • 기대 결과:
      • 전체 탭 mapper/day/ViewModel/source test가 PASS한다.
      • 시리즈 카드 성인 배지 test가 PASS한다.
    • 검증 기록:
      • 2026-06-25: PASS. ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*" 성공.
      • 2026-06-25: PASS. ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest" 성공.
  • Task 6.2: 리소스/컴파일/스타일 검증

    • 실행:
      • ./gradlew :app:mergeDebugResources
      • ./gradlew :app:compileDebugKotlin
      • ./gradlew :app:ktlintCheck
      • git diff --check
    • 기대 결과:
      • resource merge, Kotlin compile, ktlint, whitespace 검증이 모두 성공한다.
    • 검증 기록:
      • 2026-06-25: PASS. ./gradlew :app:mergeDebugResources 성공.
      • 2026-06-25: PASS. ./gradlew :app:compileDebugKotlin 성공.
      • 2026-06-25: PASS. ./gradlew :app:ktlintCheck 성공.
      • 2026-06-25: PASS. git diff --check 성공.
  • Task 6.3: 수동 화면 검증

    • 확인:
      • 콘텐츠 탭 진입 후 추천이 기존처럼 표시된다.
      • 랭킹이 기존처럼 표시된다.
      • 전체 선택 시 오디오 type이 기본 선택된다.
      • type chip에는 오디오, 시리즈, 오리지널, 무료, 포인트만 보인다.
      • type chip에 Figma의 전체, 연재는 보이지 않는다.
      • AUDIO, FREE, POINT는 오디오 카드 3열 grid로 표시된다.
      • SERIES, ORIGINAL은 시리즈 카드 3열 grid로 표시된다.
      • SERIES에서만 요일 필터가 보인다.
      • RANDOM 요일은 한국어 기타, 일본어 その他, 영어 OTHER로 표시된다.
      • sort popup은 기존 ContentSort UI와 동일한 선택 UI로 동작한다.
      • 하단 스크롤 시 다음 페이지가 append된다.
      • 시리즈 성인 콘텐츠는 SeriesContentCardView 우측 상단에 성인 배지를 표시한다.
    • 검증 기록:
      • 2026-06-25: PARTIAL PASS. 디바이스 2cec640c34017ece에서 kr.co.vividnext.sodalive.debug/kr.co.vividnext.sodalive.splash.SplashActivity 실행 성공.
      • 2026-06-25: PASS. 콘텐츠 탭 진입 후 추천이 기존처럼 표시됨을 캡처 sodalive_phase6_content_recommend.png와 hierarchy로 확인했다.
      • 2026-06-25: PASS. 랭킹이 기존처럼 표시됨을 캡처 sodalive_phase6_content_ranking.png와 hierarchy로 확인했다.
      • 2026-06-25: PASS. 전체 선택 시 오디오 type이 기본 선택되고, type chip은 오디오, 시리즈, 오리지널, 무료, 포인트만 표시됨을 캡처 sodalive_phase6_all_audio.png와 hierarchy로 확인했다.
      • 2026-06-25: PASS. type chip에 Figma의 전체, 연재가 표시되지 않음을 hierarchy로 확인했다.
      • 2026-06-25: PASS. SERIES 선택 시에만 요일 필터가 표시되고, RANDOM 요일 한국어 라벨 기타가 표시됨을 캡처 sodalive_phase6_all_series.png와 hierarchy로 확인했다.
      • 2026-06-25: PASS. sort popup이 기존 CreatorChannelSortPopup 형태로 열리고 ContentSort 옵션 최신순, 인기순, 소장순, 높은 가격순, 낮은 가격순이 표시됨을 캡처 sodalive_phase6_sort_popup.png와 hierarchy로 확인했다.
      • 2026-06-25: BLOCKED. GET /api/v2/audio/contents가 수동 검증 환경에서 HTTP 404를 반환해 실제 오디오/시리즈 카드 3열 grid 데이터 표시, 하단 스크롤 append, 시리즈 성인 배지의 실데이터 표시는 확인하지 못했다. 해당 항목은 단위 테스트와 source/layout 검증으로 대체 확인했다.

Verification Log

  • 문서 작성 시점에는 구현 전이므로 실행한 빌드/테스트가 없다.

  • 2026-06-25 Phase 1~3 구현 검증:

    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.MainContentAllDayOfWeekMapperTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.MainContentAllTabMapperTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck (disabled_rules deprecation warning만 출력)
    • PASS: git diff --check
    • 참고: Kotlin LSP는 로컬에 kotlin-lsp가 설치되어 있지 않아 lsp_diagnostics를 사용할 수 없었고, compileDebugKotlin으로 대체 검증했다.
  • 2026-06-25 Phase 1~3 코드 리뷰 및 재검증:

    • PASS: Phase 1~3 변경 범위를 PRD와 계획/TASK 문서 기준으로 검토했고, 차단 이슈는 발견하지 않았다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • 참고: ./gradlew :app:mergeDebugResources는 최초 실행 시 Gradle wrapper lock 파일 접근이 샌드박스에서 차단되어 실패했고, 승인 실행으로 재검증해 성공했다.
  • 2026-06-25 Phase 4~5 구현 검증:

    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck (.editorconfig disabled_rules deprecation warning만 출력)
    • PASS: git diff --check
    • 참고: Kotlin LSP는 로컬에 kotlin-lsp가 설치되어 있지 않아 lsp_diagnostics를 실행할 수 없었고, Gradle compile/tests로 대체 검증했다.
  • 2026-06-25 Phase 4~5 review-fix 재검증:

    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentAllTabViewModelTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck (.editorconfig disabled_rules deprecation warning만 출력)
    • PASS: git diff --check
    • 범위: Empty display-list 처리, sort popup 정리, 현재 탭 guard, 전체 탭 empty/error UI review fix를 포함해 재검증했다.
    • 참고: Kotlin LSP는 로컬에 kotlin-lsp가 설치되어 있지 않아 lsp_diagnostics를 실행할 수 없었고, Gradle compile/tests로 대체 검증했다.
  • 2026-06-25 Phase 4~5 review-blocker 수정 재검증:

    • 수정 범위: Loading/Empty/Error 상태가 selectedType, selectedSort, selectedDayOfWeek, totalCount metadata를 보존하도록 수정한 내용을 포함한다.
    • 수정 범위: ContentMainFragment가 최신 전체 탭 state를 캐시하고, ALL 탭 재진입 시 캐시된 state를 다시 렌더링하도록 수정한 내용을 포함한다.
    • 수정 범위: Empty/Error/Loading 상태에서도 공통 control을 bind해 type/sort/day/count UI가 stale 상태로 남지 않도록 수정한 내용을 포함한다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentAllTabViewModelTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck (.editorconfig disabled_rules deprecation warning만 출력)
    • PASS: git diff --check
    • 참고: Kotlin LSP는 로컬에 kotlin-lsp가 설치되어 있지 않아 lsp_diagnostics를 여전히 실행할 수 없었다.
    • 확인: Phase 6 작업(Task 6.1, Task 6.2, Task 6.3)은 모두 [ ] 상태를 유지한다.
  • 2026-06-25 Phase 4~5 코드 리뷰 및 검증:

    • 리뷰 결과: rvContentAllItems는 3열 grid인데 addContentGridItemSpacing() 내부 ContentGridItemDecoration이 2열 기준 GRID_SPAN_COUNT를 사용해 첫 행/열 spacing이 어긋나는 이슈를 발견했다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • 참고: ./gradlew :app:mergeDebugResources는 최초 실행 시 Gradle wrapper lock 파일 접근이 샌드박스에서 차단되어 실패했고, 승인 실행으로 재검증해 성공했다.
  • 2026-06-25 Phase 4~5 3열 spacing 코드 리뷰 수정 및 재검증:

    • 수정 완료: addContentGridItemSpacing(spanCount)로 item spacing helper를 parameterize하고, rvContentAllItems에는 CONTENT_ALL_GRID_SPAN_COUNT를 전달해 전체 탭 3열 grid spacing 이슈를 수정했다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck (.editorconfig disabled_rules deprecation warning만 출력)
    • PASS: git diff --check
    • 참고: Kotlin LSP는 로컬에 kotlin-lsp가 설치되어 있지 않아 lsp_diagnostics를 여전히 실행할 수 없었다.
    • 확인: Phase 6 작업(Task 6.1, Task 6.2, Task 6.3)은 모두 [ ] 상태를 유지한다.
  • 2026-06-25 Phase 4~5 코드 리뷰 및 검증:

    • 리뷰 결과: Phase 5 Figma 노드 35:5857, 24:6909, 24:9105와 대조했다.
    • 수정 필요: SERIES 요일 필터는 Figma에서 어두운 rounded 컨테이너와 선택 요일의 흰 배경/검은 텍스트가 표시되지만, 현재 layout_content_all_day_filtercreateAllDayFilterView()는 텍스트 색상만 변경해 선택 배경/컨테이너 스타일이 반영되지 않는다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • 참고: ./gradlew :app:mergeDebugResources는 최초 일반 실행에서 Gradle wrapper lock 파일 접근이 샌드박스에서 차단되어 실패했고, 승인 실행으로 재검증해 성공했다.
  • 2026-06-25 Phase 5 리뷰 finding 3열 grid spacing 수정 및 재검증:

    • 수정 완료: ContentRecyclerItemLayoutParamsContentGridItemDecorationisLeftColumn 기반 2열 전제 계산에서 columnIndex/spanCount 기반 offset 계산으로 변경했다. 2열은 기존 인접 gap 8dp를 유지하고, 3열은 1-2열/2-3열 사이 gap이 모두 8dp가 되도록 보정했다.
    • 회귀 방지: ContentMainFragmentSourceTest에 grid spacing이 spanCount 기반으로 계산되고 isLeftColumn을 사용하지 않는다는 source 계약을 추가했다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • 참고: ktlintCheck.editorconfig disabled_rules deprecation warning과 Gradle deprecation warning은 기존 경고로 이번 변경과 무관하다.
    • 확인: Phase 6 작업(Task 6.1, Task 6.2, Task 6.3)은 모두 [ ] 상태를 유지한다.
  • 2026-06-25 Phase 4~5 day-filter/sort-bar Figma 수정 및 재검증:

    • 수정 완료: SERIES 요일 필터를 Figma 기준의 36dp rounded pill 컨테이너와 선택 요일 흰 배경/검은 텍스트 구조로 조정했다.
    • 수정 완료: sort-bar를 Figma 24:6927 기준의 52dp 높이, 좌측 전체 n count, 우측 ContentSort label + ic_new_sort 아이콘 구조로 조정했다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck (.editorconfig disabled_rules deprecation warning만 출력)
    • PASS: git diff --check
    • 참고: Kotlin LSP는 로컬에 kotlin-lsp가 설치되어 있지 않아 lsp_diagnostics를 여전히 실행할 수 없었다.
    • 확인: Phase 6 작업(Task 6.1, Task 6.2, Task 6.3)은 모두 [ ] 상태를 유지한다.
  • 2026-06-25 Phase 4~5 sort count 색상/요일 필터 중앙 정렬 수정 및 재검증:

    • 수정 완료: Figma 24:6909 기준으로 sort-bar의 전체 라벨은 흰색, count 숫자는 gray_500으로 분리 표시하도록 조정했다.
    • 수정 완료: 요일 필터 컨테이너를 parent start/end 양쪽에 constraint하고 layout_constraintHorizontal_bias="0.5"를 적용해 화면 중앙 정렬로 조정했다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck (.editorconfig disabled_rules deprecation warning만 출력)
    • PASS: git diff --check
    • 참고: Kotlin LSP는 로컬에 kotlin-lsp가 설치되어 있지 않아 lsp_diagnostics를 여전히 실행할 수 없었다.
    • 확인: Phase 6 작업(Task 6.1, Task 6.2, Task 6.3)은 모두 [ ] 상태를 유지한다.
  • 2026-06-25 Phase 4~5 코드 리뷰 및 검증:

    • 리뷰 결과: Phase 4 ViewModel paging/type/sort/day 상태와 stale response guard를 테스트 및 구현과 대조했고, 추가 차단 이슈는 발견하지 않았다.
    • 리뷰 결과: Phase 5 layout/adapter/Fragment 연결을 Figma 35:5857, 24:6909, 24:9105 및 source와 대조했고, type tab 제외 범위(전체, 연재 미구현), day filter, sort-bar, 3열 grid, routing 연결에서 추가 차단 이슈는 발견하지 않았다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • 참고: ./gradlew :app:mergeDebugResources는 최초 일반 실행에서 Gradle wrapper lock 파일 접근이 샌드박스에서 차단되어 실패했고, 승인 실행으로 재검증해 성공했다.
    • 확인: Phase 6 작업(Task 6.1, Task 6.2, Task 6.3)은 모두 [ ] 상태를 유지한다.
  • 2026-06-25 Phase 6 통합 검증과 수동 확인:

    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • PASS: Figma 기준 노드 35:5857, 24:6909, 24:9105의 design context와 screenshot을 확보해 화면 기준과 대조했다.
    • PASS: 디바이스 2cec640c34017ece에서 debug 앱 실행 후 콘텐츠 추천, 랭킹, 전체 > 오디오, 전체 > 시리즈, sort popup을 실제 화면으로 확인했다.
    • PARTIAL: 수동 검증 환경에서 GET /api/v2/audio/contentsHTTP 404를 반환해 실제 API 데이터 기반 3열 카드, 페이징 append, 실데이터 성인 배지 표시는 확인하지 못했다. 관련 로직은 Phase 6.1 단위 테스트와 Phase 6.2 컴파일/리소스 검증으로 대체 확인했다.
    • 참고: Kotlin LSP는 로컬에 kotlin-lsp가 설치되어 있지 않아 이번 Phase 6에서도 lsp_diagnostics를 실행하지 못했고, Gradle compile/tests로 대체 검증했다.
  • 2026-06-25 Phase 1~5 코드 리뷰 및 점검:

    • 리뷰 결과: Phase 1~4의 기존 구조 경계, API/DTO/Repository, 요일/타입 mapper, ViewModel paging/type/sort/day 상태 처리는 PRD와 계획/TASK 문서 기준의 추가 차단 이슈를 발견하지 않았다.
    • 리뷰 결과: Phase 5에서 rvContentAllItemsCONTENT_ALL_GRID_SPAN_COUNT = 3을 전달하지만, ContentGridItemDecoration의 offset 계산이 left column 여부만 사용해 3열의 2번째-3번째 열 사이 간격이 1번째-2번째 열 사이보다 좁아질 수 있는 이슈를 발견했다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • 참고: ./gradlew :app:mergeDebugResources는 최초 일반 실행에서 Gradle wrapper lock 파일 접근이 샌드박스에서 차단되어 실패했고, 승인 실행으로 재검증해 성공했다.
  • 2026-06-25 Phase 1~5 spacing 수정 후 코드 리뷰 및 재점검:

    • 리뷰 결과: ContentGridItemDecorationcolumnIndexspanCount 기반 offset 계산으로 변경되어 2열 기본 grid와 전체 탭 3열 grid 모두 인접 열 간격이 GRID_ITEM_GAP_DP로 유지됨을 확인했다.
    • 리뷰 결과: 회귀 방지를 위해 ContentMainFragmentSourceTestspanCount 기반 spacing source 계약이 추가되었고, Phase 1~5 범위에서 추가 차단 이슈는 발견하지 않았다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:compileDebugKotlin
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • 참고: ./gradlew :app:mergeDebugResources는 최초 일반 실행에서 Gradle wrapper lock 파일 접근이 샌드박스에서 차단되어 실패했고, 승인 실행으로 재검증해 성공했다.
  • 2026-06-25 전체 탭 카드 동적 grid width 후속 보정:

    • 수정 완료: ContentAllAudioCardAdapter, ContentAllSeriesCardAdapter에서 fixed setSize(*ContentCardSize.Small) 호출을 제거하고, RecyclerView.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT) 결과를 각 카드의 setGridItemWidthPx()에 전달하도록 변경했다.

    • 수정 완료: AudioContentCardView.setGridItemWidthPx()는 Small typography/gap visual variant를 유지하면서 root/thumbnail/label width를 grid item width로 적용하고 썸네일을 1:1로 유지한다.

    • 수정 완료: SeriesContentCardView.setGridItemWidthPx()는 Small typography/adult badge/gap visual variant를 유지하면서 root/thumbnail/label width를 grid item width로 적용하고 썸네일 높이를 round(width * 172 / 122)로 계산한다.

    • 회귀 방지: ContentMainFragmentSourceTest에 전체 탭 adapter의 fixed Small 호출 부재와 동적 grid width 계산/API 사용 계약을 추가했다.

    • 회귀 방지: SeriesContentCardViewTest에 dynamic width가 series thumbnail width와 122:172 height 및 label width에 적용되는지 검증을 추가했다.

    • 회귀 방지: AudioContentCardViewTest를 추가해 dynamic width가 audio thumbnail width/height 1:1 및 label width에 적용되는지 검증한다.

    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivitySourceTest" --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest" --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentCardViewTest"

    • PASS: ./gradlew :app:mergeDebugResources

    • PASS: ./gradlew :app:compileDebugKotlin

    • PASS: ./gradlew :app:ktlintCheck

    • PASS: git diff --check

    • 참고: 최초 focused test 실행은 CONTENT_ALL_GRID_SPAN_COUNT가 Fragment private companion 상수라 adapter에서 참조할 수 없어 컴파일 실패했고, 상수를 ContentRecyclerItemLayoutParams.kt의 공개 UI 상수로 이동한 뒤 재검증해 통과했다.

    • 참고: 두 번째 focused test 실행은 소개 섹션 source test가 기존 android:textSize="16sp" assertion을 남겨 실패했고, @style/Typography.Body3 계약 assertion으로 보정한 뒤 재검증해 통과했다.

    • 참고: ktlintCheck.editorconfig disabled_rules deprecation warning과 Gradle deprecation warning은 기존 경고로 이번 변경과 무관하다.

  • 2026-06-25: 실제 화면 수동 검증 가능 여부 확인을 위해 adb devices를 실행했으나 연결된 device/emulator가 없어 전면 화면 육안 검증은 수행하지 못했다. 이번 후속 보정은 source/widget 테스트와 Gradle 리소스/컴파일/스타일 검증으로 확인했다.

  • 2026-06-25 전체 탭 카드 measured-width rebind 후속 보정 예정:

    • 범위: rvContentAllItems가 측정 전 0폭으로 최초 bind된 경우에도 layout 완료 후 현재 adapter를 재바인딩해 audio/series 카드가 3열 grid 측정 폭을 다시 적용하도록 보정한다.
    • 회귀 방지: ContentMainFragmentSourceTest에 layout-after-measure rebind source 계약을 추가하고, audio/series 카드 widget 테스트에서 root/card width와 thumbnail MATCH_PARENT 계약을 보강한다.
    • 수정 완료: ContentMainFragment의 전체 탭 grid RecyclerView에 doOnLayout 기반 현재 adapter 재바인딩을 추가해 측정 완료 후 calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT)가 실제 폭으로 다시 계산되도록 보정했다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentCardViewTest" --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • 참고: 최초 focused test 실행은 doOnLayout callback parameter가 View로 추론되어 adapter 참조 컴파일 오류가 발생했고, RecyclerView 명시 캐스팅으로 보정한 뒤 재검증해 통과했다.
    • 참고: ktlintCheck.editorconfig disabled_rules deprecation warning과 Gradle deprecation warning은 기존 경고로 이번 변경과 무관하다.
    • 참고: adb devices에서 연결된 device/emulator가 없어 실제 화면 육안 검증은 수행하지 못했다.
  • 2026-06-25 전체 탭 카드 measured grid width 주입 후속 보정:

    • 수정 이유: onBindViewHolder에서 holder.itemView.parent as? RecyclerView로 폭을 찾는 방식은 최초 bind 시점에 parent가 없거나 아직 측정되지 않으면 0폭을 전달해 카드가 기본 Medium/Large 크기에 머물 수 있어 충분하지 않았다.
    • 수정 완료: ContentMainFragmentrvContentAllItems.calculateContentGridItemWidthPx(CONTENT_ALL_GRID_SPAN_COUNT)로 측정된 3열 item 폭을 계산하고, doOnLayout 및 전체 탭 content bind 전에 ContentAllAudioCardAdapter/ContentAllSeriesCardAdaptersetGridItemWidthPx()로 주입하도록 변경했다.
    • 수정 완료: 두 전체 탭 adapter는 양수 폭이 실제로 바뀔 때만 저장 후 notifyDataSetChanged()를 호출하고, bind 시 저장된 폭만 카드 setGridItemWidthPx()에 전달해 parent 조회 의존성을 제거했다.
    • PASS: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest" --tests "kr.co.vividnext.sodalive.v2.widget.AudioContentCardViewTest" --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest"
    • PASS: ./gradlew :app:mergeDebugResources
    • PASS: ./gradlew :app:ktlintCheck
    • PASS: git diff --check
    • PASS: device 2cec640c34017ece에서 debug 앱 실행 후 콘텐츠 전체 > 오디오, 전체 > 시리즈를 탭하고 UI hierarchy 기준 카드 x bounds가 3열 grid 열 안에 들어오는 것을 확인했다.
    • 참고: 최초 ktlintCheck 실행은 ContentMainFragmentSourceTest의 긴 assertion 한 줄로 실패했고, 줄바꿈 보정 후 재실행해 통과했다.
    • 참고: ktlintCheck.editorconfig disabled_rules deprecation warning과 Gradle deprecation warning은 기존 경고로 이번 변경과 무관하다.