# PRD: 메인 콘텐츠 추천 탭 ## 1. Overview Figma `cont_001` 화면(`24:6737`)을 기준으로 메인 콘텐츠 탭의 내부 `추천` 탭을 구성하고, 오디오 추천 API 응답을 V2 패키지 하위 화면과 위젯 중심으로 바인딩한다. --- ## 2. Problem - `ContentMainFragment`는 현재 빈 화면 수준이므로 메인 콘텐츠 탭 내부의 추천 화면 구조와 데이터 계약을 새로 정의해야 한다. - 화면 상단의 title-bar와 tab-bar는 고정되어야 하고, 배너부터 하위 추천 콘텐츠만 세로 스크롤되어야 한다. - 추천 화면은 배너, 오리지널 시리즈, 최신/인기/무료/포인트/댓글/추천 오디오 등 여러 섹션을 단일 API 응답으로 표시해야 한다. - Figma에는 가로 카드, 리스트, 배너, 댓글 카드, 2열 그리드가 혼재되어 있어 화면에 보이는 섹션별로 Phase를 분리해 구현 계획을 세워야 한다. - V2 패키지 하위에 이미 구현된 배너, 오디오 카드, 시리즈 카드, 탭 위젯이 있으므로 재사용 가능한 후보를 먼저 식별하고 중복 UI 생성을 줄여야 한다. --- ## 3. Goals - API endpoint `GET /api/v2/audio/recommendations` 응답을 받아 메인 콘텐츠 추천 탭에 표시한다. - Figma `24:6737`의 고정/스크롤 구조를 따른다. - title-bar와 tab-bar는 화면 상단에 고정하고, 배너부터 `추천 오디오`까지의 콘텐츠 영역만 스크롤한다. - 화면에 보이는 주요 섹션별로 구현 Phase를 분리할 수 있도록 요구사항을 정리한다. - 이번 범위에서 `추천 시리즈`, `키워드의 오디오` 섹션은 구현하지 않는다. - V2 패키지 하위 기존 위젯 중 재사용 가능한 후보를 문서에 기록한다. - API DTO, 화면 상태, empty/error/loading, click routing은 구현 계획에서 검증 가능하도록 정리한다. --- ## 4. Non-Goals - 이번 PRD 작성 단계에서는 코드, 리소스, 레이아웃 파일을 구현하지 않는다. - `추천 시리즈` 섹션은 이번 범위에서 구현하지 않는다. - `키워드의 오디오` 섹션은 이번 범위에서 구현하지 않는다. - 추천 알고리즘, API 응답 정렬 기준, 서버 필드명은 변경하지 않는다. - 외부 라이브러리를 추가하지 않는다. - Compose 화면으로 전환하지 않는다. - 기존 레거시 오디오 메인 화면을 직접 수정하지 않는다. - Figma asset localhost URL을 앱 코드에 직접 의존하지 않는다. - Figma에 없는 skeleton loading, shimmer, pull-to-refresh, 임의 애니메이션, 추가 badge는 만들지 않는다. --- ## 5. Target Users - 메인 콘텐츠 탭에서 추천 오디오와 오리지널 시리즈를 탐색하는 앱 사용자. - 무료/포인트/오리지널/첫 콘텐츠 여부를 카드에서 빠르게 구분하려는 앱 사용자. - 댓글이 많은 오디오와 추천 오디오 목록을 통해 오디오 콘텐츠 상세로 이동하려는 앱 사용자. - `kr.co.vividnext.sodalive.v2` 하위 메인 콘텐츠 화면을 구현/유지보수하는 Android 개발자. --- ## 6. User Stories - 사용자는 콘텐츠 탭에서 `추천` 내부 탭을 열자마자 배너와 추천 오디오 섹션을 순서대로 보고 싶다. - 사용자는 상단 title-bar와 tab-bar가 스크롤 중에도 유지되어 현재 위치와 탭 맥락을 잃지 않길 기대한다. - 사용자는 배너를 터치해 이벤트, 크리에이터, 시리즈 또는 링크 목적지로 이동하고 싶다. - 사용자는 오디오 카드에서 제목, 크리에이터, 썸네일, 무료/포인트/오리지널/첫 콘텐츠/성인 태그를 구분하고 싶다. - 사용자는 댓글이 많은 오디오에서 최근 댓글 작성자의 프로필 이미지를 함께 보고 콘텐츠에 관심을 가질 수 있어야 한다. - 개발자는 기존 V2 위젯을 최대한 재사용해 콘텐츠 추천 탭과 다른 V2 화면의 카드 스타일 차이를 줄이고 싶다. --- ## 7. Core Features ### 메인 콘텐츠 추천 API 메인 콘텐츠 추천 탭은 다음 응답을 단일 API 결과로 받아 섹션별 UI 모델로 변환한다. #### API Contract ```kotlin GET /api/v2/audio/recommendations ``` #### Response Contract ```kotlin data class AudioRecommendationsResponse( val banners: List, val originalSeries: List, val latestAudios: List, val newAndHotAudios: List, val freeAudios: List, val pointAudios: List, val mostCommentedAudios: List, val recommendedAudios: List ) data class AudioBannerResponse( val imageUrl: String, val eventItem: EventItem?, val creatorId: Long?, val seriesId: Long?, val link: String? ) data class OriginalSeriesResponse( val seriesId: Long, val coverImageUrl: String? ) data class AudioCardResponse( val audioContentId: Long, val title: String, val duration: String?, val imageUrl: String?, val price: Int, val isAdult: Boolean, val isPointAvailable: Boolean, val isFirstContent: Boolean, val isOriginalSeries: Boolean, val creatorNickname: String ) data class CommentedAudioResponse( val audioContentId: Long, val title: String, val imageUrl: String?, val latestComment: String, val latestCommentWriterProfileImageUrl: String ) ``` #### Requirements - 신규 `ContentMainFragment`, API, Repository, ViewModel, adapter/helper 등 메인 콘텐츠 추천 탭 연결 하위 코드는 `kr.co.vividnext.sodalive.v2.main.content.*` 패키지를 기본 패키지로 작성한다. - 기존 레거시 API 또는 화면 파일을 직접 수정하지 않는다. - 응답 DTO는 서버 필드명을 임의 변경하지 않고, Jackson `@JsonProperty`는 사용하지 않는다. - JSON 매핑 annotation이 필요한 경우 Gson 기반 기존 프로젝트 관례에 맞춰 `@SerializedName`을 사용한다. - 화면 UI는 DTO를 직접 노출하지 않고 섹션별 UI model 또는 adapter item으로 변환한다. - `AudioCardResponse.duration`은 DTO에 유지하지만, Figma 카드에 표시 영역이 없으므로 이번 화면 UI에는 표시하지 않는다. - 각 리스트가 비어 있으면 해당 섹션은 숨기는 것을 기본 정책으로 한다. - 전체 API 실패 시 기존 V2 홈 추천 화면의 loading/error/toast 패턴을 우선 참고한다. - 이미지 URL이 null이면 기존 이미지 로딩 placeholder 또는 숨김 정책을 구현 계획에서 확인해 따른다. #### Edge Cases - `banners`가 비어 있으면 배너 영역을 숨긴다. - `imageUrl` 또는 `coverImageUrl`이 null/blank이면 해당 위젯의 기존 placeholder 정책을 따른다. - `duration == null`이어도 이번 화면 UI에는 표시하지 않으므로 카드 표시는 유지한다. - `audioContentId <= 0` 또는 `seriesId <= 0`처럼 목적지 식별자가 유효하지 않은 item은 터치 이동을 무시한다. ### 고정 Title Bar와 내부 Tab Bar Figma `24:6738`, `24:6739`는 화면 상단 고정 영역이다. #### Requirements - title-bar와 tab-bar는 세로 스크롤 대상에 포함하지 않는다. - 배너 시작 지점부터 하위 콘텐츠만 세로 스크롤한다. - 내부 tab-bar의 `추천` 탭이 선택된 상태의 화면을 이 PRD 범위로 한다. - 다른 내부 탭의 실제 화면 구현은 이번 범위에 포함하지 않는다. - title-bar 우측 아이콘은 왼쪽부터 `ic_bar_cash`, `ic_bar_search`, `ic_bar_storage` 순서로 배치한다. - title-bar 우측 아이콘 asset은 `app/src/main/res/drawable-mdpi/`에 위치한 drawable을 사용한다. - title-bar/tab-bar에 사용할 기존 위젯 또는 include layout은 구현 계획에서 현재 메인 V2 구조를 확인해 결정한다. ### Phase 1: 배너 섹션 Figma `24:6741` 기준으로 최상단 배너 carousel을 표시한다. #### Requirements - `banners`를 가로 스와이프 가능한 배너로 표시한다. - 배너 item은 `imageUrl`, `eventItem`, `creatorId`, `seriesId`, `link`를 유지한다. - 배너 터치 시 기존 홈 배너 routing 정책을 우선 참고해 이벤트, 크리에이터, 시리즈, 링크 이동을 처리한다. - 기존 `v2.widget.banner.BannerView`와 `BannerItem` 재사용을 우선 검토한다. #### Edge Cases - 배너가 1개이면 무한 carousel 또는 auto-scroll 여부는 기존 `BannerView` 정책을 따른다. - 이동 목적지가 없는 배너는 터치해도 아무 동작하지 않는다. ### Phase 2: 오직 보이스온에서만! Figma `24:6745` 기준으로 오리지널 시리즈 커버를 가로 목록으로 표시한다. #### Requirements - 섹션 타이틀은 `오직 보이스온에서만!`이다. - `originalSeries`를 가로 스크롤 시리즈 카드 목록으로 표시한다. - 각 item은 `seriesId`, `coverImageUrl`을 사용한다. - Figma에서는 label 없이 커버만 표시되므로 제목/크리에이터명은 노출하지 않는다. - 기존 `SeriesContentCardView` 또는 시리즈 썸네일 위젯을 재사용할 수 있는지 구현 계획에서 검증한다. - item 터치 시 시리즈 상세 화면으로 이동한다. #### Edge Cases - `coverImageUrl == null`이면 기존 이미지 placeholder 정책을 따른다. - `seriesId <= 0`이면 item 터치 이동을 무시한다. ### Phase 3: 새로 올라온 오디오 Figma `24:6751` 기준으로 최신 오디오를 가로 카드 목록으로 표시한다. #### Requirements - 섹션 타이틀은 `새로 올라온 오디오`이다. - `latestAudios`를 `AudioContentCardSize.Medium`에 가까운 가로 카드 목록으로 표시한다. - 카드에는 썸네일, 제목, 크리에이터 닉네임을 표시한다. - `isOriginalSeries`, `isFirstContent`, `isPointAvailable`, `price`, `isAdult`는 기존 `AudioContentTag` 기반 태그로 매핑한다. - item 터치 시 오디오 콘텐츠 상세로 이동한다. #### Edge Cases - `title` 또는 `creatorNickname`이 긴 경우 Figma처럼 한 줄 말줄임 처리한다. - `price == 0`이면 무료 태그 표시 대상으로 본다. ### Phase 4: New&Hot Figma `24:6758` 기준으로 New&Hot 오디오를 가로 paging 가능한 리스트 묶음으로 표시한다. #### Requirements - 섹션 타이틀은 `New&Hot`이다. - `newAndHotAudios`를 88dp 썸네일 기반 세로 리스트 3개 단위의 가로 묶음으로 표시한다. - 각 리스트 item에는 썸네일, 제목, 크리에이터 닉네임, 태그를 표시한다. - 기존 `AudioContentCardView`만으로 대응이 어렵다면 최소 범위의 리스트 item view 또는 adapter item을 만든다. - item 터치 시 오디오 콘텐츠 상세로 이동한다. #### Edge Cases - item 수가 3개 미만이면 남은 빈 row를 만들지 않는다. - 마지막 묶음이 3개 미만이어도 기존 item 간격을 유지한다. ### Phase 5: 무료 오디오 Figma `24:6807` 기준으로 무료 오디오 가로 카드 목록을 표시한다. #### Requirements - 섹션 타이틀은 `무료 오디오`이다. - `freeAudios`를 가로 카드 목록으로 표시한다. - 무료 태그는 반드시 표시한다. - 오리지널/첫 콘텐츠/성인/포인트 태그는 응답 값에 따라 함께 표시한다. - item 터치 시 오디오 콘텐츠 상세로 이동한다. ### Phase 6: 포인트 오디오 Figma `24:6813` 기준으로 포인트 오디오 가로 카드 목록을 표시한다. #### Requirements - 섹션 타이틀은 `포인트 오디오`이다. - `pointAudios`를 가로 카드 목록으로 표시한다. - 포인트 태그는 반드시 표시한다. - 무료/오리지널/첫 콘텐츠/성인 태그는 응답 값에 따라 함께 표시한다. - item 터치 시 오디오 콘텐츠 상세로 이동한다. ### Phase 7: 최근 댓글이 많은 오디오 Figma `24:6820` 기준으로 댓글이 많은 오디오를 가로 카드 묶음으로 표시한다. #### Requirements - 섹션 타이틀은 `최근 댓글이 많은 오디오`이다. - `mostCommentedAudios`를 가로 스크롤 가능한 카드 묶음으로 표시한다. - 각 카드에는 오디오 리스트 item, 최근 댓글 본문, 최근 댓글 작성자 프로필 이미지 영역을 함께 표시한다. - 댓글 영역에는 `latestComment`를 표시한다. - item 터치 시 오디오 콘텐츠 상세로 이동한다. #### Edge Cases - `latestCommentWriterProfileImageUrl`이 blank이면 프로필 이미지 placeholder 정책을 따른다. - `latestComment`가 blank이면 댓글 영역을 숨긴다. ### Phase 8: 추천 오디오 Figma `24:6842` 기준으로 추천 오디오를 2열 그리드로 표시한다. #### Requirements - 섹션 타이틀은 `추천 오디오`이다. - `recommendedAudios`를 2열 그리드로 표시한다. - 카드에는 썸네일, 제목, 크리에이터 닉네임, 태그를 표시한다. - 기존 `AudioContentCardView`의 `AudioContentCardSize.Large` 재사용을 우선 검토한다. - item 터치 시 오디오 콘텐츠 상세로 이동한다. #### Edge Cases - 홀수 개수이면 마지막 item은 좌측 정렬로 표시한다. - 긴 제목/크리에이터명은 한 줄 말줄임 처리한다. ### 제외 섹션 Figma에는 존재하지만 이번 범위에서는 구현하지 않는다. #### Non-Implemented Sections - `추천 시리즈`: Figma `24:6770` - `키워드의 오디오`: Figma `24:6829` #### Requirements - API 응답에 대응 필드가 없거나 이번 범위에서 명시 제외된 섹션은 화면에 placeholder로도 만들지 않는다. - 구현 계획 문서에서는 이 두 섹션을 Non-Goals로 유지한다. ### 재사용 가능한 V2 위젯 후보 구현 전 다음 V2 위젯과 기존 화면 패턴을 우선 검토한다. #### Widget Candidates - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerView.kt`: 배너 carousel, counter, auto-scroll. - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/banner/BannerItem.kt`: 배너 item 계약 후보. - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardView.kt`: 오디오 카드 썸네일/라벨/태그 표시. - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentCardSize.kt`: `Large`, `Medium`, `Small` 카드 크기. - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/AudioContentTag.kt`: `Original`, `Point`, `First`, `Free` 태그 매핑. - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardView.kt`: 시리즈 커버 카드 후보. - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/SeriesContentCardSize.kt`: 시리즈 카드 크기. - `app/src/main/java/kr/co/vividnext/sodalive/v2/widget/TextTabBarView.kt`: 텍스트 탭바 후보. - `app/src/main/res/layout/view_section_title.xml`: 섹션 타이틀 include 후보. - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt`: V2 홈 추천의 loading/error/empty, 배너 routing, section visibility 참고. - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/ui/HomeBannerBinder.kt`: 배너 바인딩 및 이미지 로딩 참고. #### Reuse Constraints - 기존 위젯을 재사용하되, 현재 동작을 깨는 방식으로 기존 위젯을 확장하지 않는다. - 재사용 후보가 Figma 요구사항과 맞지 않으면 신규 wrapper/adapter/item view를 V2 하위에 최소 범위로 추가한다. - 레거시 파일 수정이 필요해 보이면 구현 전에 사용자 확인을 받는다. --- ## 8. UX / UI Expectations - 전체 배경은 Figma처럼 black 계열을 유지한다. - title-bar와 tab-bar는 고정되고, 콘텐츠 영역만 세로 스크롤된다. - 섹션은 Figma 순서대로 배치한다: 배너 → 오직 보이스온에서만! → 새로 올라온 오디오 → New&Hot → 무료 오디오 → 포인트 오디오 → 최근 댓글이 많은 오디오 → 추천 오디오. - 각 섹션은 데이터가 없으면 숨겨 스크롤 중 빈 영역이 생기지 않게 한다. - 가로 스크롤 섹션은 Figma 기준 좌측 inset과 item spacing을 구현 계획에서 수치화한다. - 카드 썸네일은 centerCrop과 radius 14dp를 기본으로 한다. - 섹션 타이틀, 카드 타이틀, 크리에이터명은 기존 typography resource를 우선 사용한다. - 성인 콘텐츠는 `isAdult` 값에 따라 기존 성인 태그 또는 이에 준하는 표시를 한다. - 접근성을 위해 주요 카드와 배너는 터치 가능한 item임을 유지하고, 장식용 태그 아이콘은 기존 위젯 정책처럼 불필요한 접근성 읽기를 피한다. --- ## 9. Technical Constraints - Android Gradle 단일 `:app` 모듈 내에서 작업한다. - 메인 콘텐츠 추천 탭의 기본 패키지는 `kr.co.vividnext.sodalive.v2.main.content.*`로 한다. - 기존 `ContentMainFragment`도 `kr.co.vividnext.sodalive.v2.main.content` 패키지 안쪽에 배치한다. - 신규 `Activity`, `Fragment`, `ViewModel`, API, Repository, adapter/helper 코드는 기본 패키지 하위에 작성한다. - 레이어 흐름은 기존 관례인 `Api -> Repository -> ViewModel -> Fragment`를 따른다. - DI는 `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt`의 Koin 구성을 따른다. - 네트워크 응답은 기존 `ApiResponse`와 Rx `Single` 사용 패턴을 우선 따른다. - 기존 레거시 오디오 화면 파일은 직접 수정하지 않는다. - 공개 API 스키마를 임의 변경하지 않는다. - 문구는 string resource 기반으로 관리한다. - 구현 계획 작성 시 화면에 보이는 섹션별 Phase를 분리하고 각 Phase별 검증 기준을 둔다. --- ## 10. Metrics - API 성공 시 각 응답 리스트가 대응 섹션에 표시되는지 확인한다. - 비어 있는 리스트의 섹션이 화면에서 숨겨지는지 확인한다. - title-bar와 tab-bar가 스크롤 중 고정되는지 확인한다. - 배너, 오디오 카드, 시리즈 카드 터치 시 유효한 목적지로 이동하는지 확인한다. - 태그 표시가 `price`, `isPointAvailable`, `isFirstContent`, `isOriginalSeries`, `isAdult` 값과 일치하는지 확인한다. - Figma 기준 섹션 순서와 제외 섹션 미노출 여부를 수동 확인한다. --- ## 11. Open Questions - title-bar와 내부 tab-bar에 사용할 기존 layout/include가 확정되어 있는지 구현 계획 단계에서 확인이 필요하다. - 배너 `link`가 외부 URL, 앱 딥링크, 웹뷰 중 어떤 방식으로 처리되어야 하는지 기존 홈 배너 routing을 확인한 뒤 결정한다.