docs(content): 메인 콘텐츠 추천 탭 문서를 추가한다
This commit is contained in:
464
docs/20260623_메인_콘텐츠_추천_탭/plan-task.md
Normal file
464
docs/20260623_메인_콘텐츠_추천_탭/plan-task.md
Normal file
@@ -0,0 +1,464 @@
|
||||
# 메인 콘텐츠 추천 탭 구현 계획/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.*`이다.
|
||||
- 기존 `ContentMainFragment`는 `kr.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.png`는 `app/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`
|
||||
- `BannerView`와 `BannerItem` 바인딩 및 이미지 로딩을 담당한다.
|
||||
- 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`
|
||||
- 기대 결과: 이동 전/후 참조 지점이 모두 확인된다.
|
||||
|
||||
- [ ] **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` 재사용으로 고정한다.
|
||||
- 일반 오디오 카드는 `AudioContentCardView`와 `AudioContentCardSize.Medium/Large` 재사용으로 고정한다.
|
||||
- `오직 보이스온에서만!`은 label 없는 cover만 필요하므로 기존 `SeriesContentCardView`를 직접 변경하지 않고 신규 `item_content_original_series.xml`로 처리한다.
|
||||
- `New&Hot`, `최근 댓글이 많은 오디오`는 Figma 구조가 공통 카드와 달라 신규 item view로 처리한다.
|
||||
- 검증:
|
||||
- PRD의 재사용 후보와 신규 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개 파일이 모두 출력된다.
|
||||
|
||||
---
|
||||
|
||||
### 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 파일이 컴파일된다.
|
||||
|
||||
- [ ] **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 구현 전 실패한다.
|
||||
|
||||
- [ ] **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 필드에 넣지 않는다.
|
||||
- `isAdult`는 `showAdultBadge` 필드로 변환한다.
|
||||
- `CommentedAudioResponse.latestComment`는 댓글 표시용 model에 넣는다.
|
||||
- `AudioBannerResponse`는 `BannerItem` 또는 `ContentBannerUiModel`로 변환한다.
|
||||
- `OriginalSeriesResponse`는 `ContentOriginalSeriesUiModel(seriesId, coverImageUrl)`로 변환한다.
|
||||
- 검증 명령:
|
||||
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.AudioRecommendationsMapperTest"`
|
||||
- 기대 결과:
|
||||
- mapper 테스트가 PASS한다.
|
||||
|
||||
- [ ] **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`
|
||||
- 작업:
|
||||
- `ContentMainViewModel`에 `recommendationsStateLiveData`, `isLoading`, `toastLiveData`, `loadRecommendations()`를 추가한다.
|
||||
- API success + data 존재 시 `AudioRecommendationsUiState.Content`를 emit한다.
|
||||
- success지만 모든 섹션이 비면 `AudioRecommendationsUiState.Empty`로 처리한다.
|
||||
- error/Throwable은 기존 V2 홈 추천 ViewModel 패턴을 참고해 toast/error 상태로 처리한다.
|
||||
- `AppDI.networkModule`에 `AudioRecommendationsApi`, `repositoryModule`에 `AudioRecommendationsRepository`, `viewModelModule`에 `ContentMainViewModel`을 등록한다.
|
||||
- 검증 명령:
|
||||
- `./gradlew :app:compileDebugKotlin`
|
||||
- 기대 결과:
|
||||
- Koin 등록과 ViewModel 생성이 컴파일된다.
|
||||
|
||||
---
|
||||
|
||||
### 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를 정상 참조한다.
|
||||
|
||||
- [ ] **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.xml`은 `ic_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한다.
|
||||
|
||||
- [ ] **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 우측 icon 순서를 `ic_bar_cash`, `ic_bar_search`, `ic_bar_storage`로 맞춘다.
|
||||
- 콘텐츠 추천 섹션 title string을 추가한다.
|
||||
- 검증 명령:
|
||||
- `./gradlew :app:mergeDebugResources`
|
||||
- `./gradlew :app:compileDebugKotlin`
|
||||
- 기대 결과:
|
||||
- 문자열과 title-bar drawable 참조가 모두 resolve된다.
|
||||
|
||||
---
|
||||
|
||||
### 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이 숨겨진다.
|
||||
|
||||
- [ ] **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`가 비면 `오직 보이스온에서만!` 섹션 전체가 숨겨진다.
|
||||
|
||||
- [ ] **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를 추가한다.
|
||||
- `AudioContentCardView`에 `setAdultVisible(isVisible: Boolean)`을 추가하고 기존 호출부 기본 동작은 변경하지 않는다.
|
||||
- `AudioContentCardView`를 inflate한다.
|
||||
- `latestAudios`, `freeAudios`, `pointAudios`에는 `AudioContentCardSize.Medium`을 사용한다.
|
||||
- `recommendedAudios`에는 `AudioContentCardSize.Large`를 사용한다.
|
||||
- `setContent(title, creatorNickname)`와 `setTags(tags)`를 사용한다.
|
||||
- `showAdultBadge`는 `setAdultVisible(showAdultBadge)`로 표시한다.
|
||||
- `duration`은 표시하지 않는다.
|
||||
- 이미지 로딩은 `thumbnailView().loadUrl(imageUrl)`을 사용한다.
|
||||
- 클릭 시 `AudioContentDetailActivity`로 이동하고 `Constants.EXTRA_AUDIO_CONTENT_ID`를 전달한다.
|
||||
- 검증:
|
||||
- 무료/포인트/첫 콘텐츠/오리지널 태그 조합이 mapper 테스트와 화면에서 일치한다.
|
||||
|
||||
- [ ] **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 상태를 수동 확인한다.
|
||||
|
||||
---
|
||||
|
||||
### 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 수가 의도대로 보인다.
|
||||
|
||||
- [ ] **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은 댓글 영역이 숨겨진다.
|
||||
|
||||
- [ ] **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`가 최신 오디오 뒤, 무료 오디오 앞에 배치되는지 확인한다.
|
||||
- `최근 댓글이 많은 오디오`가 포인트 오디오 뒤, 추천 오디오 앞에 배치되는지 확인한다.
|
||||
|
||||
---
|
||||
|
||||
### 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 테스트로 상태 전환을 확인한다.
|
||||
|
||||
- [ ] **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이 추가되지 않았는지 검증한다.
|
||||
- 오디오 상세 이동이 `AudioContentDetailActivity`와 `Constants.EXTRA_AUDIO_CONTENT_ID`를 사용하는지 검증한다.
|
||||
- 시리즈 상세 이동이 `SeriesDetailActivity`와 `Constants.EXTRA_SERIES_ID`를 사용하는지 검증한다.
|
||||
- 검증 명령:
|
||||
- `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"`
|
||||
- 기대 결과:
|
||||
- 구현 전 실패하고, Phase 6 완료 후 PASS한다.
|
||||
|
||||
- [ ] **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가 의도대로 들어가는지 확인한다.
|
||||
|
||||
---
|
||||
|
||||
### 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한다.
|
||||
- 검증 기록:
|
||||
- 구현 후 실행 명령, 결과, 실패 시 수정 내용을 이 Task 아래에 누적 기록한다.
|
||||
|
||||
- [ ] **Task 7.2: 수동 UI 검증**
|
||||
- 확인:
|
||||
- title-bar와 tab-bar가 스크롤 중 고정된다.
|
||||
- 스크롤 영역은 배너부터 시작한다.
|
||||
- 섹션 순서는 배너 → 오직 보이스온에서만! → 새로 올라온 오디오 → New&Hot → 무료 오디오 → 포인트 오디오 → 최근 댓글이 많은 오디오 → 추천 오디오다.
|
||||
- `추천 시리즈`, `키워드의 오디오`는 표시되지 않는다.
|
||||
- 각 section의 empty 상태에서 빈 title/list 공간이 남지 않는다.
|
||||
- `duration`은 어떤 오디오 카드에도 표시되지 않는다.
|
||||
- 태그는 `price`, `isPointAvailable`, `isFirstContent`, `isOriginalSeries` 정책과 일치한다.
|
||||
- 성인 badge는 `isAdult` 정책과 일치한다.
|
||||
- 배너, 오디오, 시리즈 item 클릭 routing이 의도대로 동작한다.
|
||||
- 검증 기록:
|
||||
- 기기/에뮬레이터, 화면 크기, 확인 결과를 이 Task 아래에 기록한다.
|
||||
|
||||
---
|
||||
|
||||
## Verification Log
|
||||
- 구현 중 여러 Phase에 걸친 통합 검증, 회귀 검증, 최종 수동 확인 기록을 여기에 누적한다.
|
||||
324
docs/20260623_메인_콘텐츠_추천_탭/prd.md
Normal file
324
docs/20260623_메인_콘텐츠_추천_탭/prd.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# 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<AudioBannerResponse>,
|
||||
val originalSeries: List<OriginalSeriesResponse>,
|
||||
val latestAudios: List<AudioCardResponse>,
|
||||
val newAndHotAudios: List<AudioCardResponse>,
|
||||
val freeAudios: List<AudioCardResponse>,
|
||||
val pointAudios: List<AudioCardResponse>,
|
||||
val mostCommentedAudios: List<CommentedAudioResponse>,
|
||||
val recommendedAudios: List<AudioCardResponse>
|
||||
)
|
||||
|
||||
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<T>`와 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을 확인한 뒤 결정한다.
|
||||
Reference in New Issue
Block a user