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

16 KiB

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 = 현재 디바이스에 설정된 요일
  • dayOfWeektype == 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

data class MainContentAllTabResponse(
    val type: MainContentAllType,
    val totalCount: Int,
    val audios: List<MainContentAudioResponse>,
    val series: List<MainContentSeriesResponse>,
    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: 성인 배지 표시
  • SeriesContentCardViewisAdult 표시 기능을 추가한다.
    • 오디오 콘텐츠의 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열 그리드 형태를 따른다. 전체 탭 adapter는 AudioContentCardSize.Small의 고정 122dp card width에 의존하지 않고 RecyclerView 3열 item width를 계산해 썸네일과 이미지에 1:1 비율로 적용한다.
  • 시리즈/오리지널은 세로형 커버 카드 3열 그리드 형태를 따른다. 전체 탭 adapter는 SeriesContentCardSize.Small의 고정 122dp card width에 의존하지 않고 RecyclerView 3열 item width를 계산해 썸네일과 이미지에 122:172 비율로 적용한다.
  • 시리즈 선택 상태에서만 요일 필터가 콘텐츠 타입 칩과 정렬 바 사이에 표시된다.
  • 선택된 칩은 흰색 배경/검정 텍스트, 비선택 칩은 검정 배경/회색 테두리/흰색 텍스트 스타일을 따른다.
  • 정렬 바는 왼쪽에 전체 {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 typography/tag visual variant를 유지하되, card/thumbnail/label width는 RecyclerView grid item width 기반 동적 API로 적용한다.
  • kr.co.vividnext.sodalive.v2.widget.SeriesContentCardView
    • SERIES, ORIGINAL 카드에 재사용 가능하다.
    • Figma의 3열 그리드에서는 SeriesContentCardSize.Small typography/adult badge visual variant를 유지하되, card/thumbnail/label width는 RecyclerView grid item width 기반 동적 API로 적용한다.
    • 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<T>와 Rx 타입(Single, Flowable) 패턴을 우선 검토한다.
  • 공개 API 스키마는 사용자 요청에 명시된 계약을 그대로 따른다.
  • BuildConfig 값이나 민감 정보는 로그/Toast/크래시 메시지에 노출하지 않는다.

10. Metrics

  • 내부 전체 탭 첫 페이지 API 성공률
  • 콘텐츠 타입별 탭 클릭 수
  • 시리즈 요일 필터 클릭 수
  • 페이징 추가 로드 성공률과 실패율
  • 첫 페이지 로딩 시간과 추가 페이지 로딩 시간
  • 빈 목록 노출 빈도

11. Open Questions

  • 없음.

12. Verification Log

  • 2026-06-25: 사용자 후속 요구사항에 따라 전체 탭 오디오/시리즈 카드가 fixed Small 122dp card width에 의존하지 않고 RecyclerView 3열 가용 item width를 사용해야 함을 문서화했다. 오디오 썸네일은 item width 기준 1:1, 시리즈 썸네일은 item width 기준 122:172 비율을 유지한다.
  • 2026-06-25: 후속 UI 보정 검증으로 focused source/widget tests, mergeDebugResources, compileDebugKotlin, ktlintCheck, git diff --check가 모두 PASS했다. 전체 탭 adapter의 fixed Small 호출 제거와 동적 grid width API 사용, 오디오 1:1 및 시리즈 122:172 썸네일 계약을 테스트로 고정했다.