# PRD: 메인 콘텐츠 탭 내부 전체 탭 ## 1. Overview 메인 하단 `콘텐츠` 탭 안의 내부 `전체` 탭에서 `GET /api/v2/audio/contents` 응답을 사용해 오디오/시리즈/오리지널/무료/포인트 콘텐츠 목록을 제공한다. 상단 title bar와 내부 `추천/랭킹/전체` tab bar는 기존 콘텐츠 탭 구조를 유지한다. 내부 `전체` 탭 화면에서는 콘텐츠 타입 chip, 요일 필터, 정렬 필터, 콘텐츠 grid를 구성하고, 목록에는 스크롤 페이징을 적용한다. Figma 타입 chip에는 `전체`가 보이지만 API `type` enum에 대응 값이 없으므로 `전체` chip은 구현 대상에서 제외한다. Figma 참조: - 오디오 선택 상태: `35:5857`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=35-5857&m=dev` - 시리즈 선택 상태: `24:6909`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-6909&m=dev` - 오리지널 선택 상태: `24:9105`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=24-9105&m=dev` ## 2. Problem - 현재 V2 메인 콘텐츠 탭은 내부 `추천` 탭 중심으로 구성되어 있고, 내부 `전체` 탭에서 타입별 콘텐츠 목록을 API 기반으로 탐색하는 요구사항이 분리되어 있지 않다. - 같은 endpoint가 `type`에 따라 `audios` 또는 `series` 배열을 내려주므로, 선택 타입별 데이터 매핑 규칙을 명확히 해야 한다. - `SERIES` 타입에서만 `dayOfWeek` query parameter를 보내야 하므로, 요일 필터 UI와 API 요청 조건을 분리해야 한다. - 기존 `SeriesPublishedDaysOfWeek`는 legacy 영역에 있고, 짧은 요일 다국어 표기 중 `기타`의 영어/일본어 요구사항이 기존 `Random` 표기와 다르다. ## 3. Goals - 메인 콘텐츠 탭 내부 `전체` 탭을 추가한다. - 콘텐츠 타입 기본 선택값은 `AUDIO`로 둔다. - Figma 타입 chip 중 `전체` chip은 구현하지 않고 `오디오`, `시리즈`, `오리지널`, `무료`, `포인트`만 제공한다. - `GET /api/v2/audio/contents`를 호출하고 `type`, `sort`, `page`, `size`, `dayOfWeek` query parameter를 요구사항대로 전송한다. - 기본 query 값은 `page = 0`, `size = 20`, `sort = LATEST`, `type = AUDIO`로 둔다. - `dayOfWeek` 기본값은 현재 디바이스에 설정된 요일로 둔다. - `dayOfWeek`는 `type == SERIES`일 때만 요청 query에 포함한다. - `AUDIO`, `FREE`, `POINT` 선택 시 response의 `audios`를 표시한다. - `SERIES`, `ORIGINAL` 선택 시 response의 `series`를 표시한다. - 목록 하단 도달 시 `hasNext == true`이면 다음 page를 로드해 append한다. - 내부 `추천/랭킹/전체` 탭 전환 시 `전체` 탭의 선택 타입, 정렬, 요일, 로드된 목록 상태를 유지한다. - response의 `totalCount`는 모델에는 반영하되 UI에는 표시하지 않는다. - Figma 기준 오디오/무료/포인트는 `AudioContentThumbnailCard` 재사용을 우선한다. - Figma 기준 시리즈/오리지널은 기존 시리즈 카드 UI 후보를 확인하고, 응답 모델에 맞는 전용 adapter 또는 전용 thumbnail card를 사용한다. - 요일 짧은 표기에서 `기타`는 영어 `OTHER`, 일본어 `その他`로 표시한다. ## 4. Non-Goals - 실제 구현과 Xcode 프로젝트 수정은 이번 범위에 포함하지 않는다. - 추천 탭의 `GET /api/v2/audio/recommendations` 구현 범위는 변경하지 않는다. - 랭킹 탭 API/화면은 이번 범위에 포함하지 않는다. - Figma 타입 chip의 `전체` chip은 구현하지 않는다. - Figma sort menu의 `추천순`은 구현하지 않는다. - response의 `totalCount`를 화면에 표시하지 않는다. - Figma에 보이는 localhost asset URL을 앱 코드에 직접 사용하지 않는다. - API 명세에 없는 신규 `type` 값을 임의로 추가하지 않는다. - 외부 라이브러리를 추가하지 않는다. - `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다. ## 5. Target Users - 메인 콘텐츠 탭에서 전체 콘텐츠를 타입별로 빠르게 탐색하려는 사용자 - 오디오/무료/포인트 콘텐츠를 썸네일 grid로 훑고 상세로 이동하려는 사용자 - 시리즈와 오리지널 시리즈를 구분해 탐색하려는 사용자 - 요일별 시리즈를 현재 디바이스 요일 기준으로 바로 확인하려는 사용자 ## 6. User Stories - 사용자는 콘텐츠 탭의 내부 `전체` 탭에 진입하면 기본으로 오디오 목록을 보고 싶다. - 사용자는 타입 chip을 눌러 오디오, 시리즈, 오리지널, 무료, 포인트 목록을 전환하고 싶다. - 사용자는 시리즈 타입에서 현재 요일의 시리즈가 기본으로 선택되어 있기를 기대한다. - 사용자는 시리즈 타입에서 요일을 바꾸면 해당 요일 시리즈 목록이 새로 로드되기를 기대한다. - 사용자는 정렬 메뉴에서 최신순 등 정렬 기준을 바꾸고 첫 페이지부터 다시 보고 싶다. - 사용자는 스크롤을 내리면 다음 페이지가 자연스럽게 이어서 로드되기를 기대한다. - 사용자는 `추천/랭킹/전체` 탭을 오가도 `전체` 탭에서 보던 필터와 목록 상태가 유지되기를 기대한다. ## 7. Core Requirements ### 7.1 API - Method: `GET` - Path: `/api/v2/audio/contents` - 인증: 기존 V2 API 인증 헤더 패턴을 따른다. - 응답 래퍼: 기존 관례대로 `ApiResponse` 디코딩을 우선한다. Query parameter: - `type: MainContentAllType` - `sort: ContentSort` - `page: Int` - `size: Int` - `dayOfWeek: SeriesPublishedDaysOfWeek` 기본값: - `type = AUDIO` - `sort = LATEST` - `page = 0` - `size = 20` - `dayOfWeek = Calendar.current` 기준 현재 디바이스 요일 전송 조건: - `dayOfWeek`는 `type == SERIES`일 때만 query에 포함한다. - `type`이 `AUDIO`, `ORIGINAL`, `FREE`, `POINT`이면 `dayOfWeek`를 보내지 않는다. - 타입, 정렬, 요일 변경 시 `page = 0`부터 다시 요청한다. - 스크롤 페이징 요청은 현재 filter 상태를 유지한 채 `page + 1`로 요청한다. ### 7.2 Type enum ```kotlin enum class MainContentAllType { AUDIO, SERIES, ORIGINAL, FREE, POINT } ``` Swift 모델은 기존 enum 스타일에 맞춰 `String`, `Decodable`, `Encodable`, `CaseIterable`, `Hashable` 채택을 우선한다. ### 7.3 Response model ```kotlin data class MainContentAllTabResponse( val type: MainContentAllType, val totalCount: Int, val audios: List, val series: List, val sort: ContentSort, val dayOfWeek: SeriesPublishedDaysOfWeek?, val page: Int, val size: Int, @JsonProperty("hasNext") val hasNext: Boolean ) 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 ) ``` Swift 모델은 기존 V2 관례에 맞춰 `Decodable` 구조체로 선언한다. `Long` 대응 id는 프로젝트 기존 모델 관례에 맞춰 Swift `Int`를 우선 사용한다. ### 7.4 Type별 data mapping - `AUDIO`: response `audios`를 오디오 grid로 표시한다. - `FREE`: response `audios`를 오디오 grid로 표시한다. - `POINT`: response `audios`를 오디오 grid로 표시한다. - `SERIES`: response `series`를 시리즈 grid로 표시하고, 요일 필터를 노출한다. - `ORIGINAL`: response `series`를 시리즈 grid로 표시하고, 요일 필터는 노출하지 않는다. - API enum에 `ALL`이 없으므로 Figma의 `전체` type chip은 구현하지 않는다. - response의 `totalCount`는 pagination 판단이나 디버깅 보조 값으로만 유지하고, 화면에는 표시하지 않는다. ### 7.5 Paging - `page = 0` 응답은 기존 목록을 교체한다. - `page > 0` 응답은 현재 선택 타입에 맞는 목록 뒤에 append한다. - `hasNext == false`이면 추가 요청을 막는다. - 이미 다음 페이지 로딩 중이면 중복 요청을 막는다. - 타입, 정렬, 요일 변경 시 기존 목록을 비우고 첫 페이지를 다시 요청한다. - API 실패 시 기존 목록이 있으면 유지하고 toast 또는 error message를 표시한다. - 첫 페이지 실패 또는 결과 없음은 empty state를 표시한다. ### 7.6 Sort - 기본 정렬은 `ContentSort.latest` / API 값 `LATEST`이다. - 정렬 menu는 Figma처럼 sort bar 우측에 표시한다. - 정렬 변경 시 첫 페이지부터 다시 요청한다. - `추천순`은 구현하지 않는다. - `FREE` 타입에서는 `최신순`, `인기순`만 제공한다. - `AUDIO`, `SERIES`, `ORIGINAL`, `POINT` 타입에서 제공할 sort 후보: - `LATEST` - `POPULAR` - `PRICE_HIGH` - `PRICE_LOW` - `FREE` 타입에서 제공할 sort 후보: - `LATEST` - `POPULAR` - 현재 선택된 sort가 타입 변경 후 허용되지 않는 값이면 `LATEST`로 초기화하고 첫 페이지부터 다시 요청한다. ### 7.7 Day of week - `SeriesPublishedDaysOfWeek` 기존 enum을 사용한다. - 현재 디바이스 요일 기본값은 `Calendar.current.component(.weekday, from: Date())`를 기준으로 매핑한다. - Sunday: `.SUN` - Monday: `.MON` - Tuesday: `.TUE` - Wednesday: `.WED` - Thursday: `.THU` - Friday: `.FRI` - Saturday: `.SAT` - `RANDOM`은 Figma의 `기타` 표시와 매핑한다. - 요일 필터는 `SERIES` 타입에서만 노출한다. - `ORIGINAL` 타입은 response `series`를 쓰지만 `dayOfWeek`를 보내지 않고 요일 필터도 노출하지 않는다. 짧은 요일 다국어 표기: - `MON`: ko `월`, en `MON`, ja `月` - `TUE`: ko `화`, en `TUE`, ja `火` - `WED`: ko `수`, en `WED`, ja `水` - `THU`: ko `목`, en `THU`, ja `木` - `FRI`: ko `금`, en `FRI`, ja `金` - `SAT`: ko `토`, en `SAT`, ja `土` - `SUN`: ko `일`, en `SUN`, ja `日` - `RANDOM`: ko `기타`, en `OTHER`, ja `その他` ### 7.8 UI layout - 내부 tab bar에서 `전체`가 선택된 상태로 표시된다. - 콘텐츠 타입 chip은 Figma 기준 수평 스크롤 형태를 따른다. - 콘텐츠 타입 chip은 `오디오`, `시리즈`, `오리지널`, `무료`, `포인트`만 표시한다. - Figma에 보이는 `전체` chip은 표시하지 않는다. - 콘텐츠 타입 chip 기본 선택은 `오디오`이다. - sort bar는 좌측에 현재 범위 label, 우측에 현재 sort label과 펼침 아이콘을 둔다. - sort menu에는 `추천순`을 표시하지 않는다. - 오디오/무료/포인트 grid는 Figma 오디오 상태와 동일한 3열 정사각형 썸네일 grid를 따른다. - 시리즈/오리지널 grid는 Figma 시리즈/오리지널 상태와 동일한 3열 세로형 썸네일 grid를 따른다. - response의 `totalCount` 또는 전체 개수 문구는 표시하지 않는다. - 하단 main tab bar와 mini player 영역은 기존 `MainView` 구조를 유지한다. - 빈 목록, 로딩, pagination loading 상태는 기존 V2 콘텐츠/추천 탭 패턴을 따른다. ### 7.9 Tab state preservation - 내부 `추천/랭킹/전체` 탭 전환 시 `전체` 탭 ViewModel 상태를 유지한다. - 유지 대상은 선택된 콘텐츠 타입, sort, dayOfWeek, 현재 page, hasNext, 로드된 `audios`/`series` 목록이다. - `전체` 탭으로 돌아왔을 때 사용자가 명시적으로 filter를 변경하지 않았다면 첫 페이지를 자동 재요청하지 않는다. ### 7.10 Navigation - 오디오 item 탭은 `audioContentId`로 기존 content detail 진입 흐름을 사용한다. - 시리즈 item 탭은 `seriesId`로 기존 series detail 진입 흐름을 사용한다. - 로그인/성인 콘텐츠 guard가 기존 상세 진입 흐름에 있다면 재사용한다. ## 8. UX / UI Expectations - 전체 배경은 기존 V2 dark UI 기준을 따른다. - 신규 사용자 노출 문자열은 `I18n`에 ko/en/ja로 추가한다. - Figma localhost asset URL은 앱 코드에 사용하지 않는다. - 타입 chip의 `전체`와 sort menu의 `추천순`은 표시하지 않는다. - 전체 개수 또는 `totalCount`는 표시하지 않는다. - 오디오 카드의 title은 `title`, subtitle은 `creatorNickname`을 표시한다. - 시리즈 카드의 title은 `title`, subtitle은 `creatorNickname`을 표시한다. - `MainContentAudioResponse.isOriginalSeries == true`이면 오디오 original tag를 표시한다. - `MainContentAudioResponse.isFirstContent == true`이면 first tag를 표시한다. - `MainContentAudioResponse.isPointAvailable == true`이면 point tag를 표시한다. - `MainContentAudioResponse.price == 0`이면 free tag를 표시한다. - `isAdult == true`이면 adult shield tag를 표시한다. - `MainContentSeriesResponse.isOriginal == true`이면 `ic_series_original` + `img_new_only` 조합의 `ONLY` tag를 표시한다. - 텍스트가 길면 Figma 기준 line limit로 말줄임 처리한다. ## 9. 재사용 가능한 V2 위젯 후보 - `SodaLive/Sources/V2/Main/Content/MainContentView.swift` - 콘텐츠 탭 root와 title bar, 내부 tab bar 구조를 확장할 후보. - `SodaLive/Sources/V2/Main/Content/MainContentTab.swift` - 내부 `전체` 탭 case 추가 후보. - `SodaLive/Sources/V2/Component/HomeTitleBar.swift` - 콘텐츠 탭 title bar 재사용 후보. - `SodaLive/Sources/V2/Component/TextTabBar.swift` - 내부 `추천/랭킹/전체` tab bar 재사용 후보. - `SodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swift` - `AUDIO`, `FREE`, `POINT` 타입 카드의 1차 재사용 후보. - `AudioContentThumbnailCardItem`에 `MainContentAudioResponse` adapter init 추가를 검토한다. - `SodaLive/Sources/V2/CreatorChannel/Series/Components/CreatorChannelSeriesListItem.swift` - `ic_series_original` + `img_new_only` tag와 세로형 시리즈 썸네일 스타일 참고 후보. - 현재 `CreatorChannelSeriesResponse` 전용이고 요일/진행/소장률 필드를 요구하므로 그대로 재사용하기보다 `MainContentSeriesResponse` 전용 thumbnail card 또는 adapter를 우선 검토한다. - `SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.swift` - `sort` query와 sort menu label 재사용 후보. - `SodaLive/Sources/Home/HomeTabViewModel.swift` - 기존 `SeriesPublishedDaysOfWeek` enum 위치 확인 대상. - `SodaLive/Sources/Content/Series/Main/DayOfWeek/SeriesMainDayOfWeekView.swift` - 현재 디바이스 요일 매핑과 요일 selector 구현 참고 대상. ## 10. Technical Constraints - 신규 구현이 이어질 경우 기능 변경은 `SodaLive/Sources/V2/Main/Content/**` 하위에서 해결한다. - API, Repository, ViewModel, Model은 내부 전체 탭 전용 경로로 분리한다. - 여러 화면에서 재사용할 명확한 근거가 있는 UI만 `SodaLive/Sources/V2/Component/**`로 승격한다. - 기존 legacy `SodaLive/Sources/Content/**` 화면을 리팩터링하지 않는다. - 기존 `MainContentRecommendation` API/ViewModel을 직접 확장하지 않는다. - 외부 라이브러리는 추가하지 않는다. - `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다. ## 11. Success Criteria - 콘텐츠 탭 내부 tab bar에 `전체` 탭이 추가되고 선택 가능하다. - 내부 `전체` 탭 최초 진입 시 `GET /api/v2/audio/contents?type=AUDIO&sort=LATEST&page=0&size=20` 요청이 발생한다. - 최초 진입 요청에는 `dayOfWeek`가 포함되지 않는다. - `SERIES` 타입 선택 시 현재 디바이스 요일이 선택되고, `dayOfWeek` query가 포함된다. - `SERIES` 외 타입 요청에는 `dayOfWeek`가 포함되지 않는다. - `AUDIO`, `FREE`, `POINT` 선택 시 response `audios`가 `AudioContentThumbnailCard` 기반 grid로 표시된다. - `SERIES`, `ORIGINAL` 선택 시 response `series`가 세로형 시리즈 grid로 표시된다. - Figma에 보이는 타입 chip `전체`는 표시되지 않는다. - sort menu에 `추천순`이 표시되지 않는다. - `FREE` 타입의 sort menu에는 `최신순`, `인기순`만 표시된다. - response의 `totalCount` 또는 전체 개수 문구가 표시되지 않는다. - 내부 `추천/랭킹/전체` 탭 전환 후 `전체` 탭으로 돌아오면 선택 타입, 정렬, 요일, 로드된 목록이 유지된다. - `hasNext == true`인 상태에서 목록 하단 도달 시 다음 page가 요청되고 기존 목록 뒤에 append된다. - 타입, 정렬, 요일 변경 시 page가 0으로 초기화되고 목록이 새 응답으로 교체된다. - `기타` 요일은 영어에서 `OTHER`, 일본어에서 `その他`로 표시된다. - 오디오 item 탭 시 오디오 상세로 이동하고, 시리즈 item 탭 시 시리즈 상세로 이동한다. ## 12. Open Questions - 현재 확정된 요구사항 기준으로 미결정 사항은 없다. ## 13. Decisions - Figma 타입 chip의 `전체`는 구현 대상에서 제외한다. - Figma sort menu의 `추천순`은 구현하지 않는다. - 내부 `추천/랭킹/전체` 탭 전환 시 `전체` 탭 상태를 유지한다. - `FREE` 타입의 sort menu는 `LATEST`, `POPULAR`만 제공한다. - response의 `totalCount`는 UI에 표시하지 않는다. ## 14. Verification Notes - 2026-07-06: `docs/agent-guides/documentation-policy.md`를 확인해 PRD-only 작업은 필요한 최소 문서만 작성해도 됨을 확인했다. - 2026-07-06: 기존 `docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md`, `plan-task.md`를 확인해 이번 작업이 추천 탭이 아닌 내부 전체 탭 별도 범위임을 확인했다. - 2026-07-06: Figma `get_design_context`와 `get_screenshot`으로 `35:5857`, `24:6909`, `24:9105`를 확인했다. - 2026-07-06: `AudioContentThumbnailCard`, `CreatorChannelSeriesListItem`, `ContentSort`, `SeriesPublishedDaysOfWeek`, `SeriesMainDayOfWeekView`, `I18n.Series`를 확인해 재사용 후보와 제약을 정리했다. - 2026-07-06: 사용자 결정 사항을 반영해 `전체` chip 제외, `추천순` 제외, 탭 전환 상태 유지, `FREE` sort 제한, `totalCount` 미표시를 확정했다.