Files
sodalive-android/docs/20260623_메인_콘텐츠_추천_탭/plan-task.md

41 KiB

메인 콘텐츠 추천 탭 구현 계획/TASK

For agentic workers: 각 단계는 체크박스(- [ ])로 추적하고, 완료 즉시 - [x]로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다.

Goal: GET /api/v2/audio/recommendations 응답을 기반으로 메인 콘텐츠 탭 내부 추천 탭에 배너, 오리지널 시리즈, 최신/New&Hot/무료/포인트/댓글/추천 오디오 섹션을 표시한다.

Architecture: 메인 콘텐츠 추천 탭의 Fragment/ViewModel/API/Repository/DTO/UI model/mapper/adapter는 kr.co.vividnext.sodalive.v2.main.content 하위에 둔다. 기존 ContentMainFragment는 해당 패키지로 이동하고, MainV2Activity는 이동된 Fragment를 참조한다. 화면은 XML View + ViewBinding + RecyclerView 기반으로 구성하며, 기존 V2 배너/오디오 카드/태그 위젯은 재사용하고 맞지 않는 섹션만 최소 신규 item view를 만든다.

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


전제와 성공 기준

  • PRD: docs/20260623_메인_콘텐츠_추천_탭/prd.md
  • Figma 전체 화면: cont_001 24:6737
  • API endpoint는 GET /api/v2/audio/recommendations이다.
  • 기본 패키지는 kr.co.vividnext.sodalive.v2.main.content.*이다.
  • 기존 ContentMainFragmentkr.co.vividnext.sodalive.v2.main.content.ContentMainFragment로 이동한다.
  • Jackson @JsonProperty는 사용하지 않고, Gson @SerializedName 정책을 따른다.
  • AudioCardResponse.duration은 DTO에는 유지하지만 이번 UI에는 표시하지 않는다.
  • title-bar와 tab-bar는 고정하고, 배너부터 하위 콘텐츠만 세로 스크롤한다.
  • title-bar 우측 아이콘은 ic_bar_cash, ic_bar_search, ic_bar_storage 순서로 배치한다.
  • ic_bar_cash.png, ic_bar_search.png, ic_bar_storage.pngapp/src/main/res/drawable-mdpi/의 drawable을 사용한다.
  • 이번 범위에서 추천 시리즈, 키워드의 오디오 섹션은 만들지 않는다.
  • 각 API 리스트가 비어 있으면 해당 섹션 전체를 숨긴다.
  • 구현 완료 후 최소 다음 명령을 실행한다.
    • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
    • ./gradlew :app:mergeDebugResources
    • ./gradlew :app:compileDebugKotlin
    • ./gradlew :app:ktlintCheck
    • git diff --check

Figma 참조 필요 Phase

  • Phase 1: 제한 참조
    • 기존 Main V2, DI, title-bar, 위젯 구조 확인 중심으로 진행한다.
  • Phase 2: Figma 참조 불필요
    • API/DTO/Repository/ViewModel 상태 모델은 서버 계약과 기존 V2 홈 추천 패턴을 따른다.
  • Phase 3: 필수 참조
    • 고정 title-bar/tab-bar와 스크롤 영역 분리는 Figma 24:6738, 24:6739, 24:6740을 기준으로 확인한다.
  • Phase 4: 필수 참조
    • 배너, 오직 보이스온에서만!, 새로 올라온 오디오, 무료 오디오, 포인트 오디오, 추천 오디오 카드 크기/간격을 확인한다.
  • Phase 5: 필수 참조
    • New&Hot 리스트 묶음과 최근 댓글이 많은 오디오 댓글 카드 구조를 확인한다.
  • Phase 6: 제한 참조
    • routing, section visibility, loading/error는 기존 코드 패턴 중심으로 검증한다.
  • Phase 7: 필수 참조
    • 최종 수동 화면 검증은 PRD의 모든 포함/제외 섹션과 실제 화면을 대조한다.

