# PRD: 메인 콘텐츠 탭 내부 전체 탭 ## 1. Overview 메인 콘텐츠 화면의 내부 `전체` 탭에서 콘텐츠 타입별 목록을 `GET /api/v2/audio/contents` API로 조회하고, Figma 디자인에 맞춰 오디오/시리즈 카드를 페이징 목록으로 제공한다. 작성일: 2026-06-25 --- ## 2. Problem - 현재 메인 콘텐츠 화면의 내부 `전체` 탭에 연결할 신규 V2 API 계약과 UI 상태별 데이터 표시 규칙이 문서화되어 있지 않다. - 콘텐츠 타입에 따라 응답 필드가 `audios` 또는 `series`로 나뉘며, 타입별로 카드 UI와 필터 노출 조건이 다르다. - 시리즈 타입에서는 요일 필터가 필요하고, 기본 요일은 디바이스의 현재 요일을 따라야 한다. --- ## 3. Goals - 내부 `전체` 탭 진입 시 기본 콘텐츠 타입 `AUDIO`를 선택하고 첫 페이지를 조회한다. - 콘텐츠 타입 탭 선택 시 `type`, `sort`, `page`, `size`, `dayOfWeek` query parameter 규칙에 맞게 API를 호출한다. - 스크롤 페이징을 적용해 `hasNext = true`인 동안 다음 페이지를 이어서 조회한다. - `SERIES`, `ORIGINAL` 선택 시 응답의 `series` 목록을 표시하고, `AUDIO`, `FREE`, `POINT` 선택 시 응답의 `audios` 목록을 표시한다. - 이번 범위의 `ORIGINAL`은 오리지널 시리즈 목록만 의미한다. - `SERIES` 타입에서만 요일 UI를 노출하고 `dayOfWeek` query parameter를 함께 전송한다. - Figma 디자인의 오디오, 시리즈, 오리지널 상태를 기준으로 기존 V2 위젯 재사용 후보를 우선 검토한다. --- ## 4. Non-Goals - 추천 탭, 랭킹 탭의 기존 기능 변경은 제외한다. - 레거시 화면 또는 레거시 API 파일을 직접 수정하지 않는다. 필요한 레거시 타입은 참조만 한다. - API 계약에 없는 별도 콘텐츠 타입을 새로 만들지 않는다. - 상세 화면, 결제, 보관함, 검색, 정렬 팝업의 신규 기능 확장은 제외한다. - 오리지널 오디오를 `ORIGINAL` 타입에서 별도로 표시하는 동작은 제외한다. - 오프라인 캐시 또는 로컬 DB 저장은 이번 범위에서 제외한다. --- ## 5. Target Users - 메인 콘텐츠 탭에서 최신 오디오 콘텐츠를 탐색하는 사용자 - 시리즈/오리지널/무료/포인트 콘텐츠를 타입별로 빠르게 전환해 탐색하는 사용자 - 특정 요일에 연재되는 시리즈를 확인하려는 사용자 --- ## 6. User Stories - 사용자는 콘텐츠 화면에서 `전체` 탭을 선택하면 기본적으로 오디오 콘텐츠 목록을 보고 싶다. - 사용자는 콘텐츠 타입 탭에서 `시리즈`를 선택하면 시리즈 카드 목록과 요일 필터를 보고 싶다. - 사용자는 요일을 선택하면 해당 요일에 맞는 시리즈 목록으로 갱신되길 원한다. - 사용자는 목록 하단으로 스크롤하면 다음 콘텐츠가 자연스럽게 이어서 로드되길 원한다. - 사용자는 `오리지널`을 선택하면 오리지널 시리즈 목록을 보고 싶다. - 사용자는 `무료`, `포인트`를 선택하면 오디오 카드 UI와 동일한 형태로 콘텐츠를 보고 싶다. --- ## 7. Core Features ### Feature A. 콘텐츠 타입별 API 조회 #### Requirements - API endpoint는 `GET /api/v2/audio/contents`를 사용한다. - query parameter: - `type`: `MainContentAllType` - `sort`: `ContentSort` - `page`: `Int` - `size`: `Int` - `dayOfWeek`: `SeriesPublishedDaysOfWeek` - query parameter 기본값: - `page = 0` - `size = 20` - `sort = LATEST` - `type = AUDIO` - `dayOfWeek = 현재 디바이스에 설정된 요일` - `dayOfWeek`는 `type == SERIES`일 때만 전송한다. - `dayOfWeek` 타입은 기존 `kr.co.vividnext.sodalive.home.SeriesPublishedDaysOfWeek`를 사용한다. - `sort` 타입은 기존 `kr.co.vividnext.sodalive.v2.common.data.ContentSort`를 우선 재사용한다. - 신규 API/Repository/DTO/ViewModel 등 구현이 필요하면 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다. #### Response Data Class ```kotlin data class MainContentAllTabResponse( val type: MainContentAllType, val totalCount: Int, val audios: List, val series: List, val sort: ContentSort, val dayOfWeek: SeriesPublishedDaysOfWeek?, val page: Int, val size: Int, @JsonProperty("hasNext") val hasNext: Boolean ) enum class MainContentAllType { AUDIO, SERIES, ORIGINAL, FREE, POINT } data class MainContentAudioResponse( val audioContentId: Long, val title: String, val imageUrl: String?, val price: Int, @JsonProperty("isAdult") val isAdult: Boolean, @JsonProperty("isPointAvailable") val isPointAvailable: Boolean, @JsonProperty("isFirstContent") val isFirstContent: Boolean, @JsonProperty("isOriginalSeries") val isOriginalSeries: Boolean, val creatorNickname: String ) data class MainContentSeriesResponse( val seriesId: Long, val title: String, val coverImageUrl: String?, val creatorNickname: String, @JsonProperty("isOriginal") val isOriginal: Boolean, @JsonProperty("isAdult") val isAdult: Boolean ) ``` #### Edge Cases - 첫 페이지 응답이 비어 있으면 빈 목록 상태를 표시한다. - 다음 페이지 요청 중 콘텐츠 타입, 정렬, 요일이 변경되면 기존 페이지 요청 상태를 초기화하고 `page = 0`부터 다시 조회한다. - `hasNext = false`이면 추가 페이지를 요청하지 않는다. - API 응답의 `dayOfWeek`가 null이어도 `SERIES` 타입이 아니면 정상으로 처리한다. - `SERIES` 타입에서 `series`가 비어 있고 `audios`가 내려와도 UI에는 `series`만 표시한다. - `AUDIO`, `FREE`, `POINT` 타입에서 `audios`가 비어 있고 `series`가 내려와도 UI에는 `audios`만 표시한다. --- ### Feature B. 콘텐츠 타입 탭 #### Requirements - UI의 콘텐츠 타입 기본값은 `오디오`이며 API 기본 `type = AUDIO`와 일치해야 한다. - 타입 탭은 최소 아래 타입을 제공한다. - 오디오: `AUDIO` - 시리즈: `SERIES` - 오리지널: `ORIGINAL` - 무료: `FREE` - 포인트: `POINT` - Figma에 `전체`, `연재` 칩이 보이더라도 이번 범위에서는 구현하지 않고, 본 PRD에 정의된 API 계약의 `AUDIO`, `SERIES`, `ORIGINAL`, `FREE`, `POINT`만 연동한다. - 타입 변경 시 목록, 페이지 번호, `hasNext`, 로딩 상태를 새 타입 기준으로 초기화한다. - `무료`와 `포인트`는 오디오 선택 UI와 동일한 카드 레이아웃을 사용한다. - `ORIGINAL`은 이번 범위에서 `series` 목록만 의미하며, 시리즈 카드 레이아웃으로 표시한다. #### Edge Cases - 빠르게 탭을 연속 선택해도 마지막으로 선택한 타입의 데이터만 화면에 남아야 한다. - 타입 변경 직후 이전 타입의 페이징 요청이 완료되어도 현재 타입 목록에 섞이면 안 된다. --- ### Feature C. 시리즈 요일 필터 #### Requirements - 요일 UI는 `SERIES` 타입 선택 시에만 노출한다. - 기본 선택 요일은 디바이스의 현재 요일을 `SeriesPublishedDaysOfWeek`로 변환해 사용한다. - 요일 목록은 `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`, `SUN`, `RANDOM`을 제공한다. - 이번 디자인에서 `RANDOM`은 언어별로 아래처럼 표시한다. - 한국어: `기타` - 일본어: `その他` - 영어: `OTHER` - 요일 변경 시 `type = SERIES`, `page = 0`, 선택한 `dayOfWeek`로 목록을 다시 조회한다. #### Edge Cases - 디바이스 날짜/요일을 읽을 수 없는 예외 상황에서는 `RANDOM`을 fallback으로 사용할 수 있다. - `SERIES`가 아닌 타입으로 전환하면 요일 UI를 숨기고 API 요청에서 `dayOfWeek`를 제거한다. --- ### Feature D. 목록 표시와 스크롤 페이징 #### Requirements - 내부 `전체` 탭의 콘텐츠 목록은 스크롤 가능한 그리드로 표시한다. - 첫 페이지 로딩, 추가 페이지 로딩, 빈 목록, 에러 상태를 구분한다. - `hasNext = true`이고 사용자가 목록 하단에 접근하면 다음 `page`를 요청한다. - 추가 페이지 응답은 기존 목록 뒤에 append한다. - `totalCount`는 Figma의 `전체 23` 영역처럼 전체 개수 표시가 필요한 경우 사용한다. - 정렬 기본값은 `ContentSort.LATEST`이며 화면에는 기존 최신순 문자열을 사용한다. - 정렬 옵션은 기존 `ContentSort` 값을 모두 사용한다. - `LATEST` - `POPULAR` - `OWNED` - `PRICE_HIGH` - `PRICE_LOW` - 정렬 UI는 기존 `ContentSort` 사용 화면과 동일한 팝업/선택 UI 패턴을 따른다. - 기존 후보: `CreatorChannelSortPopup`, `ContentSort.toLabelResId()` - 정렬 변경 시 `page = 0`부터 현재 선택된 `type`, `dayOfWeek` 조건으로 다시 조회한다. #### Edge Cases - 추가 페이지 로딩 중 중복 요청을 방지한다. - 추가 페이지 실패 시 기존 목록은 유지하고 재시도 가능한 상태를 제공한다. - 첫 페이지 실패 시 기존 콘텐츠 탭의 토스트/에러 처리 관례를 따른다. --- ### Feature E. 타입별 카드 바인딩 #### Requirements - `AUDIO`, `FREE`, `POINT` 선택 시 `audios` 데이터를 오디오 카드에 바인딩한다. - `audioContentId`: 상세 이동에 사용 - `title`: 제목 - `imageUrl`: 썸네일 - `creatorNickname`: 크리에이터명 - `isAdult`: 성인 배지 표시 - `isPointAvailable`: 포인트 태그 표시 - `isFirstContent`: FIRST 태그 표시 - `isOriginalSeries`: 오리지널 태그 표시 - `price`: 무료 여부 판단이 필요한 경우 사용 - `SERIES`, `ORIGINAL` 선택 시 `series` 데이터를 시리즈 카드에 바인딩한다. - `seriesId`: 상세 이동에 사용 - `title`: 제목 - `coverImageUrl`: 커버 이미지 - `creatorNickname`: 크리에이터명 - `isOriginal`: 오리지널 태그 표시 - `isAdult`: 성인 배지 표시 - `SeriesContentCardView`에 `isAdult` 표시 기능을 추가한다. - 오디오 콘텐츠의 `isAdult` 표시와 동일하게 썸네일 우측 상단에 성인 배지를 표시한다. - 배경은 `bg_creator_channel_live_adult_badge`를 사용한다. - `SeriesContentCardSize.Large`에서는 `ic_new_shield_large`를 사용한다. - `SeriesContentCardSize.Small`에서는 `ic_new_shield_small`을 사용한다. - `isAdult = true`이면 표시하고, `isAdult = false`이면 숨긴다. #### Edge Cases - 이미지 URL이 null이면 기존 이미지 로딩 관례의 placeholder/error 처리를 따른다. - 제목/크리에이터명은 한 줄 ellipsize로 표시해 카드 높이가 흔들리지 않게 한다. --- ## 8. UX / UI Expectations - 기준 디자인: - 오디오 선택 상태: Figma node `35:5857` - 시리즈 선택 상태: Figma node `24:6909` - 오리지널 선택 상태: Figma node `24:9105` - 성인 배지 Large 태그: Figma node `567:18346` - 성인 배지 Small 태그: Figma node `567:18347` - 화면 상단은 기존 콘텐츠 타이틀 바와 내부 텍스트 탭(`추천`, `랭킹`, `전체`) 구조를 유지한다. - 내부 `전체` 탭 선택 시 콘텐츠 타입 칩 영역을 표시한다. - 오디오/무료/포인트는 정사각형 썸네일 카드 3열 그리드 형태를 따른다. - 시리즈/오리지널은 세로형 커버 카드 3열 그리드 형태를 따른다. - 시리즈 선택 상태에서만 요일 필터가 콘텐츠 타입 칩과 정렬 바 사이에 표시된다. - 선택된 칩은 흰색 배경/검정 텍스트, 비선택 칩은 검정 배경/회색 테두리/흰색 텍스트 스타일을 따른다. - 정렬 바는 왼쪽에 `전체 {totalCount}`, 오른쪽에 `최신순`을 표시한다. - 한국어 요일 UI의 `RANDOM` 표기는 `기타`로 표시한다. - 일본어 요일 UI의 `RANDOM` 표기는 `その他`로 표시한다. - 영어 요일 UI의 `RANDOM` 표기는 `OTHER`로 표시한다. - 시리즈 카드 성인 배지는 Figma Large/Small 태그 크기와 기존 오디오 성인 배지 표현을 따른다. ### 재사용 가능한 V2 위젯 후보 - `kr.co.vividnext.sodalive.v2.widget.CapsuleTabBarView` - 콘텐츠 타입 칩 탭에 재사용 가능하다. - `kr.co.vividnext.sodalive.v2.widget.TextTabBarView` - 기존 내부 텍스트 탭(`추천`, `랭킹`, `전체`)에 이미 사용 중이다. - `kr.co.vividnext.sodalive.v2.widget.AudioContentCardView` - `AUDIO`, `FREE`, `POINT` 카드에 재사용 가능하다. - Figma의 3열 그리드에는 `AudioContentCardSize.Small` 후보가 적합하다. - `kr.co.vividnext.sodalive.v2.widget.SeriesContentCardView` - `SERIES`, `ORIGINAL` 카드에 재사용 가능하다. - Figma의 3열 그리드에는 `SeriesContentCardSize.Small` 후보가 적합하다. - `isAdult` 표시를 위해 성인 배지 View와 표시 제어 API를 추가해야 한다. - `kr.co.vividnext.sodalive.v2.main.content.ui.ContentAudioCardAdapter` - 기존 어댑터가 오디오 카드 바인딩 패턴을 제공하므로 신규 전체 탭 어댑터 작성 시 참고하거나 확장 후보로 검토한다. - `kr.co.vividnext.sodalive.v2.main.content.ui.ContentOriginalSeriesAdapter` - 기존 오리지널 시리즈 바인딩 패턴 참고 후보이나, 현재는 전용 레이아웃 기반이므로 전체 탭의 3열 그리드에는 `SeriesContentCardView` 기반 신규 어댑터가 더 적합할 수 있다. - `kr.co.vividnext.sodalive.v2.main.content.ui.addContentGridItemSpacing` - 3열 그리드 간격 재사용 후보로 검토한다. --- ## 9. Technical Constraints - Android Gradle 단일 모듈 `:app`에서 작업한다. - 모든 명령은 저장소 루트에서 실행한다. - 신규 구현은 `kr.co.vividnext.sodalive.v2` 패키지 하위에 둔다. - 레거시 코드는 직접 수정하지 않고, 필요한 경우 기존 enum 또는 화면 이동 API를 참조한다. - DI 추가가 필요하면 기존 `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`의 Koin 패턴을 따른다. - 응답 처리는 기존 `ApiResponse`와 Rx 타입(`Single`, `Flowable`) 패턴을 우선 검토한다. - 공개 API 스키마는 사용자 요청에 명시된 계약을 그대로 따른다. - `BuildConfig` 값이나 민감 정보는 로그/Toast/크래시 메시지에 노출하지 않는다. --- ## 10. Metrics - 내부 `전체` 탭 첫 페이지 API 성공률 - 콘텐츠 타입별 탭 클릭 수 - 시리즈 요일 필터 클릭 수 - 페이징 추가 로드 성공률과 실패율 - 첫 페이지 로딩 시간과 추가 페이지 로딩 시간 - 빈 목록 노출 빈도 --- ## 11. Open Questions - 없음.