28 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다. dayOfWeek는type=SERIES일 때만 전송한다.- 기본
dayOfWeek는 디바이스 현재 요일을SeriesPublishedDaysOfWeek로 변환한다. - UI 타입 탭은
오디오,시리즈,오리지널,무료,포인트만 구현한다. - Figma에 보이는
전체,연재콘텐츠 타입 칩은 이번 범위에서 구현하지 않는다. SERIES,ORIGINAL은 응답의series목록을 표시한다.AUDIO,FREE,POINT는 응답의audios목록을 표시한다.- 이번 범위의
ORIGINAL은 오리지널 시리즈 목록만 의미한다. RANDOM요일 라벨은 한국어기타, 일본어その他, 영어OTHER로 표시한다.- 정렬 옵션은 기존
ContentSort.entries전체를 사용하고, UI는CreatorChannelSortPopup/ContentSort.toLabelResId()패턴을 따른다. SeriesContentCardView는isAdult성인 배지를 지원해야 한다.SeriesContentCardSize.Large:ic_new_shield_largeSeriesContentCardSize.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:ktlintCheckgit diff --check
Figma 참조 필요 Phase
- Phase 1: 제한 참조
- 기존 콘텐츠 추천/랭킹 구조, DI, sort popup, 카드 위젯 구조 확인 중심으로 진행한다.
- Phase 2: Figma 참조 불필요
- API/DTO/Repository와 enum/요일 변환은 PRD 서버 계약과 기존 V2 data layer 패턴을 따른다.
- Phase 3: 필수 참조
SeriesContentCardView성인 배지는 Figma567:18346,567:18347과 기존 오디오 배지 구조를 대조한다.
- Phase 4: Figma 참조 불필요
- ViewModel 페이징/정렬/요일 상태는 기존 크리에이터 채널 pagination 패턴을 따른다.
- Phase 5: 필수 참조
- 전체 탭 type tab, 요일 필터, 정렬 바, 3열 그리드 위치는 Figma
35:5857,24:6909,24:9105를 기준으로 확인한다.
- 전체 탭 type tab, 요일 필터, 정렬 바, 3열 그리드 위치는 Figma
- Phase 6: 필수 참조
- 최종 수동 화면 검증은 PRD의 모든 포함/제외 항목과 실제 화면을 대조한다.
파일 구조
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabApi.ktGET /api/v2/audio/contentsRetrofit endpoint를 정의한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabModels.ktMainContentAllTabResponse,MainContentAllType,MainContentAudioResponse,MainContentSeriesResponseDTO를 정의한다.
- 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.ktLoading,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선택 규칙을 구현한다.
- DTO를 UI model로 변환하고 타입별
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllDayOfWeekMapper.kt- 디바이스 현재 요일과 UI 라벨을
SeriesPublishedDaysOfWeek로 매핑한다.
- 디바이스 현재 요일과 UI 라벨을
- 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.ktMainContentAllTabApi,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.ktsetAdultVisible(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.xmlSeriesContentCardView기반 전체 탭 시리즈 카드 item이다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.ktSERIES,ORIGINALgrid item을 바인딩한다.
- Create:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.ktAUDIO,FREE,POINTgrid 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 라벨, 요일
기타, 정렬/빈 목록/오류 표시 문구를 추가한다.
- 전체 탭 type 라벨, 요일
- Modify:
app/src/main/res/values-en/strings.xml- 전체 탭 type 라벨, 요일
OTHER, 정렬/빈 목록/오류 표시 문구를 추가한다.
- 전체 탭 type 라벨, 요일
- Modify:
app/src/main/res/values-ja/strings.xml- 전체 탭 type 라벨, 요일
その他, 정렬/빈 목록/오류 표시 문구를 추가한다.
- 전체 탭 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를 검증한다.
- 전체 탭 layout id, adapter/ViewModel 연결,
Phase 1: 기존 구조 확인과 작업 경계 고정
-
Task 1.1: 기존 콘텐츠 탭 구조와 전체 탭 삽입 지점 확인
- 확인:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.ktapp/src/main/res/layout/fragment_v2_main_content.xmlapp/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainViewModel.ktapp/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 지점이 확인된다.
- Run:
- 확인:
-
Task 1.2: 재사용 위젯과 신규 adapter 경계 확정
- 확인:
app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/widget/CapsuleTabBarView.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/ui/CreatorChannelSortPopup.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/model/CreatorChannelSortModels.kt
- 작업:
- 타입 칩은
CapsuleTabBarView재사용으로 고정한다. - 정렬 팝업은
CreatorChannelSortPopup과ContentSort.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: 재사용 후보 클래스와 함수가 확인된다.
- Run:
- 확인:
-
Task 1.3: 제외 범위 확인
- 확인:
docs/20260625_메인_콘텐츠_탭_내부_전체_탭/prd.md
- 제외:
- Figma type chip
전체,연재구현 - 추천/랭킹 탭 기존 기능 변경
- 레거시 API/화면 파일 직접 수정
- 오리지널 오디오 별도 표시
- 오프라인 캐시/로컬 DB 저장
- Figma type chip
- 검증:
- Run:
rg -n '전체|연재|Non-Goals|오리지널 오디오|레거시|Open Questions' docs/20260625_메인_콘텐츠_탭_내부_전체_탭/prd.md - Expected: 제외 범위와 Open Questions 없음 상태가 확인된다.
- Run:
- 확인:
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.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabModels.ktapp/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를 정의한다. dayOfWeekparameter는 nullable로 두고 repository/ViewModel에서SERIES일 때만 값을 전달한다.- DTO는 PRD response class 필드를 모두 포함하되 Gson
@SerializedName을 사용한다. MainContentAllType은AUDIO,SERIES,ORIGINAL,FREE,POINT만 정의한다.ContentSort와SeriesPublishedDaysOfWeek는 기존 타입을 import한다.- Koin
networkModule,repositoryModule에 신규 API/Repository를 등록한다.
- Retrofit endpoint는
- 검증:
- Run:
./gradlew :app:compileDebugKotlin - Expected: 신규 data layer와 DI 등록이 컴파일된다.
- Run:
- 생성:
-
Task 2.2: 타입 라벨과 요일 라벨 리소스 추가
- 수정:
app/src/main/res/values/strings.xmlapp/src/main/res/values-en/strings.xmlapp/src/main/res/values-ja/strings.xml
- 작업:
- type tab 라벨 문자열을 추가한다.
screen_content_all_type_audioscreen_content_all_type_seriesscreen_content_all_type_originalscreen_content_all_type_freescreen_content_all_type_point
- 전체 탭 요일
RANDOM전용 문자열을 추가한다.screen_content_all_day_other- values:
기타 - values-en:
OTHER - values-ja:
その他
- 기존 전역
day_random은 레거시 사용처 영향 방지를 위해 수정하지 않는다. - 빈 목록/페이징 오류 표시를 위해 전체 탭 전용 문자열을 추가한다.
screen_content_all_emptyscreen_content_all_pagination_error
- type tab 라벨 문자열을 추가한다.
- 검증:
- Run:
./gradlew :app:mergeDebugResources - Expected: 3개 locale string resource가 중복 없이 merge된다.
- Run:
- 수정:
-
Task 2.3: 요일 mapping 테스트 작성
- 생성:
app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/MainContentAllDayOfWeekMapperTest.kt
- 테스트 케이스:
Calendar.MONDAY는SeriesPublishedDaysOfWeek.MONCalendar.TUESDAY는SeriesPublishedDaysOfWeek.TUECalendar.WEDNESDAY는SeriesPublishedDaysOfWeek.WEDCalendar.THURSDAY는SeriesPublishedDaysOfWeek.THUCalendar.FRIDAY는SeriesPublishedDaysOfWeek.FRICalendar.SATURDAY는SeriesPublishedDaysOfWeek.SATCalendar.SUNDAY는SeriesPublishedDaysOfWeek.SUNSeriesPublishedDaysOfWeek.RANDOM라벨은screen_content_all_day_other
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.MainContentAllDayOfWeekMapperTest" - Expected: mapper 구현 전 RED 실패.
- Run:
- 생성:
-
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를 추가한다.RANDOM은R.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.
- Run:
- 생성:
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는 responseaudios만 UI 목록으로 사용한다.SERIES,ORIGINAL은 responseseries만 UI 목록으로 사용한다.ORIGINAL은 시리즈 카드 타입으로 매핑한다.isAdult는 오디오/시리즈 UI model의showAdultBadge로 매핑된다.- 오디오
isPointAvailable=true는AudioContentTag.Point로 매핑된다. - 오디오
isFirstContent=true는AudioContentTag.First로 매핑된다. - 오디오
isOriginalSeries=true는AudioContentTag.Original로 매핑된다. - 오디오
price == 0은AudioContentTag.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 실패.
- Run:
- 생성:
-
Task 3.2: 전체 탭 UI model과 mapper 구현
- 생성:
app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiState.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabUiModels.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/main/content/model/MainContentAllTabMappers.kt
- 작업:
MainContentAllAudioUiModel은audioContentId,title,imageUrl,price,creatorNickname,tags,showAdultBadge를 가진다.MainContentAllSeriesUiModel은seriesId,title,coverImageUrl,creatorNickname,showOriginalTag,showAdultBadge를 가진다.MainContentAllTabUiState.Content는selectedType,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.
- Run:
- 생성:
-
Task 3.3: SeriesContentCardView 성인 배지 테스트 작성
- 생성:
app/src/test/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardViewTest.kt
- 테스트 케이스:
- inflate 직후 adult badge는
GONE setAdultVisible(true)호출 시 adult badge는VISIBLEsetAdultVisible(false)호출 시 adult badge는GONEsetSize(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다.
- inflate 직후 adult badge는
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest" - Expected: adult badge 미구현으로 RED 실패.
- Run:
- 생성:
-
Task 3.4: SeriesContentCardView 성인 배지 구현
- 수정:
app/src/main/res/layout/view_series_content_card.xmlapp/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt
- 작업:
view_series_content_card.xml의FrameLayout안에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
- Large: 24dp, marginTop/marginEnd 8dp, padding 4dp,
- 검증:
- Run:
./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.widget.SeriesContentCardViewTest" - Expected: PASS.
- Run:
- 수정:
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 실패.
- Run:
- 생성:
-
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_unknowntoast로 처리한다. - 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.
- Run:
- 생성:
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.xml에view_content_all_type_tabs가 있다.fragment_v2_main_content.xml에layout_content_all_day_filter가 있다.fragment_v2_main_content.xml에layout_content_all_sort_bar가 있다.fragment_v2_main_content.xml에rv_content_all_items가 있다.- source에
ContentAllTabViewModel by viewModel()이 있다. - source에
ContentAllAudioCardAdapter와ContentAllSeriesCardAdapter가 있다. - 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 실패.
- Run:
- 수정:
-
Task 5.2: 전체 탭 item layout과 adapter 구현
- 생성:
app/src/main/res/layout/item_content_all_series_card.xmlapp/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllAudioCardAdapter.ktapp/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ui/ContentAllSeriesCardAdapter.kt
- 작업:
- 오디오 adapter는
AudioContentCardView에AudioContentCardSize.Small을 적용한다. - 오디오 adapter는 title/creator/image/tags/adult badge를 바인딩하고 클릭 시
audioContentId를 전달한다. - 시리즈 adapter는
SeriesContentCardView에SeriesContentCardSize.Small을 적용한다. - 시리즈 adapter는 title/creator/cover/original/adult badge를 바인딩하고 클릭 시
seriesId를 전달한다. - adapter는
submitItems(items)로 전체 list를 교체하며, ViewModel이 append된 list를 전달한다.
- 오디오 adapter는
- 검증:
- Run:
./gradlew :app:mergeDebugResources - Expected: 신규 item layout binding class가 생성된다.
- Run:
./gradlew :app:compileDebugKotlin - Expected: 신규 adapter가 컴파일된다.
- Run:
- 생성:
-
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_tabsinclude를 추가한다.layout_content_all_day_filter를 추가하고 기본 visibility는gone으로 둔다.layout_content_all_sort_bar를 추가하고 total count와 sort label view id를 둔다.tv_content_all_total_countlayout_content_all_sort_buttontv_content_all_sort_label
rv_content_all_itemsRecyclerView를 추가하고 기본 visibility는gone으로 둔다.- 기존 추천/랭킹 surface와 겹치지 않도록
showContentTab()에서 visibility를 제어할 수 있는 root id를 둔다.layout_content_all_surface
- 검증:
- Run:
./gradlew :app:mergeDebugResources - Expected: layout resource merge가 성공한다.
- Run:
- 수정:
-
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 연결이 컴파일된다.
- Run:
- 수정:
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한다.
- 검증 기록:
- 구현 후 실행 결과를 여기에 누적한다.
- 실행:
-
Task 6.2: 리소스/컴파일/스타일 검증
- 실행:
./gradlew :app:mergeDebugResources./gradlew :app:compileDebugKotlin./gradlew :app:ktlintCheckgit diff --check
- 기대 결과:
- resource merge, Kotlin compile, ktlint, whitespace 검증이 모두 성공한다.
- 검증 기록:
- 구현 후 실행 결과를 여기에 누적한다.
- 실행:
-
Task 6.3: 수동 화면 검증
- 확인:
- 콘텐츠 탭 진입 후
추천이 기존처럼 표시된다. 랭킹이 기존처럼 표시된다.전체선택 시 오디오 type이 기본 선택된다.- type chip에는
오디오,시리즈,오리지널,무료,포인트만 보인다. - type chip에 Figma의
전체,연재는 보이지 않는다. AUDIO,FREE,POINT는 오디오 카드 3열 grid로 표시된다.SERIES,ORIGINAL은 시리즈 카드 3열 grid로 표시된다.SERIES에서만 요일 필터가 보인다.RANDOM요일은 한국어기타, 일본어その他, 영어OTHER로 표시된다.- sort popup은 기존
ContentSortUI와 동일한 선택 UI로 동작한다. - 하단 스크롤 시 다음 페이지가 append된다.
- 시리즈 성인 콘텐츠는
SeriesContentCardView우측 상단에 성인 배지를 표시한다.
- 콘텐츠 탭 진입 후
- 검증 기록:
- 구현 후 수동 확인 결과를 여기에 누적한다.
- 확인:
Verification Log
- 문서 작성 시점에는 구현 전이므로 실행한 빌드/테스트가 없다.