파일 구조

  • Move: app/src/main/java/kr/co/vividnext/sodalive/v2/main/ContentMainFragment.kt -> app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
    • 메인 콘텐츠 추천 탭 화면 Fragment다.
  • Modify: app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt
    • 이동된 ContentMainFragment import를 반영한다.
  • Modify: app/src/main/res/layout/fragment_v2_main_content.xml
    • title-bar, 내부 tab-bar, 추천 콘텐츠 scroll container, 섹션별 container/RecyclerView를 구성한다.
  • Create: app/src/main/res/layout/view_title_bar_content.xml
    • view_title_bar_home.xml 구조를 기준으로 하되 우측 아이콘을 ic_bar_cash, ic_bar_search, ic_bar_storage 순서로 둔다.
  • Modify: app/src/main/res/layout/view_audio_content_card.xml
    • 기존 사용처에 영향이 없도록 기본 gone인 성인 badge view를 추가한다.
  • Modify: app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt
    • setAdultVisible(isVisible: Boolean)을 추가해 신규 content 화면에서만 성인 badge를 표시한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRecommendationsApi.kt
    • GET /api/v2/audio/recommendations Retrofit endpoint를 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRecommendationsModels.kt
    • AudioRecommendationsResponse, AudioBannerResponse, OriginalSeriesResponse, AudioCardResponse, CommentedAudioResponse를 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRecommendationsRepository.kt
    • API 호출을 Repository method로 감싼다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainViewModel.kt
    • 추천 데이터 로딩, loading/error/content 상태, toast 상태를 관리한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsUiState.kt
    • Loading, Content, Empty, Error 상태를 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsUiModels.kt
    • 섹션별 UI model과 route model을 정의한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsMappers.kt
    • DTO를 UI model로 변환하고, AudioContentTag 매핑과 section empty 판단을 담당한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentBannerBinder.kt
    • BannerViewBannerItem 바인딩 및 이미지 로딩을 담당한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentOriginalSeriesAdapter.kt
    • originalSeries 가로 목록을 표시한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAudioCardAdapter.kt
    • 최신/무료/포인트/추천 오디오 카드 목록을 공통 처리한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentNewAndHotAdapter.kt
    • New&Hot 3개 row 단위 가로 묶음 리스트를 처리한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentCommentedAudioAdapter.kt
    • 최근 댓글이 많은 오디오 카드와 댓글 영역을 처리한다.
  • Create: app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentRecyclerItemLayoutParams.kt
    • 가로 section item spacing과 grid spacing helper를 둔다.
  • Create: app/src/main/res/layout/item_content_original_series.xml
    • label 없는 오리지널 시리즈 cover item이다.
  • Create: app/src/main/res/layout/item_content_audio_card.xml
    • AudioContentCardView 기반 공통 오디오 카드 item이다.
  • Create: app/src/main/res/layout/item_content_new_and_hot_group.xml
    • 3개 item_content_audio_list row를 담는 가로 paging item이다.
  • Create: app/src/main/res/layout/item_content_audio_list.xml
    • 88dp 썸네일 기반 오디오 리스트 row다. 성인 콘텐츠 badge를 기본 gone으로 포함한다.
  • Create: app/src/main/res/layout/item_content_commented_audio.xml
    • 오디오 리스트 row와 latestComment 댓글 영역을 담는 item이다.
  • Modify: app/src/main/res/values/strings.xml, app/src/main/res/values-en/strings.xml, app/src/main/res/values-ja/strings.xml
    • 콘텐츠 추천 탭/섹션 문구를 추가한다.
  • Modify: app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
    • API, Repository, ViewModel을 Koin에 등록한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRecommendationsMapperTest.kt
    • DTO -> UI model, 태그, duration 미표시, empty section 정책을 검증한다.
  • Create: app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt
    • package 이동, title-bar icon, 제외 섹션 미구현, routing source를 검증한다.

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

  • Task 1.1: 현재 ContentMainFragment와 MainV2 연결 확인

    • 확인:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/ContentMainFragment.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt
      • app/src/main/res/layout/fragment_v2_main_content.xml
    • 작업:
      • ContentMainFragment가 현재 빈 FrameLayout 기반임을 확인한다.
      • MainV2Activity.changeFragment()MainV2Tab.CONTENT -> ContentMainFragment() 연결을 이동 후 import 갱신 대상으로 기록한다.
    • 검증:
      • rg -n "ContentMainFragment|MainV2Tab.CONTENT|fragment_v2_main_content" app/src/main/java app/src/main/res
      • 기대 결과: 이동 전/후 참조 지점이 모두 확인된다.
    • 검증 기록:
      • 2026-06-23: 이동 전 ContentMainFragment가 빈 FragmentV2MainContentBinding inflate Fragment이고, fragment_v2_main_content.xml이 black FrameLayout만 가진 상태임을 확인했다. 이동 후 MainV2Activitykr.co.vividnext.sodalive.v2.main.content.ContentMainFragment import와 MainV2Tab.CONTENT -> ContentMainFragment() 참조를 사용한다.
  • Task 1.2: 재사용 위젯과 신규 item 범위 확정

    • 확인:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerItem.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTag.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt
      • app/src/main/res/layout/view_audio_content_card.xml
      • app/src/main/res/layout/view_series_content_card.xml
    • 작업:
      • 배너는 BannerView/BannerItem 재사용으로 고정한다.
      • 일반 오디오 카드는 AudioContentCardViewAudioContentCardSize.Medium/Large 재사용으로 고정한다.
      • 오직 보이스온에서만!은 label 없는 cover만 필요하므로 기존 SeriesContentCardView를 직접 변경하지 않고 신규 item_content_original_series.xml로 처리한다.
      • New&Hot, 최근 댓글이 많은 오디오는 Figma 구조가 공통 카드와 달라 신규 item view로 처리한다.
    • 검증:
      • PRD의 재사용 후보와 신규 item 범위가 일치하는지 문서상 체크한다.
    • 검증 기록:
      • 2026-06-23: Phase 1~3에서는 adapter/item view를 만들지 않고, Phase 4 이후 계획대로 BannerView, AudioContentCardView, 신규 original/New&Hot/comment item 범위를 유지하는 것으로 확인했다.
  • Task 1.3: title-bar icon asset 확인

    • 확인:
      • app/src/main/res/drawable-mdpi/ic_bar_cash.png
      • app/src/main/res/drawable-mdpi/ic_bar_search.png
      • app/src/main/res/drawable-mdpi/ic_bar_storage.png
      • app/src/main/res/layout/view_title_bar_home.xml
    • 작업:
      • view_title_bar_home.xml은 홈 탭에서 사용 중이므로 직접 수정하지 않는다.
      • 콘텐츠 탭 전용 view_title_bar_content.xml을 생성해 ic_bar_storage 요구사항을 격리한다.
    • 검증:
      • rg --files app/src/main/res/drawable-mdpi | rg "ic_bar_(cash|search|storage)\\.png$"
      • 기대 결과: 3개 파일이 모두 출력된다.
    • 검증 기록:
      • 2026-06-23: ic_bar_cash.png, ic_bar_search.png, ic_bar_storage.pngdrawable-mdpi에 있음을 확인했고, 홈 title-bar는 수정하지 않고 view_title_bar_content.xml을 별도 생성했다.

