From 8da39949e5eb944c7203d037ff104ac608884fb4 Mon Sep 17 00:00:00 2001 From: klaus Date: Sat, 27 Jun 2026 05:29:35 +0900 Subject: [PATCH] =?UTF-8?q?docs(content):=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=EB=B3=B4=EA=B8=B0=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=95=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/20260627_콘텐츠_전체보기/plan-task.md | 511 +++++++++++++++++++++ docs/20260627_콘텐츠_전체보기/prd.md | 248 ++++++++++ 2 files changed, 759 insertions(+) create mode 100644 docs/20260627_콘텐츠_전체보기/plan-task.md create mode 100644 docs/20260627_콘텐츠_전체보기/prd.md diff --git a/docs/20260627_콘텐츠_전체보기/plan-task.md b/docs/20260627_콘텐츠_전체보기/plan-task.md new file mode 100644 index 00000000..e3423fdc --- /dev/null +++ b/docs/20260627_콘텐츠_전체보기/plan-task.md @@ -0,0 +1,511 @@ +# 콘텐츠 전체보기 구현 계획/TASK + +> **For agentic workers:** REQUIRED SUB-SKILL: 구현 시 `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`를 사용해 task 단위로 진행한다. 각 단계는 체크박스(`- [ ]`)로 추적하고, 완료 즉시 `- [x]`로 갱신한다. 구현 범위 변경이 생기면 이 문서를 먼저 수정한 뒤 코드에 반영한다. + +**Goal:** 홈/콘텐츠 추천 섹션의 전체보기 chevron을 연결하고, 기존 콘텐츠 `전체` 탭 이동을 먼저 적용한 뒤 `New&Hot` 및 `처음부터 함께 성장!` 전용 신규 전체보기 화면을 제공한다. + +**Architecture:** 구현 순서는 `chevron 표시 -> 기존 콘텐츠 전체 탭 이동 -> 신규 전체보기 페이지/API`로 고정한다. 기존 화면 이동은 `MainV2Activity`, `ContentMainFragment`, `ContentAllTabViewModel`에 작은 public navigation contract를 추가해 처리한다. 신규 전체보기 화면은 마지막 단계에서 `kr.co.vividnext.sodalive.v2.main.content.overview` 하위 Activity/ViewModel/API/Repository/DTO/UI model/adapter로 격리한다. + +**Tech Stack:** Kotlin, Android XML Views, ViewBinding, RecyclerView, Retrofit, Gson, RxJava3, Koin, JUnit4/Robolectric local unit test. + +--- + +## 전제와 성공 기준 +- PRD: `docs/20260627_콘텐츠_전체보기/prd.md` +- 구현 순서: + - 1순위: 홈/콘텐츠 추천 섹션 chevron 표시. + - 2순위: 신규 페이지가 필요 없는 섹션을 기존 콘텐츠 `전체` 탭으로 이동. + - 3순위: 신규 콘텐츠 전체보기 페이지/API 생성 및 신규 페이지 대상 섹션 이동. +- 전체보기 chevron 표시 섹션: + - 홈 추천 탭: `처음부터 함께 성장!` + - 콘텐츠 추천 탭: `오직 보이스온에서만!`, `새로 올라온 오디오`, `New&Hot`, `무료 오디오`, `포인트 오디오` +- 기존 콘텐츠 `전체` 탭 이동 섹션: + - `오직 보이스온에서만!` -> 콘텐츠 탭 `전체` -> `오리지널` 카테고리 + - `새로 올라온 오디오` -> 콘텐츠 탭 `전체` -> `오디오` 카테고리 + - `무료 오디오` -> 콘텐츠 탭 `전체` -> `무료` 카테고리 -> `인기순` + - `포인트 오디오` -> 콘텐츠 탭 `전체` -> `포인트` 카테고리 -> `인기순` +- 신규 전체보기 페이지 이동 섹션: + - 홈 추천 탭 `처음부터 함께 성장!` -> 신규 콘텐츠 전체보기 -> `FIRST_AUDIO_CONTENT` + - 콘텐츠 추천 탭 `New&Hot` -> 신규 콘텐츠 전체보기 -> `NEW_AND_HOT_AUDIO` +- `댓글 많은 오디오`, `추천 오디오`에는 chevron을 표시하지 않는다. +- 신규 API endpoint는 `GET /api/v2/contents`이다. +- 신규 API query parameter key는 소문자 `page`, `size`, `type`만 사용한다. +- 신규 API 기본값은 `page=0`, `size=20`, `type=NEW_AND_HOT_AUDIO`다. +- 앱 구현 DTO annotation은 Gson `@SerializedName`을 사용한다. +- 신규 전체보기 화면 title은 `NEW_AND_HOT_AUDIO`일 때 `New&Hot`, `FIRST_AUDIO_CONTENT`일 때 `처음부터 함께 성장!`이다. +- 신규 전체보기 화면은 Figma node `482:15105` 기준 2열 오디오 카드 grid를 사용한다. +- 구현 완료 후 최소 다음 명령을 실행한다. + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.*"` + - `./gradlew :app:mergeDebugResources` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + - `git diff --check` + +--- + +## Phase 순서 +- Phase 1: 기존 구조 확인과 작업 경계 고정 +- Phase 2: 홈/콘텐츠 추천 섹션 chevron 표시 +- Phase 3: 기존 콘텐츠 `전체` 탭 이동 contract 추가 +- Phase 4: 기존 콘텐츠 `전체` 탭 이동 섹션 연결 +- Phase 5: 신규 전체보기 API, DTO, Repository, UI model 추가 +- Phase 6: 신규 전체보기 ViewModel, Activity, Adapter 구현 +- Phase 7: 신규 전체보기 대상 섹션 이동 연결 +- Phase 8: 통합 검증과 수동 확인 + +--- + +## Figma 참조 필요 Phase +- Phase 1: 제한 참조 + - 기존 홈/콘텐츠 추천 섹션 title, 콘텐츠 전체 탭, MainV2 navigation 구조 확인 중심으로 진행한다. +- Phase 2: 제한 참조 + - chevron 표시는 기존 `view_section_title.xml`과 홈/콘텐츠 추천 섹션 구조를 따른다. +- Phase 3~4: Figma 참조 불필요 + - 기존 콘텐츠 `전체` 탭 이동은 기존 `ContentAllTabViewModel`의 type/sort 계약과 MainV2 tab 전환 구조를 따른다. +- Phase 5: Figma 참조 불필요 + - API/DTO/Repository/UI model/mapper/ViewModel은 PRD 서버 계약과 기존 V2 data layer/paging 패턴을 따른다. +- Phase 6: 필수 참조 + - 신규 전체보기 Activity layout은 Figma `482:15105`의 title bar, black background, 2열 grid, card spacing을 기준으로 확인한다. +- Phase 7~8: 필수 참조 + - 신규 페이지 대상 섹션 이동과 최종 수동 화면 검증은 PRD의 Figma reference와 실제 화면을 대조한다. + +--- + +## 파일 구조 + +### 기존 화면 이동 선행 범위 +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - `처음부터 함께 성장!` title chevron을 먼저 표시한다. 신규 페이지 이동은 Phase 7에서 연결한다. +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` + - 콘텐츠 추천 섹션 chevron 표시, 기존 콘텐츠 `전체` 탭 이동, 신규 페이지 이동을 단계별로 추가한다. +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModel.kt` + - 외부 진입용 type/sort 선택 API를 추가한다. +- Modify: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` + - 콘텐츠 탭 내부 `전체`를 특정 type/sort로 여는 public method와 기존 fragment 재사용 처리를 추가한다. +- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` + - 콘텐츠 추천 섹션 chevron/routing/source를 검증한다. +- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModelTest.kt` + - 외부 type/sort 선택 API를 검증한다. +- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentSourceTest.kt` + - 홈 `처음부터 함께 성장!` chevron/source를 검증한다. +- Modify: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt` + - 콘텐츠 탭 내부 `전체` type/sort routing contract를 검증한다. + +### 신규 전체보기 후행 범위 +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewApi.kt` + - `GET /api/v2/contents` Retrofit endpoint를 정의한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewModels.kt` + - `ContentOverviewPageResponse`, `ContentOverviewType`, `ContentOverviewItemResponse` DTO를 정의한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewRepository.kt` + - API 호출을 repository method로 감싼다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiState.kt` + - `Loading`, `Content`, `Empty`, `Error` 상태와 paging/loading-more 상태를 정의한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiModels.kt` + - 신규 전체보기 화면 item/title/type UI model을 정의한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewMappers.kt` + - DTO를 UI model로 변환하고 `AudioContentTag`를 매핑한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewViewModel.kt` + - type/page/hasNext/loading-more/API 상태를 관리한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt` + - title bar, 2열 RecyclerView, paging, empty/error/loading, 상세 routing을 연결한다. +- Create: `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt` + - `AudioContentCardView` 기반 2열 grid item을 바인딩한다. +- Create: `app/src/main/res/layout/activity_content_overview.xml` + - 신규 전체보기 화면 layout이다. +- Modify: `app/src/main/AndroidManifest.xml` + - `ContentOverviewActivity`를 등록한다. +- 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/overview/ContentOverviewMapperTest.kt` + - DTO -> UI model/tag/title mapping을 검증한다. +- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewViewModelTest.kt` + - 첫 페이지, load-more, 실패, stale response 방지를 검증한다. +- Create: `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivitySourceTest.kt` + - layout id, 2열 grid, intent extra, 상세 이동 source를 검증한다. + +--- + +### Phase 1: 기존 구조 확인과 작업 경계 고정 + +- [ ] **Task 1.1: PRD와 기존 구현 상태 대조** + - 확인: + - `docs/20260627_콘텐츠_전체보기/prd.md` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` + - `app/src/main/res/layout/view_section_title.xml` + - 작업: + - `HomeMainFragment`의 `ViewSectionTitleBinding.setTitle(titleResId, showMore)`가 chevron 표시를 이미 지원하는지 확인한다. + - `ContentMainFragment`의 `ViewSectionTitleBinding.setTitle(titleResId)`가 현재 chevron을 항상 숨기는지 확인한다. + - 이번 작업에서 레거시 `audio_content/*` 파일을 직접 수정하지 않는 것을 확인한다. + - 검증: + - Run: `rg -n "setTitle\\(|ivSectionTitleChevron|viewHomeFirstAudioTitle|viewContentNewAndHotTitle|viewContentOriginalSeriesTitle" app/src/main/java/kr/co/vividnext/sodalive/v2/main app/src/main/res/layout/view_section_title.xml` + - Expected: 홈 title helper와 콘텐츠 title helper, chevron view id가 확인된다. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 1.2: 기존 콘텐츠 전체 탭 이동 가능 지점 확인** + - 확인: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModel.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/data/MainContentAllTabModels.kt` + - 작업: + - `MainV2Activity`의 bottom tab 전환과 fragment 재사용 위치를 확인한다. + - `ContentMainFragment`의 `showAllContent()`와 `ContentAllTabViewModel` type/sort 변경 위치를 확인한다. + - `MainContentAllType.ORIGINAL`, `AUDIO`, `FREE`, `POINT`와 `ContentSort.POPULAR`, `LATEST`가 기존 목적지 요구를 충족하는지 확인한다. + - 검증: + - Run: `rg -n "MainContentAllType|ContentSort|showAllContent|changeType|changeSort|clickTab\\(MainV2Tab.CONTENT\\)" app/src/main/java/kr/co/vividnext/sodalive/v2/main` + - Expected: type/sort 선택과 bottom tab 전환 지점이 확인된다. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +--- + +### Phase 2: 홈/콘텐츠 추천 섹션 chevron 먼저 표시 + +- [ ] **Task 2.1: 홈 추천 탭 FIRST 섹션 chevron 표시** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - 테스트: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentSourceTest.kt` + - 작업: + - `viewHomeFirstAudioTitle.setTitle(..., showMore = true)` 상태를 유지하거나 누락되어 있으면 추가한다. + - 이 Task에서는 신규 페이지 이동 click listener를 연결하지 않는다. + - source test는 `viewHomeFirstAudioTitle.setTitle` 호출이 `showMore = true`임을 검증한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest"` + - Expected: 홈 FIRST chevron 표시 source 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 2.2: 콘텐츠 추천 탭 section title helper 확장** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` + - 테스트: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` + - 작업: + - 콘텐츠 탭의 `ViewSectionTitleBinding.setTitle(titleResId)`를 `setTitle(titleResId, showMore = false)`로 확장한다. + - `오직 보이스온에서만!`, `새로 올라온 오디오`, `New&Hot`, `무료 오디오`, `포인트 오디오`는 `showMore = true`로 설정한다. + - `댓글 많은 오디오`, `추천 오디오`는 `showMore = false`를 유지한다. + - 이 Task에서는 기존 전체 탭 이동과 신규 페이지 이동 click listener를 연결하지 않는다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` + - Expected: 콘텐츠 추천 chevron 표시/숨김 source 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +--- + +### Phase 3: 기존 콘텐츠 전체 탭 이동 contract 추가 + +- [ ] **Task 3.1: ContentAllTabViewModel 외부 선택 API 추가** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModel.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentAllTabViewModelTest.kt` + - 작업: + - `fun selectTypeAndSort(type: MainContentAllType, sort: ContentSort)`를 추가한다. + - 함수는 `selectedType`, `selectedSort`, `selectedDayOfWeek`를 갱신하고 `page=0`부터 재조회한다. + - `ORIGINAL`, `AUDIO`, `FREE`, `POINT`는 `dayOfWeek = null`로 요청한다. + - 기존 `changeType`, `changeSort`, `loadInitial` 테스트를 깨지 않는다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentAllTabViewModelTest"` + - Expected: 기존 테스트와 신규 외부 선택 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 3.2: ContentMainFragment에 내부 전체 탭 선택 API 추가** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` + - 작업: + - `fun selectAllTab(type: MainContentAllType, sort: ContentSort = ContentSort.LATEST)`를 추가한다. + - 함수는 `binding.textTabBarContent.root`의 선택을 `전체`로 전환하고 `showContentTab(CONTENT_TAB_ALL)`을 호출한다. + - 이후 `contentAllTabViewModel.selectTypeAndSort(type, sort)`를 호출한다. + - fragment view가 아직 생성되지 않은 경우를 대비해 pending type/sort를 fragment field에 저장하고 `onViewCreated` 이후 적용한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` + - Expected: 내부 전체 탭 선택 source 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 3.3: MainV2Activity 콘텐츠 전체 탭 진입 API 추가** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/MainV2Activity.kt` + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/MainV2ActivitySourceTest.kt` + - 작업: + - `fun openContentAllTab(type: MainContentAllType, sort: ContentSort = ContentSort.LATEST)`를 추가한다. + - 현재 탭을 `MainV2Tab.CONTENT`로 전환한다. + - `changeFragment` 이후 기존 또는 신규 `ContentMainFragment`에 `selectAllTab(type, sort)`를 전달한다. + - 이미 콘텐츠 탭에 있는 경우 fragment를 중복 생성하지 않고 현재 fragment method를 호출한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.MainV2ActivitySourceTest"` + - Expected: MainV2 콘텐츠 전체 탭 routing source 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +--- + +### Phase 4: 기존 콘텐츠 전체 탭 이동 섹션 연결 + +- [ ] **Task 4.1: 콘텐츠 추천 탭 기존 전체 탭 목적지 연결** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` + - 테스트: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` + - 작업: + - `오직 보이스온에서만!` chevron은 `MainContentAllType.ORIGINAL`, `ContentSort.LATEST`로 `MainV2Activity.openContentAllTab`을 호출한다. + - `새로 올라온 오디오` chevron은 `MainContentAllType.AUDIO`, `ContentSort.LATEST`로 호출한다. + - `무료 오디오` chevron은 `MainContentAllType.FREE`, `ContentSort.POPULAR`로 호출한다. + - `포인트 오디오` chevron은 `MainContentAllType.POINT`, `ContentSort.POPULAR`로 호출한다. + - `New&Hot`은 신규 페이지 대상이므로 이 Phase에서는 click listener를 연결하지 않는다. + - 홈 `처음부터 함께 성장!`은 신규 페이지 대상이므로 이 Phase에서는 click listener를 연결하지 않는다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` + - Expected: 기존 전체 탭 이동 섹션별 목적지 source 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 4.2: 기존 전체 탭 이동 수동 검증** + - 확인: + - 콘텐츠 추천 탭 `오직 보이스온에서만!` chevron 클릭 시 콘텐츠 `전체` -> `오리지널` 진입. + - 콘텐츠 추천 탭 `새로 올라온 오디오` chevron 클릭 시 콘텐츠 `전체` -> `오디오` 진입. + - 콘텐츠 추천 탭 `무료 오디오` chevron 클릭 시 콘텐츠 `전체` -> `무료` + `인기순` 진입. + - 콘텐츠 추천 탭 `포인트 오디오` chevron 클릭 시 콘텐츠 `전체` -> `포인트` + `인기순` 진입. + - 콘텐츠 추천 탭 `New&Hot`과 홈 `처음부터 함께 성장!`은 chevron만 표시되고 신규 페이지 이동은 아직 연결되지 않음. + - 검증 기록: + - 구현 시 이 Task 아래에 테스트 기기/빌드 variant/API 응답 조건과 확인 결과를 한국어로 누적한다. + +--- + +### Phase 5: 신규 전체보기 API, DTO, Repository, UI model 추가 + +- [ ] **Task 5.1: ContentOverview API/DTO/Repository 추가** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewApi.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewModels.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/data/ContentOverviewRepository.kt` + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - 작업: + - `ContentOverviewApi.getContents(authHeader, page, size, type)`를 `@GET("/api/v2/contents")`로 정의한다. + - query annotation 이름은 `page`, `size`, `type`만 사용한다. + - DTO는 `@Keep`과 `@SerializedName`을 사용한다. + - `ContentOverviewType`은 `NEW_AND_HOT_AUDIO`, `FIRST_AUDIO_CONTENT`만 정의한다. + - Repository는 `Single>`를 반환한다. + - Koin `networkModule`, `repositoryModule`에 신규 API/Repository를 등록한다. + - 검증: + - Run: `./gradlew :app:compileDebugKotlin` + - Expected: 신규 data layer와 DI 등록이 컴파일된다. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 5.2: Mapper RED 테스트 작성** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewMapperTest.kt` + - 테스트 케이스: + - `NEW_AND_HOT_AUDIO` title은 `New&Hot`이다. + - `FIRST_AUDIO_CONTENT` title은 `처음부터 함께 성장!`이다. + - `price == 0`이면 `AudioContentTag.Free`가 포함된다. + - `isPointAvailable == true`이면 `AudioContentTag.Point`가 포함된다. + - `isFirstContent == true`이면 `AudioContentTag.First`가 포함된다. + - `isOriginalSeries == true`이면 `AudioContentTag.Original`이 포함된다. + - `isAdult == true`이면 `showAdultBadge == true`다. + - `page`, `size`, `hasNext`, `type`은 UI state에 보존된다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewMapperTest"` + - Expected: mapper 구현 전 RED 실패. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 5.3: UI model과 mapper 구현** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiState.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewUiModels.kt` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/model/ContentOverviewMappers.kt` + - 작업: + - `ContentOverviewUiState.Loading`, `Content`, `Empty`, `Error`를 정의한다. + - `ContentOverviewUiModel`은 `contentId`, `title`, `coverImage`, `creatorNickname`, `tags`, `showAdultBadge`를 포함한다. + - `ContentOverviewType.toTitleResId()`를 정의한다. + - `NEW_AND_HOT_AUDIO`는 `R.string.content_recommendation_section_new_and_hot`, `FIRST_AUDIO_CONTENT`는 `R.string.home_recommendation_section_first_audio_contents`로 매핑한다. + - `ContentOverviewItemResponse.toUiModel()`에서 PRD tag mapping을 구현한다. + - `ContentOverviewPageResponse.toContent()`에서 paging metadata와 item list를 보존한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewMapperTest"` + - Expected: mapper 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +--- + +### Phase 6: 신규 전체보기 ViewModel, Activity, Adapter 구현 + +- [ ] **Task 6.1: ViewModel RED 테스트 작성** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewViewModelTest.kt` + - 테스트 케이스: + - 초기 로드는 전달받은 type으로 `page=0`, `size=20`을 요청한다. + - 첫 페이지 성공 + items 있음은 `Content` 상태를 emit한다. + - 첫 페이지 성공 + items empty는 `Empty` 상태를 emit한다. + - 첫 페이지 실패는 `Error` 상태와 toast를 emit한다. + - `hasNext = true` 상태에서 `loadMore()`는 다음 page를 요청하고 append한다. + - `hasNext = false`이면 `loadMore()`가 repository를 추가 호출하지 않는다. + - 추가 페이지 실패 시 기존 items를 유지하고 pagination error message를 emit한다. + - type 변경 또는 새 화면 생성 상태 간 page/items를 공유하지 않는다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewViewModelTest"` + - Expected: ViewModel 구현 전 RED 실패. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 6.2: ViewModel 구현** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewViewModel.kt` + - 작업: + - `loadFirstPage(type: ContentOverviewType)`를 구현한다. + - `loadMore()`는 `Content` 상태, `hasNext`, `isLoadingMore`를 확인한 뒤 다음 page만 요청한다. + - `requestGeneration`으로 stale response가 현재 화면 상태를 덮어쓰지 않게 한다. + - auth header는 기존 ViewModel 패턴대로 `Bearer ${SharedPreferenceManager.token}`를 사용한다. + - 첫 페이지 loading은 `isLoading` LiveData로 처리하고, 추가 페이지 loading은 `Content.isLoadingMore`로 처리한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewViewModelTest"` + - Expected: ViewModel 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 6.3: Activity layout과 Adapter 구현** + - 생성: + - `app/src/main/res/layout/activity_content_overview.xml` + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ui/ContentOverviewAdapter.kt` + - 수정: + - `app/src/main/AndroidManifest.xml` + - 작업: + - layout root는 black background를 사용한다. + - title bar는 60dp 높이, 좌측 back chevron, 22sp bold title을 배치한다. + - RecyclerView는 2열 `GridLayoutManager`로 표시한다. + - RecyclerView item은 기존 `item_content_audio_card.xml`의 `AudioContentCardView`를 재사용한다. + - Adapter는 `setGridItemWidthPx(widthPx)`를 호출해 2열 item width를 적용한다. + - 이미지 로딩은 기존 `loadUrl` 확장을 사용한다. + - manifest에 `kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewActivity`를 등록한다. + - 검증: + - Run: `./gradlew :app:mergeDebugResources` + - Expected: 신규 layout과 resource reference가 merge된다. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 6.4: Activity source RED 테스트 작성** + - 생성: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivitySourceTest.kt` + - 테스트 케이스: + - `ContentOverviewActivity.newIntent(context, type)`가 type extra를 포함한다. + - layout에 back button, title TextView, RecyclerView, empty/error TextView가 있다. + - Activity가 `GridLayoutManager(this, CONTENT_OVERVIEW_GRID_SPAN_COUNT)`로 2열 grid를 설정한다. + - scroll bottom에서 `viewModel.loadMore()`를 호출한다. + - item click은 `Constants.EXTRA_AUDIO_CONTENT_ID`로 `AudioContentDetailActivity`를 연다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.ContentOverviewActivitySourceTest"` + - Expected: Activity 구현 전 또는 연결 전 RED 실패. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 6.5: Activity 구현과 ViewModel 연결** + - 생성: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/overview/ContentOverviewActivity.kt` + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt` + - 작업: + - `newIntent(context, type)` companion method를 제공한다. + - intent type extra가 없거나 enum parsing에 실패하면 `NEW_AND_HOT_AUDIO`를 fallback으로 사용한다. + - title은 type mapping으로 표시한다. + - back button은 `finish()`를 호출한다. + - `Content`, `Empty`, `Error`, `Loading` 상태별 UI를 바인딩한다. + - item click은 `AudioContentDetailActivity`를 열고 `Constants.EXTRA_AUDIO_CONTENT_ID`에 `contentId`를 전달한다. + - 성인 콘텐츠 접근 제한은 기존 `AudioContentDetailActivity` 정책을 따른다. + - Koin `viewModelModule`에 `ContentOverviewViewModel`을 등록한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.*"` + - Expected: overview mapper/ViewModel/source 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +--- + +### Phase 7: 신규 전체보기 대상 섹션 이동 연결 + +- [ ] **Task 7.1: 홈 FIRST 신규 전체보기 이동 연결** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt` + - 테스트: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentSourceTest.kt` + - 작업: + - `viewHomeFirstAudioTitle.ivSectionTitleChevron.setOnClickListener`를 추가한다. + - 클릭 시 `ContentOverviewActivity.newIntent(requireContext(), ContentOverviewType.FIRST_AUDIO_CONTENT)`로 이동한다. + - `ensureMainV2NavigationAllowed`로 기존 홈 navigation guard 패턴을 따른다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.HomeMainFragmentSourceTest"` + - Expected: 홈 FIRST 신규 전체보기 이동 source 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +- [ ] **Task 7.2: 콘텐츠 New&Hot 신규 전체보기 이동 연결** + - 수정: + - `app/src/main/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragment.kt` + - 테스트: + - `app/src/test/java/kr/co/vividnext/sodalive/v2/main/content/ContentMainFragmentSourceTest.kt` + - 작업: + - `viewContentNewAndHotTitle.ivSectionTitleChevron.setOnClickListener`를 추가한다. + - 클릭 시 `ContentOverviewActivity.newIntent(requireContext(), ContentOverviewType.NEW_AND_HOT_AUDIO)`로 이동한다. + - `ensureMainV2NavigationAllowed`로 기존 콘텐츠 navigation guard 패턴을 따른다. + - 기존 전체 탭 이동 대상 섹션 click listener는 Phase 4 동작을 유지한다. + - 검증: + - Run: `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.ContentMainFragmentSourceTest"` + - Expected: 콘텐츠 New&Hot 신규 전체보기 이동 source 테스트 PASS. + - 검증 기록: + - 구현 시 이 Task 아래에 명령, 결과, 확인 내용을 한국어로 누적한다. + +--- + +### Phase 8: 통합 검증과 수동 확인 + +- [ ] **Task 8.1: 단위/source 테스트 실행** + - 실행: + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.*"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.home.*"` + - `./gradlew :app:testDebugUnitTest --tests "kr.co.vividnext.sodalive.v2.main.content.overview.*"` + - 기대 결과: + - 수정된 홈/콘텐츠/MainV2 source 테스트와 신규 overview 테스트가 모두 PASS한다. + - 검증 기록: + - 구현 시 이 Task 아래에 실행 명령과 결과를 한국어로 누적한다. + +- [ ] **Task 8.2: 빌드/리소스/린트 검증** + - 실행: + - `./gradlew :app:mergeDebugResources` + - `./gradlew :app:compileDebugKotlin` + - `./gradlew :app:ktlintCheck` + - `git diff --check` + - 기대 결과: + - resource merge, Kotlin compile, ktlint, whitespace 검증이 모두 PASS한다. + - 검증 기록: + - 구현 시 이 Task 아래에 실행 명령과 결과를 한국어로 누적한다. + +- [ ] **Task 8.3: 수동 화면 검증** + - 확인: + - 홈 추천 탭 `처음부터 함께 성장!` title 우측 chevron 표시. + - 콘텐츠 추천 탭 `오직 보이스온에서만!`, `새로 올라온 오디오`, `New&Hot`, `무료 오디오`, `포인트 오디오` title 우측 chevron 표시. + - 콘텐츠 추천 탭 `댓글 많은 오디오`, `추천 오디오`에는 chevron이 표시되지 않음. + - 콘텐츠 추천 탭 `오직 보이스온에서만!` chevron 클릭 시 콘텐츠 `전체` -> `오리지널` 진입. + - 콘텐츠 추천 탭 `새로 올라온 오디오` chevron 클릭 시 콘텐츠 `전체` -> `오디오` 진입. + - 콘텐츠 추천 탭 `무료 오디오` chevron 클릭 시 콘텐츠 `전체` -> `무료` + `인기순` 진입. + - 콘텐츠 추천 탭 `포인트 오디오` chevron 클릭 시 콘텐츠 `전체` -> `포인트` + `인기순` 진입. + - 홈 추천 탭 `처음부터 함께 성장!` chevron 클릭 시 `처음부터 함께 성장!` 신규 전체보기 진입. + - 콘텐츠 추천 탭 `New&Hot` chevron 클릭 시 `New&Hot` 신규 전체보기 진입. + - 신규 전체보기 화면 title bar, back button, 2열 grid, tag, 성인 badge, 상세 이동. + - 신규 전체보기 화면 하단 스크롤 시 `page + 1` 추가 요청. + - 검증 기록: + - 구현 시 이 Task 아래에 테스트 기기/빌드 variant/API 응답 조건과 확인 결과를 한국어로 누적한다. + +--- + +## Verification Log +- 구현 완료 후 여러 Phase에 걸친 통합 검증, 회귀 검증, 최종 수동 확인 기록을 이 섹션에 누적한다. diff --git a/docs/20260627_콘텐츠_전체보기/prd.md b/docs/20260627_콘텐츠_전체보기/prd.md new file mode 100644 index 00000000..1027e456 --- /dev/null +++ b/docs/20260627_콘텐츠_전체보기/prd.md @@ -0,0 +1,248 @@ +# PRD: 콘텐츠 전체보기 + +## 1. Overview +홈 추천 탭과 콘텐츠 추천 탭의 일부 섹션에서 `전체보기` 진입을 제공하고, `New&Hot` 및 `처음부터 함께 성장!`은 신규 콘텐츠 전체보기 화면에서 `GET /api/v2/contents` API로 페이징 목록을 표시한다. + +작성일: 2026-06-27 + +--- + +## 2. Problem +- 콘텐츠 추천 탭의 섹션 타이틀 우측 chevron이 현재 전체보기 이동 정책과 연결되어 있지 않다. +- `New&Hot`과 `처음부터 함께 성장!`은 기존 콘텐츠 탭 내부 `전체` 탭의 카테고리/정렬 조합으로 표현하지 않고, 별도 API 기반 전체보기 화면이 필요하다. +- 그 외 콘텐츠 추천 섹션은 신규 화면을 만들지 않고 기존 `콘텐츠 탭 - 전체`의 특정 카테고리/정렬 상태로 이동해야 한다. +- 동일한 오디오 카드, 태그, 페이징 목록 UI가 이미 V2 패키지 하위에 있으므로 신규 UI를 중복 작성하지 않도록 재사용 후보를 먼저 정리해야 한다. + +--- + +## 3. Goals +- 홈 추천 탭의 `처음부터 함께 성장!` 섹션 우측에 chevron을 표시하고 신규 콘텐츠 전체보기 화면으로 이동한다. +- 콘텐츠 추천 탭에서 전체보기가 필요한 섹션 제목 우측에 chevron을 표시한다. +- 콘텐츠 추천 탭의 `New&Hot` 섹션은 신규 콘텐츠 전체보기 화면으로 이동한다. +- 콘텐츠 추천 탭의 `오직 보이스온에서만!`, `새로 올라온 오디오`, `무료 오디오`, `포인트 오디오`는 기존 `콘텐츠 탭 - 전체`의 지정 상태로 이동한다. +- 신규 콘텐츠 전체보기 화면은 `type = NEW_AND_HOT_AUDIO` 또는 `type = FIRST_AUDIO_CONTENT`로 `GET /api/v2/contents`를 호출한다. +- 신규 콘텐츠 전체보기 화면은 Figma node `482:15105`의 `detail_ado_001` 구조를 기준으로 검은 배경, title bar, 2열 오디오 카드 그리드, 스크롤 페이징을 제공한다. +- V2 패키지 하위 기존 위젯 중 재사용 가능한 후보를 문서에 기록한다. + +--- + +## 4. Non-Goals +- 이번 PRD 작성 단계에서는 코드, 리소스, 레이아웃 파일을 구현하지 않는다. +- 콘텐츠 상세, 시리즈 상세, 결제, 보관함 기능은 변경하지 않는다. +- 레거시 화면 또는 레거시 API 파일을 직접 수정하지 않는다. +- `GET /api/v2/audio/contents` API 계약은 변경하지 않는다. +- 신규 콘텐츠 전체보기 화면에 별도 정렬, 필터, 검색, pull-to-refresh, skeleton loading을 추가하지 않는다. +- `New&Hot`과 `처음부터 함께 성장!` 외 섹션을 신규 전체보기 API로 조회하지 않는다. +- Figma localhost asset URL을 앱 코드에 직접 의존하지 않는다. + +--- + +## 5. Target Users +- 홈 추천 탭에서 `처음부터 함께 성장!` 콘텐츠를 더 많이 탐색하려는 사용자. +- 콘텐츠 추천 탭에서 `New&Hot`, 최신 오디오, 무료/포인트 오디오, 오리지널 콘텐츠를 섹션별로 더 보고 싶은 사용자. +- V2 메인 홈/콘텐츠 화면과 신규 전체보기 화면을 구현/유지보수하는 Android 개발자. + +--- + +## 6. User Stories +- 사용자는 홈 추천 탭의 `처음부터 함께 성장!` 섹션 제목 우측 chevron을 눌러 같은 성격의 콘텐츠 전체 목록을 보고 싶다. +- 사용자는 콘텐츠 추천 탭의 `New&Hot` 섹션 제목 우측 chevron을 눌러 `New&Hot` 전체 목록을 보고 싶다. +- 사용자는 콘텐츠 추천 탭의 `오직 보이스온에서만!`을 누르면 콘텐츠 탭의 `전체` 내부에서 오리지널 카테고리가 선택된 화면으로 이동하길 기대한다. +- 사용자는 콘텐츠 추천 탭의 `새로 올라온 오디오`를 누르면 콘텐츠 탭의 `전체` 내부에서 오디오 카테고리가 선택된 화면으로 이동하길 기대한다. +- 사용자는 콘텐츠 추천 탭의 `무료 오디오` 또는 `포인트 오디오`를 누르면 콘텐츠 탭의 `전체` 내부에서 해당 카테고리와 인기순 정렬이 선택된 화면으로 이동하길 기대한다. +- 사용자는 신규 전체보기 화면에서 콘텐츠를 2열 그리드로 탐색하고, 목록 하단에 도달하면 다음 페이지가 이어서 로드되길 기대한다. + +--- + +## 7. Core Features + +### Feature A. 전체보기 섹션 chevron 표시 +#### Requirements +- 전체보기 진입이 필요한 섹션 제목 우측에는 `view_section_title.xml`의 `iv_section_title_chevron`을 표시한다. +- 홈 추천 탭에서는 `처음부터 함께 성장!` 섹션에 chevron을 표시하고, 클릭 시 신규 콘텐츠 전체보기 화면에 `type = FIRST_AUDIO_CONTENT`로 진입한다. +- 콘텐츠 추천 탭에서는 아래 섹션에 chevron을 표시한다. + - `오직 보이스온에서만!` + - `새로 올라온 오디오` + - `New&Hot` + - `무료 오디오` + - `포인트 오디오` +- 콘텐츠 추천 탭의 `댓글 많은 오디오`, `추천 오디오`는 이번 요구사항에 전체보기 목적지가 없으므로 chevron을 표시하지 않는다. +- 섹션 데이터가 비어 섹션 자체가 숨겨지는 경우 chevron도 함께 노출되지 않는다. + +#### Edge Cases +- 빠르게 chevron을 중복 탭해도 동일 화면이 중복으로 여러 개 쌓이지 않도록 기존 navigation guard 패턴을 우선 따른다. +- 전체보기 목적지에 필요한 enum 또는 tab 상태가 유효하지 않으면 이동하지 않는다. + +### Feature B. 콘텐츠 추천 탭 전체보기 라우팅 +#### Requirements +- 콘텐츠 추천 탭의 전체보기 이동 규칙은 아래와 같다. + +| 섹션 | 이동 목적지 | +| --- | --- | +| `오직 보이스온에서만!` | `콘텐츠 탭 - 전체` -> `오리지널` 카테고리 선택 | +| `새로 올라온 오디오` | `콘텐츠 탭 - 전체` -> `오디오` 카테고리 선택 | +| `New&Hot` | 신규 콘텐츠 전체보기 화면 -> `type = NEW_AND_HOT_AUDIO` | +| `무료 오디오` | `콘텐츠 탭 - 전체` -> `무료` 카테고리 선택 -> `인기순` 정렬 | +| `포인트 오디오` | `콘텐츠 탭 - 전체` -> `포인트` 카테고리 선택 -> `인기순` 정렬 | + +- 기존 `콘텐츠 탭 - 전체`로 이동하는 경우 `ContentMainFragment` 내부 탭은 `전체`가 선택되어야 한다. +- `무료 오디오`, `포인트 오디오`는 `ContentSort.POPULAR`에 해당하는 정렬 상태로 진입한다. +- `오직 보이스온에서만!`은 `오리지널` 카테고리로 진입한다. + +#### Edge Cases +- 이미 콘텐츠 탭에 있는 상태에서 전체보기 이동을 누르면 새 메인 화면을 중복 생성하지 않고 현재 `ContentMainFragment`의 내부 상태 전환을 우선 검토한다. +- 홈 탭에서 콘텐츠 탭 내부 `전체`로 이동해야 하는 후속 요구가 생기면 `MainActivity`/`MainV2` 탭 전환 계약을 별도 계획에서 확인한다. + +### Feature C. 신규 콘텐츠 전체보기 화면 +#### Requirements +- 신규 화면은 기존 로직 수정이 아닌 신규 `Activity`, `ViewModel`, API, Repository, DTO, adapter/helper로 구현한다면 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다. +- 화면 title bar 제목은 진입 type에 따라 아래처럼 표시한다. + - `NEW_AND_HOT_AUDIO`: `New&Hot` + - `FIRST_AUDIO_CONTENT`: `처음부터 함께 성장!` +- title bar는 검은 배경, 좌측 back chevron, 22sp bold 제목 구조를 따른다. +- 콘텐츠 목록은 Figma node `482:15105` 기준으로 2열 오디오 카드 그리드로 표시한다. +- 카드에는 썸네일, 제목, 크리에이터 닉네임, 무료/포인트/FIRST/오리지널/성인 태그를 응답 값에 따라 표시한다. +- 카드 터치 시 기존 오디오 콘텐츠 상세 화면으로 이동한다. +- 목록은 첫 페이지 로딩, 빈 목록, 에러, 추가 페이지 로딩 상태를 구분한다. +- `hasNext = true`이고 사용자가 목록 하단에 접근하면 다음 `page`를 요청한다. + +#### Figma Reference +- URL: `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-공유용-보이스온-UI-UX-기획문서?node-id=482-15105&m=dev` +- node: `482:15105` +- frame name: `detail_ado_001` +- 확인된 구조: + - 화면 배경: black + - title bar height: 60 + - title: `New&Hot` + - content start: title bar 하단 이후 + - grid: 2열, 카드 폭 약 185, 카드 간격 약 4 + - thumbnail: 정사각형, radius 14 + - label: 제목 18sp bold, 크리에이터명 14sp medium, 한 줄 말줄임 + - tags: FIRST, point, free, original audio, adult badge 조합 + +#### Edge Cases +- `contentId <= 0`인 item은 상세 이동을 무시한다. +- `coverImage`가 null 또는 blank이면 기존 이미지 로딩 placeholder/null 처리 정책을 따른다. +- 제목 또는 크리에이터명이 길면 한 줄 말줄임 처리한다. +- 첫 페이지 응답의 `items`가 비어 있으면 빈 목록 상태를 표시한다. +- 추가 페이지 실패 시 기존 목록은 유지하고 재시도 가능한 상태를 제공한다. + +### Feature D. 신규 콘텐츠 전체보기 API +#### API Contract +```kotlin +GET /api/v2/contents +``` + +#### Query Parameters +```kotlin +page: Int = 0 +size: Int = 20 +type: ContentOverviewType = ContentOverviewType.NEW_AND_HOT_AUDIO +``` + +#### Response Data Class +```kotlin +data class ContentOverviewPageResponse( + val type: ContentOverviewType, + val items: List, + val page: Int, + val size: Int, + @SerializedName("hasNext") + val hasNext: Boolean +) + +enum class ContentOverviewType { + NEW_AND_HOT_AUDIO, + FIRST_AUDIO_CONTENT +} + +data class ContentOverviewItemResponse( + val contentId: Long, + val title: String, + val coverImage: String?, + val price: Int, + @SerializedName("isPointAvailable") + val isPointAvailable: Boolean, + val creatorNickname: String, + @SerializedName("isAdult") + val isAdult: Boolean, + @SerializedName("isFirstContent") + val isFirstContent: Boolean, + @SerializedName("isOriginalSeries") + val isOriginalSeries: Boolean +) +``` + +#### Requirements +- API 기본값은 `page = 0`, `size = 20`, `type = NEW_AND_HOT_AUDIO`로 취급한다. +- query parameter key는 모두 소문자 `page`, `size`, `type`을 사용한다. +- 앱에서는 진입 목적에 맞게 `type`을 명시적으로 전달한다. +- 응답 DTO는 서버 계약을 변경하지 않는다. +- 서버 예시 class에 Jackson `@JsonProperty`가 포함되어 있더라도 앱 구현에서는 기존 Gson 관례에 맞춰 `@SerializedName`을 사용한다. +- UI model에서는 `price == 0`이면 무료 태그, `isPointAvailable == true`이면 포인트 태그, `isFirstContent == true`이면 FIRST 태그, `isOriginalSeries == true`이면 오리지널 태그, `isAdult == true`이면 성인 배지로 매핑한다. + +#### Edge Cases +- 응답 `type`이 요청 `type`과 다르면 현재 요청 type 기준으로 화면 제목을 유지하고, 데이터 혼입 방지 정책은 구현 계획에서 확정한다. +- `hasNext = false`이면 다음 페이지를 요청하지 않는다. +- 동일 type에서 추가 페이지 요청 중 중복 요청을 방지한다. +- 다른 type의 신규 전체보기 화면을 열 때는 이전 화면의 page/items 상태를 공유하지 않는다. + +--- + +## 8. UX / UI Expectations +- 신규 전체보기 화면은 V2의 검은 배경과 콘텐츠 카드 스타일을 유지한다. +- 상단 title bar는 스크롤되지 않고, 목록만 세로 스크롤된다. +- 2열 그리드는 화면 폭에 맞춰 item width를 계산하되, Figma의 185px 카드와 4px 간격 비율을 Android 화면에서 자연스럽게 유지한다. +- 오디오 카드는 기존 `AudioContentCardView`의 태그 표현과 최대한 일치시킨다. +- 성인 배지는 썸네일 우측 상단에 표시한다. +- 무료/포인트 태그는 썸네일 하단 좌측, FIRST/오리지널 태그는 썸네일 상단 좌측의 기존 패턴을 우선 따른다. +- 홈 추천 탭과 콘텐츠 추천 탭의 섹션 chevron은 기존 `view_section_title.xml`의 `ic_chevron_right`를 사용한다. + +### 재사용 가능한 V2 위젯/코드 후보 +- `app/src/main/res/layout/view_section_title.xml` + - 섹션 제목과 우측 chevron 표시/숨김에 재사용 가능하다. +- `kr.co.vividnext.sodalive.v2.widget.AudioContentCardView` + - 신규 전체보기 2열 오디오 카드의 기본 카드 UI 후보이다. +- `kr.co.vividnext.sodalive.v2.widget.AudioContentCardSize` + - 기존 카드 크기 variant를 확인해 신규 2열 grid width 적용 가능성을 검토한다. +- `kr.co.vividnext.sodalive.v2.widget.AudioContentTag` + - 무료/포인트/FIRST/오리지널 태그 매핑에 재사용 가능하다. +- `kr.co.vividnext.sodalive.v2.main.content.ui.ContentAllAudioCardAdapter` + - 기존 콘텐츠 `전체` 탭 3열 grid adapter이며, 동적 grid item width 적용 패턴을 참고할 수 있다. +- `kr.co.vividnext.sodalive.v2.main.content.ui.ContentAudioCardAdapter` + - 추천 탭의 가로 오디오 카드 바인딩 패턴을 참고할 수 있다. +- `kr.co.vividnext.sodalive.v2.main.content.ui.ContentNewAndHotAdapter` + - `New&Hot` 섹션의 리스트형 표현에는 이미 사용 중이지만, 신규 전체보기 2열 grid에는 직접 재사용보다 item mapping/tag binding 참고 후보이다. +- `kr.co.vividnext.sodalive.v2.main.home.ui.HomeFirstAudioAdapter` + - 홈 `처음부터 함께 성장!` 섹션의 FIRST/point/free 태그 바인딩 패턴 참고 후보이다. +- `kr.co.vividnext.sodalive.v2.main.content.ContentAllTabViewModel` + - page/size/hasNext 기반 페이징 상태 관리 패턴 참고 후보이다. +- `kr.co.vividnext.sodalive.v2.main.content.data.MainContentAllTabApi` + - V2 콘텐츠 API의 Retrofit/Rx/`ApiResponse` 계약 참고 후보이다. + +--- + +## 9. Technical Constraints +- Android Gradle 단일 `:app` 모듈에서 작업한다. +- 모든 명령은 저장소 루트에서 실행한다. +- 신규 화면/하위 코드는 `kr.co.vividnext.sodalive.v2` 패키지 하위에 작성한다. +- 레거시 파일은 직접 수정하지 않고, 필요한 기존 화면은 Intent 또는 wrapper/adapter로 호출한다. +- API 흐름은 기존 관례인 `Api -> Repository -> ViewModel -> Activity/Fragment`를 따른다. +- DI 추가가 필요하면 `AppDI.kt`의 Koin 구성 관례를 따른다. +- 외부 라이브러리를 추가하지 않는다. +- 공개 API 스키마와 서버 enum 값을 임의 변경하지 않는다. +- `BuildConfig` 값이나 민감정보를 로그/Toast/크래시 메시지에 노출하지 않는다. + +--- + +## 10. Metrics +- 홈 추천 탭 `처음부터 함께 성장!` chevron 클릭 수. +- 콘텐츠 추천 탭 섹션별 chevron 클릭 수. +- 신규 콘텐츠 전체보기 화면 진입 수. +- 신규 콘텐츠 전체보기 화면의 다음 페이지 로드 성공/실패 수. +- 신규 콘텐츠 전체보기 화면에서 콘텐츠 상세로 이동한 클릭 수. + +--- + +## 11. Open Questions +- 기존 `ContentMainFragment` 상태를 외부에서 특정 내부 탭/카테고리/정렬로 열기 위한 public navigation contract가 충분한지 구현 계획에서 확인해야 한다.