Phase 2: API, DTO, Repository, DI, ViewModel 계약 추가

  • Task 2.1: API/DTO/Repository 파일 생성

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRecommendationsApi.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRecommendationsModels.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/AudioRecommendationsRepository.kt
    • 작업:
      • AudioRecommendationsApi.getRecommendations(@Header("Authorization") authHeader: String)@GET("/api/v2/audio/recommendations")로 추가한다.
      • DTO에는 PRD의 모든 response class를 추가한다.
      • Gson @SerializedName을 사용하고 Jackson @JsonProperty는 쓰지 않는다.
      • Repository는 기존 V2 홈 추천 Repository처럼 Retrofit Single<ApiResponse<AudioRecommendationsResponse>>를 그대로 반환한다.
    • 검증 명령:
      • ./gradlew :app:compileDebugKotlin
    • 기대 결과:
      • 신규 data layer 파일이 컴파일된다.
    • 검증 기록:
      • 2026-06-23: AudioRecommendationsApi, AudioRecommendationsModels, AudioRecommendationsRepository를 생성했다. ./gradlew :app:compileDebugKotlin 순차 재실행 결과 BUILD SUCCESSFUL.
  • Task 2.2: UI model과 mapper RED 테스트 작성

    • 생성:
      • app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/AudioRecommendationsMapperTest.kt
    • 테스트 케이스:
      • price == 0이면 AudioContentTag.Free가 포함된다.
      • isPointAvailable == true이면 AudioContentTag.Point가 포함된다.
      • isFirstContent == true이면 AudioContentTag.First가 포함된다.
      • isOriginalSeries == true이면 AudioContentTag.Original이 포함된다.
      • isAdult == true이면 UI model의 showAdultBadge가 true다.
      • duration은 UI model에 노출되지 않는다.
      • 빈 리스트 섹션은 isVisible == false 또는 빈 section으로 매핑된다.
      • latestComment가 blank이면 댓글 section item의 댓글 영역 표시 flag가 false다.
    • 검증 명령:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRecommendationsMapperTest"
    • 기대 결과:
      • mapper 구현 전 실패한다.
    • 검증 기록:
      • 2026-06-23: AudioRecommendationsMapperTest를 먼저 추가했고, production data/model 패키지와 toContent()가 없어 compileDebugUnitTestKotlin에서 Unresolved reference 'data', Unresolved reference 'model'로 실패하는 RED를 확인했다.
  • Task 2.3: UI model과 mapper 구현

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsUiState.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsUiModels.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsMappers.kt
    • 작업:
      • AudioRecommendationSection<T> 또는 섹션별 UI model을 정의한다.
      • AudioCardResponse를 공통 ContentAudioCardUiModel로 변환한다.
      • duration은 UI model 필드에 넣지 않는다.
      • isAdultshowAdultBadge 필드로 변환한다.
      • CommentedAudioResponse.latestComment는 댓글 표시용 model에 넣는다.
      • AudioBannerResponseBannerItem 또는 ContentBannerUiModel로 변환한다.
      • OriginalSeriesResponseContentOriginalSeriesUiModel(seriesId, coverImageUrl)로 변환한다.
    • 검증 명령:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRecommendationsMapperTest"
    • 기대 결과:
      • mapper 테스트가 PASS한다.
    • 검증 기록:
      • 2026-06-23: AudioRecommendationsUiState, AudioRecommendationsUiModels, AudioRecommendationsMappers를 구현했다. duration은 UI model에 넣지 않았고, 태그/성인 badge/comment 표시 flag/empty 판단을 테스트로 확인했다. ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRecommendationsMapperTest" 결과 BUILD SUCCESSFUL.
  • Task 2.4: ViewModel과 DI 등록

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainViewModel.kt
    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt
    • 작업:
      • ContentMainViewModelrecommendationsStateLiveData, isLoading, toastLiveData, loadRecommendations()를 추가한다.
      • API success + data 존재 시 AudioRecommendationsUiState.Content를 emit한다.
      • success지만 모든 섹션이 비면 AudioRecommendationsUiState.Empty로 처리한다.
      • error/Throwable은 기존 V2 홈 추천 ViewModel 패턴을 참고해 toast/error 상태로 처리한다.
      • AppDI.networkModuleAudioRecommendationsApi, repositoryModuleAudioRecommendationsRepository, viewModelModuleContentMainViewModel을 등록한다.
    • 검증 명령:
      • ./gradlew :app:compileDebugKotlin
    • 기대 결과:
      • Koin 등록과 ViewModel 생성이 컴파일된다.
    • 검증 기록:
      • 2026-06-23: ContentMainViewModel을 추가하고 AppDInetworkModule, repositoryModule, viewModelModule에 API/Repository/ViewModel을 등록했다. ./gradlew :app:compileDebugKotlin 순차 재실행 결과 BUILD SUCCESSFUL.

Phase 3: Fragment 이동, 고정 상단 UI, 기본 레이아웃 작성

  • Task 3.1: ContentMainFragment 패키지 이동

    • Move:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/ContentMainFragment.kt -> app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt
    • 작업:
      • package를 kr.co.vividnext.sodalive.v2.main.content로 변경한다.
      • MainV2Activity import를 이동된 Fragment로 갱신한다.
    • 검증 명령:
      • ./gradlew :app:compileDebugKotlin
    • 기대 결과:
      • MainV2Tab.CONTENT가 이동된 Fragment를 정상 참조한다.
    • 검증 기록:
      • 2026-06-23: ContentMainFragmentkr.co.vividnext.sodalive.v2.main.content로 이동하고 MainV2Activity import를 갱신했다. ./gradlew :app:compileDebugKotlin 순차 재실행 결과 BUILD SUCCESSFUL.
  • Task 3.2: content 추천 기본 레이아웃 작성

    • 수정:
      • app/src/main/res/layout/fragment_v2_main_content.xml
    • 생성:
      • app/src/main/res/layout/view_title_bar_content.xml
    • 작업:
      • root는 vertical 구조와 black background를 유지한다.
      • 상단에는 view_title_bar_content.xml을 include한다.
      • view_title_bar_content.xmlic_bar_cash, ic_bar_search, ic_bar_storage를 순서대로 표시한다.
      • title-bar 아래에는 @layout/view_text_tab_bar를 include한다.
      • tab-bar 아래에는 세로 scroll container를 배치한다.
      • scroll container 내부에 배너, 오리지널 시리즈, 최신 오디오, New&Hot, 무료 오디오, 포인트 오디오, 최근 댓글, 추천 오디오 section container를 순서대로 배치한다.
      • 추천 시리즈, 키워드의 오디오 section container는 만들지 않는다.
    • 검증 명령:
      • ./gradlew :app:mergeDebugResources
    • 기대 결과:
      • ViewBinding class가 생성되고 resource merge가 PASS한다.
    • 검증 기록:
      • 2026-06-23: fragment_v2_main_content.xml을 고정 title-bar/tab-bar와 NestedScrollView 구조로 작성하고, 배너부터 추천 오디오까지 Phase 3 placeholder section을 배치했다. 추천 시리즈, 키워드의 오디오 section container는 추가하지 않았다. ./gradlew :app:mergeDebugResources 결과 BUILD SUCCESSFUL.
  • Task 3.3: title-bar와 tab-bar 바인딩

    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
      • app/src/main/res/values/strings.xml
      • app/src/main/res/values-en/strings.xml
      • app/src/main/res/values-ja/strings.xml
    • 작업:
      • 내부 tab-bar 메뉴는 추천을 첫 번째로 두고 selected index를 0으로 설정한다.
      • 이번 범위에서는 다른 내부 탭 선택 시 화면 전환을 구현하지 않는다.
      • title-bar 좌측은 이미지 로고가 아니라 하단 대화 탭과 같은 텍스트 title로 콘텐츠를 표시한다.
      • title-bar 우측 icon 순서를 ic_bar_cash, ic_bar_search, ic_bar_storage로 맞춘다.
      • 콘텐츠 추천 섹션 title string을 추가한다.
    • 검증 명령:
      • ./gradlew :app:mergeDebugResources
      • ./gradlew :app:compileDebugKotlin
    • 기대 결과:
      • 문자열과 title-bar drawable 참조가 모두 resolve된다.
    • 검증 기록:
      • 2026-06-23: ContentMainFragment에서 textTabBarContent.root.setMenus(listOf(추천), selectedIndex = 0)를 설정하고, 한국어/영어/일본어 string을 추가했다. view_title_bar_content.xml의 좌측은 하단 대화 탭과 같은 텍스트 title로 screen_content_main_title을 표시하고, 우측 아이콘은 ic_bar_cash, ic_bar_search, ic_bar_storage 순서로 배치했다. ./gradlew :app:mergeDebugResources, ./gradlew :app:compileDebugKotlin 결과 BUILD SUCCESSFUL.

Phase 4: 배너, 오리지널 시리즈, 공통 오디오 카드 섹션 구현

  • Task 4.1: 배너 binder 구현

    • 생성:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentBannerBinder.kt
    • 작업:
      • BannerView.setItems()banners를 바인딩한다.
      • 이미지 로딩은 기존 HomeBannerBinder처럼 ImageView.loadUrl()을 사용한다.
      • 배너가 비면 배너 section container를 숨긴다.
      • 배너 클릭은 route model을 통해 event/creator/series/link 우선순위를 기존 홈 배너 정책과 맞춘다.
    • 검증:
      • 샘플 또는 API 응답으로 배너가 있을 때 carousel/counter가 보이고, 없을 때 section이 숨겨진다.
    • 검증 기록:
      • 2026-06-23: ContentBannerBinder를 추가하고 ContentMainFragment에서 배너 section visibility와 click route를 연결했다. 빈 배너는 ll_content_banner_sectionGONE 처리하고, ContentBannerRoute는 event/creator/series/link 우선순위와 link validation을 source test로 확인했다.
  • Task 4.2: 오리지널 시리즈 adapter 구현

    • 생성:
      • app/src/main/res/layout/item_content_original_series.xml
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentOriginalSeriesAdapter.kt
    • 작업:
      • originalSeries를 가로 RecyclerView로 표시한다.
      • item은 163dp x 230dp cover와 original series tag를 기준으로 구성한다.
      • label은 표시하지 않는다.
      • seriesId <= 0이면 클릭을 무시한다.
      • 클릭 시 SeriesDetailActivity로 이동하고 Constants.EXTRA_SERIES_ID를 전달한다.
    • 검증:
      • originalSeries가 비면 오직 보이스온에서만! 섹션 전체가 숨겨진다.
    • 검증 기록:
      • 2026-06-23: item_content_original_series.xmlContentOriginalSeriesAdapter를 추가하고 가로 RecyclerView에 연결했다. seriesId <= 0 클릭 무시와 SeriesDetailActivity/Constants.EXTRA_SERIES_ID routing source를 확인했다.
  • Task 4.3: 공통 오디오 카드 adapter 구현

    • 생성:
      • app/src/main/res/layout/item_content_audio_card.xml
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAudioCardAdapter.kt
    • 수정:
      • app/src/main/res/layout/view_audio_content_card.xml
      • app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt
    • 작업:
      • view_audio_content_card.xml의 thumbnail container 우측 상단에 기본 gone인 adult badge를 추가한다.
      • AudioContentCardViewsetAdultVisible(isVisible: Boolean)을 추가하고 기존 호출부 기본 동작은 변경하지 않는다.
      • AudioContentCardView를 inflate한다.
      • latestAudios, freeAudios, pointAudios에는 AudioContentCardSize.Medium을 사용한다.
      • recommendedAudios에는 AudioContentCardSize.Large를 사용한다.
      • setContent(title, creatorNickname)setTags(tags)를 사용한다.
      • showAdultBadgesetAdultVisible(showAdultBadge)로 표시한다.
      • duration은 표시하지 않는다.
      • 이미지 로딩은 thumbnailView().loadUrl(imageUrl)을 사용한다.
      • 클릭 시 AudioContentDetailActivity로 이동하고 Constants.EXTRA_AUDIO_CONTENT_ID를 전달한다.
    • 검증:
      • 무료/포인트/첫 콘텐츠/오리지널 태그 조합이 mapper 테스트와 화면에서 일치한다.
    • 검증 기록:
      • 2026-06-23: item_content_audio_card.xml, ContentAudioCardAdapter, AudioContentCardView.setAdultVisible() 및 기본 gone 성인 badge를 추가했다. 최신/무료/포인트는 AudioContentCardSize.Medium, 추천은 AudioContentCardSize.Large로 연결했고 duration은 표시하지 않았다.
  • Task 4.4: 최신/무료/포인트/추천 섹션 Fragment 바인딩

    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
      • app/src/main/res/layout/fragment_v2_main_content.xml
    • 작업:
      • 최신/무료/포인트 RecyclerView는 horizontal layout manager를 사용한다.
      • 추천 오디오 RecyclerView는 2열 grid layout manager를 사용한다.
      • 각 section은 item list가 비면 title과 list를 함께 숨긴다.
      • 추천 오디오 홀수 item은 마지막 item이 좌측 정렬되도록 기본 GridLayoutManager 정책을 사용한다.
    • 검증:
      • latestAudios, freeAudios, pointAudios, recommendedAudios 각각의 empty/non-empty 상태를 수동 확인한다.
    • 검증 기록:
      • 2026-06-23: 최신/무료/포인트 horizontal RecyclerView와 추천 2열 GridLayoutManager를 바인딩했다. 각 section은 item list가 비면 container 전체를 GONE 처리하도록 구현했다.

Phase 5: New&Hot와 최근 댓글 섹션 구현

  • Task 5.1: New&Hot group adapter 구현

    • 생성:
      • app/src/main/res/layout/item_content_new_and_hot_group.xml
      • app/src/main/res/layout/item_content_audio_list.xml
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentNewAndHotAdapter.kt
    • 작업:
      • newAndHotAudios를 3개 단위 group으로 chunking한다.
      • group item은 세로로 최대 3개 item_content_audio_list row를 표시한다.
      • 각 row는 88dp 썸네일, 제목, 크리에이터명, 태그를 표시한다.
      • showAdultBadge == true인 row는 우측 상단 성인 badge를 표시한다.
      • 마지막 group이 3개 미만이면 빈 row를 만들지 않는다.
      • 클릭 시 오디오 상세로 이동한다.
    • 검증:
      • item 수 1, 2, 3, 4, 6개 샘플에서 group 수와 row 수가 의도대로 보인다.
    • 검증 기록:
      • 2026-06-23: ContentNewAndHotAdapter에서 items.chunked(NEW_AND_HOT_GROUP_SIZE)private const val NEW_AND_HOT_GROUP_SIZE = 3 정책을 구현했다. 마지막 group이 3개 미만이면 실제 item row만 inflate하도록 처리했다.
  • Task 5.2: 최근 댓글이 많은 오디오 adapter 구현

    • 생성:
      • app/src/main/res/layout/item_content_commented_audio.xml
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentCommentedAudioAdapter.kt
    • 작업:
      • mostCommentedAudios를 가로 RecyclerView로 표시한다.
      • 각 item에는 88dp 오디오 썸네일 row와 댓글 영역을 표시한다.
      • 댓글 영역에는 latestComment를 표시한다.
      • latestComment가 blank이면 댓글 영역을 숨긴다.
      • latestCommentWriterProfileImageUrl은 댓글 작성자 프로필 이미지로 로딩한다.
      • 클릭 시 오디오 상세로 이동한다.
    • 검증:
      • 댓글 본문이 있는 item은 댓글 영역이 보이고, blank 댓글 item은 댓글 영역이 숨겨진다.
    • 검증 기록:
      • 2026-06-23: item_content_commented_audio.xmlContentCommentedAudioAdapter를 추가했다. layoutContentCommentArea.isVisible = item.showLatestComment로 blank 댓글 영역을 숨기고, ivContentCommentProfile.loadUrl로 댓글 작성자 프로필 이미지를 로딩하도록 구현했다.
  • Task 5.3: New&Hot와 댓글 섹션 Fragment 바인딩

    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
      • app/src/main/res/layout/fragment_v2_main_content.xml
    • 작업:
      • New&Hot는 horizontal group RecyclerView로 바인딩한다.
      • 최근 댓글이 많은 오디오는 horizontal card RecyclerView로 바인딩한다.
      • 각 section은 item list가 비면 전체를 숨긴다.
    • 검증:
      • Figma 순서상 New&Hot가 최신 오디오 뒤, 무료 오디오 앞에 배치되는지 확인한다.
      • 최근 댓글이 많은 오디오가 포인트 오디오 뒤, 추천 오디오 앞에 배치되는지 확인한다.
    • 검증 기록:
      • 2026-06-23: fragment_v2_main_content.xml의 Phase 3 section 순서를 유지하며 New&Hot는 최신 뒤/무료 앞, 최근 댓글은 포인트 뒤/추천 앞에 adapter를 연결했다. 제외 섹션 id/string은 추가하지 않았다.

Phase 6: 실제 API 상태 바인딩, routing, 제외 섹션 검증

  • Task 6.1: ViewModel observe와 실제 API 로딩 연결

    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
    • 작업:
      • ContentMainViewModel을 Koin으로 주입한다.
      • onViewCreated()에서 adapters, title/tab bar, observers를 설정한다.
      • 최초 진입 시 loadRecommendations()를 호출한다.
      • Loading은 기존 V2 홈 추천의 LoadingDialog 패턴을 참고한다.
      • Content는 모든 section bind method에 전달한다.
      • Error 또는 전체 empty는 기존 정책에 맞춰 빈 콘텐츠 또는 toast를 표시한다.
    • 검증:
      • API 성공/실패를 개발 환경에서 확인하거나 ViewModel 테스트로 상태 전환을 확인한다.
    • 검증 기록:
      • 2026-06-23: ContentMainViewModel을 Koin by viewModel()로 주입하고 recommendationsStateLiveData, isLoading, toastLiveData observer를 연결했다. LoadingDialog(requireActivity(), layoutInflater), loadingDialog.show(screenWidth), toastMessage?.let(::showToast), 최초 loadRecommendations() 호출을 source test와 컴파일로 확인했다.
  • Task 6.2: routing source test 작성

    • 생성:
      • app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt
    • 테스트 케이스:
      • ContentMainFragment package가 kr.co.vividnext.sodalive.v2.main.content인지 검증한다.
      • MainV2Activity가 이동된 ContentMainFragment를 import하는지 검증한다.
      • fragment_v2_main_content.xml 또는 content title bar가 ic_bar_cash, ic_bar_search, ic_bar_storage를 참조하는지 검증한다.
      • 추천 시리즈, 키워드의 오디오 관련 section id/string이 추가되지 않았는지 검증한다.
      • 오디오 상세 이동이 AudioContentDetailActivityConstants.EXTRA_AUDIO_CONTENT_ID를 사용하는지 검증한다.
      • 시리즈 상세 이동이 SeriesDetailActivityConstants.EXTRA_SERIES_ID를 사용하는지 검증한다.
    • 검증 명령:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"
    • 기대 결과:
      • 구현 전 실패하고, Phase 6 완료 후 PASS한다.
    • 검증 기록:
      • 2026-06-23: 기존 RED ContentMainFragmentSourceTest에 Android Uri/RuntimeEnvironment 검증이 포함되어 Robolectric runner와 기본 Application 설정을 추가했다. 이후 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest" 결과 BUILD SUCCESSFUL.
  • Task 6.3: 배너 routing 구현

    • 수정:
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/AudioRecommendationsUiModels.kt
      • app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt
    • 작업:
      • 기존 HomeRecommendationBannerRoute 정책을 참고해 eventItem, creatorId, seriesId, link 순서 또는 기존 우선순위를 그대로 적용한다.
      • creatorId > 0이면 CreatorChannelActivity.newIntent(requireContext(), creatorId)로 이동한다.
      • seriesId > 0이면 SeriesDetailActivity로 이동한다.
      • link는 기존 홈 배너 route 변환 정책을 확인해 동일하게 처리한다.
      • 목적지가 없으면 클릭을 무시한다.
    • 검증:
      • 목적지별 배너 샘플로 intent extras가 의도대로 들어가는지 확인한다.
    • 검증 기록:
      • 2026-06-23: ContentBannerRoute, toContentBannerRoute(), toContentBannerIntent(context)를 추가했다. event는 Constants.EXTRA_EVENT, creator는 CreatorChannelActivity.newIntent, series는 Constants.EXTRA_SERIES_ID, link/deep link는 Intent.ACTION_VIEW로 생성됨을 source test로 확인했다.

Phase 7: 통합 검증과 문서 검증 기록

  • Task 7.1: 단위 테스트와 리소스/컴파일 검증

    • 실행:
      • ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"
      • ./gradlew :app:mergeDebugResources
      • ./gradlew :app:compileDebugKotlin
      • ./gradlew :app:ktlintCheck
      • git diff --check
    • 기대 결과:
      • 모든 명령이 PASS한다.
    • 검증 기록:
      • 2026-06-23: ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*", ./gradlew :app:mergeDebugResources, ./gradlew :app:compileDebugKotlin, ./gradlew :app:ktlintCheck, git diff --check를 순차 실행했고 모두 성공했다. Gradle 명령은 모두 BUILD SUCCESSFUL로 종료됐고, git diff --check는 출력 없이 통과했다.
  • Task 7.2: 수동 UI 검증

    • 확인:
      • title-bar와 tab-bar가 스크롤 중 고정된다.
      • 스크롤 영역은 배너부터 시작한다.
      • 섹션 순서는 배너 → 오직 보이스온에서만! → 새로 올라온 오디오 → New&Hot → 무료 오디오 → 포인트 오디오 → 최근 댓글이 많은 오디오 → 추천 오디오다.
      • 추천 시리즈, 키워드의 오디오는 표시되지 않는다.
      • 각 section의 empty 상태에서 빈 title/list 공간이 남지 않는다.
      • duration은 어떤 오디오 카드에도 표시되지 않는다.
      • 태그는 price, isPointAvailable, isFirstContent, isOriginalSeries 정책과 일치한다.
      • 성인 badge는 isAdult 정책과 일치한다.
      • 배너, 오디오, 시리즈 item 클릭 routing이 의도대로 동작한다.
    • 검증 기록:
      • 2026-06-23: 실제 기기 SM-G960N - Android 10(2cec640c34017ece)에 ./gradlew :app:installDebug로 debug APK를 설치했고 BUILD SUCCESSFUL을 확인했다. 런처 실행 후 MainV2Activity에 진입했고, 하단 콘텐츠 탭을 탭해 콘텐츠 화면으로 이동했다. 화면 크기는 UI hierarchy 기준 1080 x 2076이었다.
      • 2026-06-23: 콘텐츠 화면 상단에서 title-bar 콘텐츠, 우측 아이콘 iv_title_bar_cashiv_title_bar_searchiv_title_bar_storage, 내부 tab-bar 추천 selected 상태를 확인했다. nsv_content_recommendation_content가 title-bar/tab-bar 아래 [0,408][1080,1836] 영역에 배치되어 상단 고정 영역과 스크롤 영역이 분리되어 있음을 확인했다.
      • 2026-06-23: 실제 API 응답 기준 화면은 ll_content_recommendation_content 내부 item이 없는 빈 상태로 렌더링되었다. 이에 따라 non-empty 데이터가 필요한 배너/섹션 순서, 태그/성인 badge, 카드 duration 미표시, item 클릭 routing은 실기기에서 최종 수동 확인하지 못했다. 해당 항목은 Phase 4~6 source test, mapper test, compile/ktlint 검증으로 대체 확인했다.
      • 2026-06-23: 실제 화면과 UI hierarchy에서 추천 시리즈, 키워드의 오디오 텍스트나 관련 section은 표시되지 않음을 확인했다.

Verification Log

  • 구현 중 여러 Phase에 걸친 통합 검증, 회귀 검증, 최종 수동 확인 기록을 여기에 누적한다.

  • 2026-06-23: Phase 1~3 구현 후 ./gradlew :app:mergeDebugResources, ./gradlew :app:compileDebugKotlin, ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*", ./gradlew :app:ktlintCheck, git diff --check를 실행했고 모두 성공했다.

  • 2026-06-23: Reviewer gate 재검토 결과 목표/범위, 코드 품질, QA, 컨텍스트 검토는 PASS였고, security review에서 debug HTTP BODY 로그의 Authorization 헤더 노출 가능성이 blocking으로 지적되었다. AppDI primary HttpLoggingInterceptorlogging.redactHeader("Authorization")를 추가한 뒤 ./gradlew :app:compileDebugKotlin, ./gradlew :app:ktlintCheck, git diff --check, content 단위 테스트를 재실행해 모두 성공했고, security re-review 결과 PASS 및 blocking issue 없음으로 확인했다.

  • 2026-06-23: lsp_diagnosticskotlin-ls 미설치(Command not found: kotlin-lsp)로 실행하지 못했다. 대신 Gradle compile/test/ktlint와 reviewer gate로 Phase 1~3 변경을 검증했다.

  • 2026-06-23: 후속 요청에 따라 content title-bar 좌측을 이미지 로고에서 하단 대화 탭과 같은 텍스트 title(screen_content_main_title, 콘텐츠)로 변경했다. ./gradlew :app:mergeDebugResources, ./gradlew :app:compileDebugKotlin, git diff --check 결과 모두 성공했다. XML lsp_diagnostics는 XML LSP 서버 미설정으로 실행하지 못했다.

  • 2026-06-23: 리뷰 지적 중 무료/포인트 섹션의 tag 강제 표시 여부를 재검토했다. AudioRecommendationsMappers, 기존 홈/크리에이터 채널 mapper, mapper 테스트, ast-grep 검색 결과 모두 tag는 섹션 소속이 아니라 item 속성(price == 0, isPointAvailable == true, isFirstContent, isOriginalSeries) 기반으로 산출하는 정책이었다. 이에 맞춰 PRD의 무료 태그는 반드시 표시한다, 포인트 태그는 반드시 표시한다 문구를 섹션 membership으로 강제하지 않는 속성 기반 표시 정책으로 정정했다.

  • 2026-06-23: Phase 1~3 코드 리뷰 및 검증을 재수행했다. ContentMainFragment 패키지 이동과 MainV2Activity 참조, AudioRecommendations API/DTO/Repository/ViewModel/mapper, 고정 title-bar/tab-bar 레이아웃, 제외 섹션 미추가 상태를 확인했고 blocking issue는 발견하지 못했다. ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*", ./gradlew :app:mergeDebugResources, ./gradlew :app:compileDebugKotlin, ./gradlew :app:ktlintCheck, git diff --check 결과 모두 성공했다.

  • 2026-06-23: Phase 4~6 구현 후 RED ContentMainFragmentSourceTestContentBannerRoute 미구현으로 실패하는 것을 먼저 확인했다. 구현 후 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*", ./gradlew :app:mergeDebugResources, ./gradlew :app:compileDebugKotlin, ./gradlew :app:ktlintCheck, git diff --check를 순차 실행했고 모두 성공했다. 병렬 Gradle 검증 중 Kotlin incremental cache 충돌이 한 번 발생했으나 ./gradlew --stop 후 순차 재실행으로 성공을 확인했다. Kotlin/XML lsp_diagnosticskotlin-lsp 및 XML LSP 서버 미설치로 실행하지 못했다.

  • 2026-06-23: Phase 4~6 코드 리뷰 및 검증을 재수행했다. 배너/오리지널 시리즈/공통 오디오 카드/New&Hot/최근 댓글 adapter, 실제 API 상태 observer, section visibility, 오디오/시리즈/배너 routing, 제외 섹션 미추가 상태를 확인했고 blocking issue는 발견하지 못했다. ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*", ./gradlew :app:mergeDebugResources, ./gradlew :app:compileDebugKotlin, ./gradlew :app:ktlintCheck, git diff --check 결과 모두 성공했다. :app:mergeDebugResources는 최초 sandbox 실행에서 ~/.gradle lock 파일 권한으로 실패해 승인 후 재실행했고 성공했다.

  • 2026-06-23: Phase 7 통합 검증으로 ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*", ./gradlew :app:mergeDebugResources, ./gradlew :app:compileDebugKotlin, ./gradlew :app:ktlintCheck, git diff --check를 순차 실행했고 모두 성공했다. 실제 기기 SM-G960N - Android 10에 debug APK를 설치해 콘텐츠 탭 진입, title-bar/tab-bar 고정 영역, 제외 섹션 미표시를 확인했다. 다만 실제 API 응답이 빈 상태여서 non-empty 섹션과 item routing은 실기기에서 확인하지 못했고 기존 source/mapper 테스트와 컴파일 검증으로 대체했다.

  • 2026-06-25: 리뷰 지적에 따라 ContentRecyclerItemLayoutParams의 grid item offset을 left column 여부 기반 계산에서 spanCountcolumnIndex 기반 계산으로 수정했다. 기본 2열은 기존처럼 인접 gap 4dp + 4dp를 유지하고, 전체 탭 3열은 1-2열/2-3열 사이 gap이 모두 8dp가 되도록 보정했다. 회귀 방지를 위해 ContentMainFragmentSourceTest에 3열 spacing 계약을 추가했고, ./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest", ./gradlew :app:compileDebugKotlin, ./gradlew :app:ktlintCheck를 실행해 모두 BUILD SUCCESSFUL을 확인했다. ktlintCheck.editorconfig disabled_rules deprecation 경고와 Gradle deprecation warning은 기존 경고로 이번 변경과 무관하다.