From f3cbf42328f171f3f50ec9328876769ea6bf6509 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Mon, 6 Jul 2026 01:13:25 +0900 Subject: [PATCH] =?UTF-8?q?docs(content):=20=EC=B6=94=EC=B2=9C=20=ED=83=AD?= =?UTF-8?q?=20=EA=B3=84=ED=9A=8D=20=EB=AC=B8=EC=84=9C=EB=A5=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 455 ++++++++++++++++++ docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md | 172 +++++++ 2 files changed, 627 insertions(+) create mode 100644 docs/20260705_메인_콘텐츠_내부_추천_탭/plan-task.md create mode 100644 docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md diff --git a/docs/20260705_메인_콘텐츠_내부_추천_탭/plan-task.md b/docs/20260705_메인_콘텐츠_내부_추천_탭/plan-task.md new file mode 100644 index 00000000..36032d3b --- /dev/null +++ b/docs/20260705_메인_콘텐츠_내부_추천_탭/plan-task.md @@ -0,0 +1,455 @@ +# 메인 콘텐츠 탭 내부 추천 탭 구현 계획 + +## 기준 문서 + +- PRD: `docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md` +- 코드 스타일: `docs/agent-guides/code-style.md` +- 빌드/검증: `docs/agent-guides/build-test-verification.md` +- 문서 정책: `docs/agent-guides/documentation-policy.md` +- Figma 전체 화면: `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-6737&m=dev` +- Figma 전체 화면 node: `24:6737` +- Figma 배너: `24:6741` +- Figma 새로 올라온 오디오: `24:6751` +- Figma New&Hot: `24:6758` +- Figma 오직 보이스온에서만: `24:6745` +- Figma 추천 시리즈: `24:6770` (이번 범위 제외) +- Figma 무료 오디오: `24:6807` +- Figma 포인트 오디오: `24:6813` +- Figma 최근 댓글이 많은 오디오: `24:6820` +- Figma 키워드의 오디오: `24:6829` (이번 범위 제외) +- Figma 추천 오디오: `24:6842` + +## 요구사항 요약 + +- 메인 하단 `콘텐츠` 탭 안에 내부 `추천` 탭 화면을 구현한다. +- API endpoint는 `GET /api/v2/audio/recommendations`를 사용한다. +- title bar와 내부 tab bar는 고정하고, 배너부터 하위 콘텐츠만 세로 스크롤한다. +- Figma에 보이는 섹션별로 Phase를 분리한다. +- 이번 범위에서 `추천 시리즈`, `키워드의 오디오`는 구현하지 않는다. +- `V2` 패키지 하위에 구현된 재사용 가능한 위젯 후보를 먼저 확인하고 필요한 경우 최소 보완한다. +- `오직 보이스온에서만!` 섹션은 보이스온 전용 시리즈 표시를 사용한다. + - `ONLY` 표시는 `ic_series_original`과 `img_new_only` 조합을 우선 재사용한다. + - Figma localhost asset URL은 앱 코드에 넣지 않는다. + +## PRD 반영 기준 + +- PRD `7.1 API`, `7.2 Response model`은 Phase 2에서 처리한다. +- PRD `7.3 Fixed shell and scrolling`은 Phase 1에서 처리한다. +- PRD `7.4 Section visibility`는 Phase 2 ViewModel과 각 섹션 Phase에서 처리한다. +- PRD `7.5 구현 대상 섹션`은 Phase 3~10에서 Figma 노드 순서대로 처리한다. +- PRD `7.6 제외 대상 섹션`은 Phase 11에서 검증한다. +- PRD `7.7 Tag display`는 Phase 4의 `MainContentAudioTagOverlay`를 기준으로 이후 섹션에서 재사용한다. +- PRD `7.8 Navigation`은 Phase 3의 배너 callback 설계, Phase 6의 original series 탭, Phase 12의 전체 라우팅 검증에서 처리한다. +- PRD `8. UX / UI Expectations` 중 `최근 댓글이 많은 오디오` 댓글 preview bubble에는 nickname을 표시하지 않는다. +- PRD `10. Success Criteria`는 Phase 12의 빌드 검증과 각 Phase 검증 기준으로 확인한다. +- PRD `11. Open Questions`는 현재 `미결정 사항 없음`이므로 계획 문서에 구현 보류 항목을 남기지 않는다. + +## 접근안 검토 + +- 선택안: `SodaLive/Sources/V2/Main/Content/Recommendation/**` 하위에 API, 모델, ViewModel, 화면, 섹션 컴포넌트를 분리한다. + - 장점: 기존 `MainHomeRecommendation`, `MainHomeRanking`, `MainHomeFollowing` 구조와 가장 유사하고 `MainView`의 `.content` placeholder를 작게 교체할 수 있다. + - 단점: 기존 legacy `SodaLive/Sources/Content/**`와 별도 경로가 생긴다. +- 대안 1: legacy `SodaLive/Sources/Content/**` 하위에 구현한다. + - 장점: 기존 콘텐츠 전체/상세 화면과 가깝다. + - 단점: 신규 V2 우선 원칙과 맞지 않고 SwiftUI/V2 컴포넌트 재사용 흐름이 약해진다. +- 대안 2: `MainHomeRecommendation`의 컴포넌트를 직접 확장한다. + - 장점: 홈 추천 구현 일부를 바로 가져올 수 있다. + - 단점: 홈 추천과 콘텐츠 추천은 endpoint, 응답 모델, 섹션 구성이 달라 결합도가 커진다. + +## 구현 원칙 + +- 신규 코드는 `SodaLive/Sources/V2/Main/Content/Recommendation/**` 아래에 둔다. +- 여러 화면에서 재사용 근거가 있는 UI만 `SodaLive/Sources/V2/Component/**`로 승격한다. +- `MainView`는 `.content` 탭에서 신규 콘텐츠 탭 root를 보여주고, 상세 진입 guard callback만 전달한다. +- 콘텐츠 탭 root는 title bar와 내부 tab bar를 고정 영역으로 두고, 추천 탭 body 내부에서만 `ScrollView`를 사용한다. +- 섹션 배열이 비어 있으면 해당 섹션을 숨긴다. +- 전체 응답이 비어 있거나 API 실패/디코딩 실패 시 최소 empty 또는 error 상태를 표시한다. +- 오디오 상세 진입은 기존 `handleRecommendationContentTap(contentId:)`의 로그인/성인 콘텐츠 guard 흐름을 재사용한다. +- 배너 탭은 `eventItem`, `creatorId`, `seriesId`, `link` 매핑을 명시적으로 분기한다. +- `seriesId`는 `AppStep.seriesDetail(seriesId:)` route로 연결한다. +- `link`는 `AppDeepLinkHandler.handle(urlString:)`로 deep link를 먼저 확인하고, deep link가 아니면 `UIApplication.shared.open`으로 외부 브라우저를 연다. +- 외부 라이브러리는 추가하지 않는다. + +## API 응답 모델 기준 + +- `AudioRecommendationsResponse` + - `banners: [AudioBannerResponse]` + - `originalSeries: [OriginalSeriesResponse]` + - `latestAudios: [AudioCardResponse]` + - `newAndHotAudios: [AudioCardResponse]` + - `freeAudios: [AudioCardResponse]` + - `pointAudios: [AudioCardResponse]` + - `mostCommentedAudios: [CommentedAudioResponse]` + - `recommendedAudios: [AudioCardResponse]` +- `AudioBannerResponse` + - `imageUrl`, `eventItem`, `creatorId`, `seriesId`, `link` +- `OriginalSeriesResponse` + - `seriesId`, `coverImageUrl` +- `AudioCardResponse` + - `audioContentId`, `title`, `duration`, `imageUrl`, `price` + - `isAdult`, `isPointAvailable`, `isFirstContent`, `isOriginalSeries`, `creatorNickname` +- `CommentedAudioResponse` + - `audioContentId`, `title`, `imageUrl`, `latestCommentWriterProfileImageUrl` + +## 재사용 가능한 V2 위젯 후보 + +- `SodaLive/Sources/V2/Component/HomeTitleBar.swift` + - 콘텐츠 탭 title bar와 우측 액션 영역의 기준 후보. +- `SodaLive/Sources/V2/Component/TextTabBar.swift` + - 내부 `추천` 탭 bar 구성 후보. +- `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` + - 배너 carousel 후보. 현재 정사각형 높이 계산이 들어 있어 Figma의 `362x374` 비율과 맞는지 확인 후 최소 보완한다. +- `SodaLive/Sources/V2/Component/SectionTitle.swift` + - 섹션 제목과 우측 chevron 표시 후보. +- `SodaLive/Sources/V2/Component/AudioContentCard.swift` + - `새로 올라온 오디오`, `무료 오디오`, `포인트 오디오`, `추천 오디오` 카드 후보. 태그 overlay가 없으므로 썸네일 overlay 슬롯 추가 또는 새 wrapper 컴포넌트 검토가 필요하다. +- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelAudioContentListItem.swift` + - `New&Hot`, `최근 댓글이 많은 오디오` list row의 크기, 태그, 가격 표시 로직 참고 후보. +- `SodaLive/Sources/V2/CreatorChannel/Series/Components/CreatorChannelSeriesListItem.swift` + - `ic_series_original` + `img_new_only` 기반 `ONLY` 표시 참고 후보. +- `SodaLive/Resources/Assets.xcassets/v2/ic_series_original.imageset` + - 보이스온 전용 original 아이콘 asset. +- `SodaLive/Resources/Assets.xcassets/v2/img_new_only.imageset` + - 보이스온 전용 `ONLY` 표시 asset. + +## 주요 대상 파일 + +### 생성 + +- `SodaLive/Sources/V2/Main/Content/MainContentView.swift` +- `SodaLive/Sources/V2/Main/Content/MainContentTab.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationViewModel.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Models/AudioRecommendationsResponse.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationApi.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationRepository.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioBannerSection.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioListCarouselSection.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentVoiceOnOnlySection.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentMostCommentedAudioSection.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentRecommendedAudioGridSection.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioTagOverlay.swift` +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioEmptyStateView.swift` + +### 수정 + +- `SodaLive/Sources/V2/Main/MainView.swift` +- `SodaLive/Sources/I18n/I18n.swift` +- 필요 시 `SodaLive/Sources/V2/Component/AudioContentCard.swift` +- 필요 시 `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` +- 필요 시 `SodaLive.xcodeproj/project.pbxproj` + +### 확인 + +- `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift` +- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelAudioContentListItem.swift` +- `SodaLive/Sources/V2/CreatorChannel/Series/Components/CreatorChannelSeriesListItem.swift` +- `SodaLive/Sources/V2/Component/AudioContentCard.swift` +- `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` +- `SodaLive/Sources/V2/Component/SectionTitle.swift` +- `SodaLive/Sources/V2/Component/TextTabBar.swift` +- `SodaLive/Sources/App/AppDeepLinkHandler.swift` +- `SodaLive/Sources/App/AppStep.swift` +- `SodaLive/Sources/ContentView.swift` + +## TASK 체크리스트 + +### Phase 1: 기준점 점검과 콘텐츠 탭 shell 설계 + +- [ ] **Task 1.1: Figma와 기존 탭 구조 확인** + - 대상 파일: + - 확인: `docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md` + - 확인: `docs/20260705_메인_콘텐츠_내부_추천_탭/plan-task.md` + - 확인: `SodaLive/Sources/V2/Main/MainView.swift` + - 확인: `SodaLive/Sources/V2/Main/MainTab.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - 작업 내용: + - PRD의 Goals, Non-Goals, Core Requirements, Success Criteria를 다시 읽고 구현 범위를 확정한다. + - `.content` 탭이 현재 `MainPlaceholderTabView`인지 확인한다. + - 홈 탭의 title bar + tab bar + content 분리 구조를 참고한다. + - Figma `24:6737`에서 title bar와 tab bar가 고정이고 `24:6741` 배너부터 스크롤 대상임을 확인한다. + - 검증 기준: + - 실행 명령: `rg "GET /api/v2/audio/recommendations|추천 시리즈|키워드의 오디오|Open Questions|미결정 사항 없음|seriesDetail|AppDeepLinkHandler|nickname" docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md && sed -n '140,190p' SodaLive/Sources/V2/Main/MainView.swift && sed -n '1,120p' SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - 기대 결과: PRD의 확정 범위가 확인되고, `.content`는 placeholder이며, `MainHomeView`는 fixed shell과 tab content를 분리한다. + +- [ ] **Task 1.2: 내부 콘텐츠 탭 root 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/MainContentView.swift` + - 생성: `SodaLive/Sources/V2/Main/Content/MainContentTab.swift` + - 수정: `SodaLive/Sources/V2/Main/MainView.swift` + - 작업 내용: + - `MainContentTab`에 이번 범위의 내부 `recommendation` case를 둔다. + - `MainContentView`는 title bar와 내부 tab bar를 고정 영역으로 배치한다. + - 추천 탭 콘텐츠만 `MainContentRecommendationView`로 분리한다. + - `MainView`의 `.content` placeholder를 `MainContentView`로 교체한다. + - 검증 기준: + - 실행 명령: `rg "enum MainContentTab|struct MainContentView|MainContentRecommendationView|case \\.content" SodaLive/Sources/V2/Main` + - 기대 결과: 콘텐츠 탭 root와 추천 탭 분기가 확인되고, `.content` placeholder가 제거된다. + +### Phase 2: API, Repository, Response model, ViewModel + +- [ ] **Task 2.1: 추천 오디오 응답 모델 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Models/AudioRecommendationsResponse.swift` + - 작업 내용: + - 사용자 제공 Kotlin response class를 Swift `Decodable` 구조체로 대응한다. + - id가 있는 항목은 `Identifiable`을 채택하고 `Int` 또는 `Int64` 대신 기존 프로젝트 관례에 맞춰 `Int`를 우선 사용한다. + - `@JsonProperty("isAdult")`, `@JsonProperty("isPointAvailable")` 등 `is` prefix boolean은 Swift property명을 그대로 둔다. + - `EventItem` 타입은 기존 모델을 재사용한다. + - 검증 기준: + - 실행 명령: `rg "struct AudioRecommendationsResponse|struct AudioBannerResponse|struct OriginalSeriesResponse|struct AudioCardResponse|struct CommentedAudioResponse|isAdult|isPointAvailable|isFirstContent|isOriginalSeries" SodaLive/Sources/V2/Main/Content/Recommendation/Models/AudioRecommendationsResponse.swift` + - 기대 결과: API 응답 필드가 모두 Swift 모델에 반영된다. + +- [ ] **Task 2.2: 추천 오디오 API endpoint 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationApi.swift` + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationRepository.swift` + - 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift` + - 작업 내용: + - `MainContentRecommendationApi`를 `TargetType`으로 만든다. + - case는 `getAudioRecommendations` 하나만 둔다. + - path는 `/api/v2/audio/recommendations`, method는 `.get`, task는 `.requestPlain`로 둔다. + - headers는 기존 V2 홈 API와 동일하게 `Authorization: Bearer {token}` 패턴을 따른다. + - repository는 `getAudioRecommendations() -> AnyPublisher`를 제공한다. + - 검증 기준: + - 실행 명령: `rg "/api/v2/audio/recommendations|getAudioRecommendations|Authorization|AnyPublisher" SodaLive/Sources/V2/Main/Content/Recommendation/Repository` + - 기대 결과: 신규 endpoint와 repository 메서드가 추천 오디오 전용 경로에만 존재한다. + +- [ ] **Task 2.3: ViewModel 생성과 섹션 표시 상태 정리** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationViewModel.swift` + - 수정: `SodaLive/Sources/I18n/I18n.swift` + - 작업 내용: + - `MainContentRecommendationViewModel`을 `ObservableObject`로 만든다. + - `isLoading`, `isShowPopup`, `errorMessage`, `recommendations`, `hasLoaded` 상태를 둔다. + - `fetchRecommendations()`에서 `ApiResponse`를 디코딩한다. + - 섹션별 배열이 모두 비어 있으면 `I18n.MainContentRecommendation.emptyStateMessage`를 표시할 수 있게 한다. + - 실패 시 기존 V2 홈 추천과 같은 toast/popup 흐름 또는 empty state 중 하나를 선택하되, 사용자 노출 문구는 I18n에 ko/en/ja로 둔다. + - 검증 기준: + - 실행 명령: `rg "final class MainContentRecommendationViewModel|fetchRecommendations|ApiResponse|hasLoaded|emptyStateMessage|ko:|en:|ja:" SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationViewModel.swift SodaLive/Sources/I18n/I18n.swift` + - 기대 결과: API 디코딩, 로딩/실패/empty 상태, I18n ko/en/ja 문구가 확인된다. + +### Phase 3: 배너 섹션 + +- [ ] **Task 3.1: 배너 섹션 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioBannerSection.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` + - 작업 내용: + - Figma `24:6741` 기준으로 배너 carousel을 표시한다. + - `AudioBannerResponse.imageUrl`을 `BannerCarouselItem` 또는 신규 display model로 변환한다. + - page indicator는 `01 / 20` 형식을 유지한다. + - 배너 탭 시 `eventItem`, `creatorId`, `seriesId`, `link`를 상위 callback으로 전달한다. + - `seriesId`가 있으면 `seriesDetail` route로 이동할 수 있도록 callback을 설계한다. + - `link`가 있으면 deep link 처리 결과가 `true`인지 먼저 확인하고, 처리되지 않은 URL만 외부 브라우저로 연다. + - 기존 `BannerCarousel`이 정사각형 고정 높이를 강제하면, 콘텐츠 추천 배너 전용 wrapper에서 Figma 비율을 맞추거나 공용 컴포넌트를 최소 보완한다. + - 검증 기준: + - 실행 명령: `rg "MainContentAudioBannerSection|BannerCarousel|AudioBannerResponse|imageUrl|eventItem|creatorId|seriesId|link|seriesDetail|AppDeepLinkHandler|UIApplication.shared.open" SodaLive/Sources/V2/Main/Content/Recommendation SodaLive/Sources/V2/Main/MainView.swift SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` + - 기대 결과: 배너 섹션이 API 응답과 탭 callback을 연결한다. + +### Phase 4: 새로 올라온 오디오 섹션 + +- [ ] **Task 4.1: 가로 카드 섹션 공통 컴포넌트 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift` + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioTagOverlay.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/AudioContentCard.swift` + - 작업 내용: + - Figma `24:6751`의 `새로 올라온 오디오`를 `latestAudios`로 표시한다. + - `AudioContentCard(size: .medium)`를 우선 재사용한다. + - `isOriginalSeries`, `isFirstContent`, `isPointAvailable`, `price == 0`, `isAdult` 상태를 태그 overlay로 표시한다. + - 카드 탭 시 `onTapContent(audioContentId)`를 호출한다. + - 검증 기준: + - 실행 명령: `rg "MainContentAudioHorizontalCardSection|MainContentAudioTagOverlay|latestAudios|AudioContentCard|isOriginalSeries|isFirstContent|isPointAvailable|price == 0|isAdult|onTapContent" SodaLive/Sources/V2/Main/Content/Recommendation` + - 기대 결과: 최신 오디오 카드와 태그 overlay 조건이 확인된다. + +### Phase 5: New&Hot 섹션 + +- [ ] **Task 5.1: New&Hot list carousel 섹션 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioListCarouselSection.swift` + - 확인: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelAudioContentListItem.swift` + - 작업 내용: + - Figma `24:6758`의 `New&Hot`을 `newAndHotAudios`로 표시한다. + - 한 페이지에 3개 list row를 세로로 묶고, 묶음 단위로 가로 스크롤한다. + - row는 88pt 썸네일, 제목 한 줄, creator nickname 한 줄 구조를 따른다. + - `MainContentAudioTagOverlay`를 list thumbnail에도 재사용한다. + - row 탭 시 `onTapContent(audioContentId)`를 호출한다. + - 검증 기준: + - 실행 명령: `rg "MainContentAudioListCarouselSection|newAndHotAudios|chunk|MainContentAudioTagOverlay|onTapContent" SodaLive/Sources/V2/Main/Content/Recommendation` + - 기대 결과: New&Hot 섹션이 3-row 묶음 가로 carousel로 구성된다. + +### Phase 6: 오직 보이스온에서만 섹션 + +- [ ] **Task 6.1: 보이스온 전용 시리즈 섹션 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentVoiceOnOnlySection.swift` + - 확인: `SodaLive/Sources/V2/CreatorChannel/Series/Components/CreatorChannelSeriesListItem.swift` + - 확인: `SodaLive/Resources/Assets.xcassets/v2/ic_series_original.imageset` + - 확인: `SodaLive/Resources/Assets.xcassets/v2/img_new_only.imageset` + - 작업 내용: + - Figma `24:6745`의 `오직 보이스온에서만!`을 `originalSeries`로 표시한다. + - 이번 섹션은 `OriginalSeriesResponse.seriesId`, `coverImageUrl`만 사용한다. + - 카드 상단에는 `ic_series_original` + `img_new_only` 조합으로 `ONLY` 표시를 붙인다. + - `추천 시리즈` 카드형 상세 정보는 이번 범위에서 만들지 않는다. + - 카드 탭 시 `onTapSeries(seriesId)`를 호출하고 상위에서 `AppStep.seriesDetail(seriesId:)`로 이동한다. + - 검증 기준: + - 실행 명령: `rg "MainContentVoiceOnOnlySection|originalSeries|OriginalSeriesResponse|ic_series_original|img_new_only|onTapSeries|seriesDetail" SodaLive/Sources/V2/Main/Content/Recommendation SodaLive/Sources/V2/Main/MainView.swift SodaLive/Resources/Assets.xcassets/v2` + - 기대 결과: 보이스온 전용 `ONLY` 표시와 `seriesDetail` 진입 연결이 확인된다. + +### Phase 7: 무료 오디오 섹션 + +- [ ] **Task 7.1: 무료 오디오 섹션 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift` + - 확인: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift` + - 작업 내용: + - Figma `24:6807`의 `무료 오디오`를 `freeAudios`로 표시한다. + - `price == 0` 또는 API 섹션 성격에 따라 무료 태그를 표시한다. + - 섹션 배열이 비어 있으면 숨긴다. + - 검증 기준: + - 실행 명령: `rg "freeAudios|무료 오디오|MainContentAudioHorizontalCardSection" SodaLive/Sources/V2/Main/Content/Recommendation SodaLive/Sources/I18n/I18n.swift` + - 기대 결과: 무료 오디오 섹션이 View에 조립된다. + +### Phase 8: 포인트 오디오 섹션 + +- [ ] **Task 8.1: 포인트 오디오 섹션 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift` + - 확인: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift` + - 작업 내용: + - Figma `24:6813`의 `포인트 오디오`를 `pointAudios`로 표시한다. + - `isPointAvailable` 상태에 따라 포인트 태그를 표시한다. + - 섹션 배열이 비어 있으면 숨긴다. + - 검증 기준: + - 실행 명령: `rg "pointAudios|포인트 오디오|isPointAvailable|MainContentAudioHorizontalCardSection" SodaLive/Sources/V2/Main/Content/Recommendation SodaLive/Sources/I18n/I18n.swift` + - 기대 결과: 포인트 오디오 섹션과 포인트 태그 조건이 확인된다. + +### Phase 9: 최근 댓글이 많은 오디오 섹션 + +- [ ] **Task 9.1: 댓글 카드 carousel 섹션 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentMostCommentedAudioSection.swift` + - 작업 내용: + - Figma `24:6820`의 `최근 댓글이 많은 오디오`를 `mostCommentedAudios`로 표시한다. + - 오디오 row와 댓글 preview bubble을 하나의 카드로 묶고, 카드 단위로 가로 스크롤한다. + - `latestCommentWriterProfileImageUrl`을 댓글 작성자 profile image로 표시한다. + - 댓글 preview bubble에는 nickname을 표시하지 않는다. + - `CommentedAudioResponse`에는 creator nickname이 없으므로 댓글 영역에 nickname용 UI나 placeholder를 추가하지 않는다. + - 카드 탭 시 `onTapContent(audioContentId)`를 호출한다. + - 검증 기준: + - 실행 명령: `rg "MainContentMostCommentedAudioSection|mostCommentedAudios|CommentedAudioResponse|latestCommentWriterProfileImageUrl|onTapContent" SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentMostCommentedAudioSection.swift` + - 기대 결과: 최근 댓글 섹션은 댓글 작성자 profile image와 오디오 탭 callback을 사용한다. + - 실행 명령: `rg "nickname|creatorNickname" SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentMostCommentedAudioSection.swift` + - 기대 결과: 검색 결과가 없어야 한다. 댓글 bubble에는 nickname을 표시하지 않는다. + +### Phase 10: 추천 오디오 섹션 + +- [ ] **Task 10.1: 추천 오디오 grid 섹션 생성** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentRecommendedAudioGridSection.swift` + - 수정: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift` + - 작업 내용: + - Figma `24:6842`의 `추천 오디오`를 `recommendedAudios`로 표시한다. + - 2열 grid를 구성한다. + - 카드 크기는 부모 width 기반으로 계산하고, 고정 숫자 width만으로 화면을 제한하지 않는다. + - `MainContentAudioTagOverlay`를 재사용한다. + - grid item 탭 시 `onTapContent(audioContentId)`를 호출한다. + - 검증 기준: + - 실행 명령: `rg "MainContentRecommendedAudioGridSection|recommendedAudios|LazyVGrid|GridItem\\(\\.flexible|MainContentAudioTagOverlay|onTapContent" SodaLive/Sources/V2/Main/Content/Recommendation` + - 기대 결과: 추천 오디오 2열 grid와 태그 overlay가 확인된다. + +### Phase 11: 제외 섹션 미구현 확인 + +- [ ] **Task 11.1: 추천 시리즈와 키워드의 오디오 제외 확인** + - 대상 파일: + - 확인: `SodaLive/Sources/V2/Main/Content/Recommendation/**` + - 작업 내용: + - Figma `24:6770`의 `추천 시리즈` 컴포넌트는 생성하지 않는다. + - Figma `24:6829`의 `#키워드의 오디오` 컴포넌트는 생성하지 않는다. + - `originalSeries`는 `오직 보이스온에서만!` 섹션에만 사용한다. + - 검증 기준: + - 실행 명령: `rg "추천 시리즈|Keyword|키워드의 오디오|RecommendedSeries" SodaLive/Sources/V2/Main/Content/Recommendation` + - 기대 결과: 이번 범위 제외 섹션 구현 파일/문구가 검색되지 않는다. + +### Phase 12: 라우팅, 프로젝트 포함, 전체 검증 + +- [ ] **Task 12.1: 상세 진입 callback 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/MainView.swift` + - 확인: `SodaLive/Sources/App/AppStep.swift` + - 확인: `SodaLive/Sources/ContentView.swift` + - 작업 내용: + - 오디오 카드/list/grid 탭은 기존 content detail guard callback을 재사용한다. + - `eventItem` 배너는 기존 event detail 흐름이 있으면 재사용한다. + - creator 이동이 필요한 배너는 기존 creator detail guard callback을 재사용한다. + - `seriesId` 배너와 `originalSeries` 탭은 `AppStep.seriesDetail(seriesId:)` route로 연결한다. + - `link` 배너는 `AppDeepLinkHandler.handle(urlString:)`로 deep link를 먼저 체크한다. + - deep link가 아니면 `UIApplication.shared.open`으로 외부 브라우저를 연다. + - 검증 기준: + - 실행 명령: `rg "contentDetail|eventItem|eventDetail|creatorDetail|seriesDetail|AppDeepLinkHandler|UIApplication.shared.open|MainContentView" SodaLive/Sources/V2/Main/MainView.swift SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift` + - 기대 결과: 상세 진입과 event/creator/series route가 기존 route와 guard 흐름을 재사용하고, link는 deep link 우선 후 외부 브라우저로 처리한다. + +- [ ] **Task 12.2: 프로젝트 포함 및 빌드 검증** + - 대상 파일: + - 확인/보완: `SodaLive.xcodeproj/project.pbxproj` + - 확인: `SodaLive/Sources/V2/Main/Content/**` + - 작업 내용: + - 생성한 Swift 파일이 빌드 대상에 포함되는지 확인한다. + - 파일 시스템 동기화 방식으로 자동 포함되면 project file은 수정하지 않는다. + - 프로젝트 파일 수정이 필요할 때만 최소 변경한다. + - 검증 기준: + - 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj` + - 기대 결과: `OK` + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - 기대 결과: `BUILD SUCCEEDED` + +## 검증 기록 + +- 2026-07-05 문서 작성: + - 실행/확인: + - `docs/agent-guides/documentation-policy.md` + - `docs/agent-guides/build-test-verification.md` + - `docs/agent-guides/code-style.md` + - `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - `SodaLive/Sources/V2/Main/MainView.swift` + - `SodaLive/Sources/V2/Component/**` + - `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelAudioContentListItem.swift` + - `SodaLive/Sources/V2/CreatorChannel/Series/Components/CreatorChannelSeriesListItem.swift` + - Figma node `24:6737`, `24:6741`, `24:6751`, `24:6758`, `24:6745`, `24:6770`, `24:6807`, `24:6813`, `24:6820`, `24:6829`, `24:6842` + - 결과: + - 구현 대상/제외 대상 섹션을 Figma 기준으로 분리했다. + - 기존 V2 재사용 후보와 필요한 신규 파일 후보를 문서화했다. + - 이번 작업은 문서 작성만 수행했으므로 빌드/테스트는 실행하지 않았다. +- 2026-07-05 문서 보완: + - 실행/확인: + - `SodaLive/Sources/App/AppDeepLinkHandler.swift` + - `SodaLive/Sources/App/AppStep.swift` + - `SodaLive/Sources/ContentView.swift` + - `SodaLive/Sources/V2/Main/MainView.swift` + - Figma node `24:6820` + - 결과: + - 최근 댓글이 많은 오디오 댓글 bubble에는 nickname을 표시하지 않는 것으로 확정했다. + - `AudioBannerResponse.link`는 deep link 우선 처리 후 외부 브라우저를 여는 것으로 확정했다. + - `AudioBannerResponse.seriesId`, `OriginalSeriesResponse.seriesId`는 `seriesDetail` route로 이동하는 것으로 확정했다. + - 이번 작업은 문서 보완만 수행했으므로 빌드/테스트는 실행하지 않았다. +- 2026-07-05 PRD 기반 계획 재보완: + - 실행/확인: + - `docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md` + - `docs/20260705_메인_콘텐츠_내부_추천_탭/plan-task.md` + - 결과: + - PRD Core Requirements와 계획 Phase의 대응 관계를 추가했다. + - PRD에서 해소된 Open Questions가 계획 문서에 구현 보류 항목으로 남지 않게 정리했다. + - 최근 댓글이 많은 오디오 댓글 bubble에 nickname이 없어야 한다는 검증 기준을 분리했다. + - 이번 작업은 문서 보완만 수행했으므로 빌드/테스트는 실행하지 않았다. +- 2026-07-06 PRD 동기화 점검 후 계획 보완: + - 실행/확인: + - `docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md` + - `docs/20260705_메인_콘텐츠_내부_추천_탭/plan-task.md` + - 결과: + - 신규 사용자 노출 문구를 I18n ko/en/ja로 추가한다는 기준을 Task 2.3에 명시했다. + - 배너 `eventItem`의 기존 event detail 흐름 재사용 기준을 Phase 12 최종 라우팅 검증에 추가했다. + - 이번 작업은 문서 보완만 수행했으므로 빌드/테스트는 실행하지 않았다. diff --git a/docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md b/docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md new file mode 100644 index 00000000..777202fa --- /dev/null +++ b/docs/20260705_메인_콘텐츠_내부_추천_탭/prd.md @@ -0,0 +1,172 @@ +# PRD: 메인 콘텐츠 탭 내부 추천 탭 + +## 1. Overview +메인 하단 `콘텐츠` 탭 안의 내부 `추천` 탭에서 `GET /api/v2/audio/recommendations` 응답을 사용해 오디오 추천 화면을 제공한다. + +화면 상단 title bar와 내부 tab bar는 고정 영역으로 유지하고, Figma 기준 `배너`부터 하위 콘텐츠만 세로 스크롤한다. 신규 기능이므로 구현 대상 API, 모델, ViewModel, 화면, 섹션 컴포넌트는 `SodaLive/Sources/V2/Main/Content/Recommendation/**` 하위에 둔다. + +Figma 참조: +- 전체 화면: `24:6737`, `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-6737&m=dev` +- 배너: `24:6741` +- 새로 올라온 오디오: `24:6751` +- New&Hot: `24:6758` +- 오직 보이스온에서만: `24:6745` +- 추천 시리즈: `24:6770` (이번 범위 제외) +- 무료 오디오: `24:6807` +- 포인트 오디오: `24:6813` +- 최근 댓글이 많은 오디오: `24:6820` +- 키워드의 오디오: `24:6829` (이번 범위 제외) +- 추천 오디오: `24:6842` + +## 2. Problem +- 현재 V2 `MainView`의 `.content` 탭은 placeholder 상태라 메인 콘텐츠 탭에서 실제 추천 오디오 콘텐츠를 제공하지 못한다. +- 기존 홈 추천 탭은 크리에이터/커뮤니티/라이브 중심이고, 콘텐츠 탭 내부 추천 화면은 오디오 추천 API와 섹션 구성이 다르다. +- Figma 화면은 카드형, 리스트형, 댓글 카드형, grid형 등 섹션마다 레이아웃이 달라 단일 컴포넌트로 단순 렌더링하면 디자인과 맞지 않는다. +- 오디오 카드에는 `ONLY`, original audio, first, free, point, adult 등 여러 태그가 겹쳐 표시되므로 기존 V2 컴포넌트 재사용 범위를 먼저 정해야 한다. + +## 3. Goals +- 메인 하단 `콘텐츠` 탭에서 내부 `추천` 탭 화면을 표시한다. +- `GET /api/v2/audio/recommendations`를 호출하고 응답 데이터로 섹션을 구성한다. +- title bar와 내부 tab bar는 고정하고, 배너부터 하위 콘텐츠만 스크롤한다. +- Figma에 보이는 구현 대상 섹션을 섹션별로 분리한다. +- `banners`, `originalSeries`, `latestAudios`, `newAndHotAudios`, `freeAudios`, `pointAudios`, `mostCommentedAudios`, `recommendedAudios`를 응답 모델에 반영한다. +- `오직 보이스온에서만!` 섹션에는 `originalSeries`를 사용하고, `ic_series_original` + `img_new_only` 조합으로 `ONLY` 표시를 노출한다. +- `새로 올라온 오디오`, `무료 오디오`, `포인트 오디오`, `추천 오디오`는 기존 `AudioContentCard` 재사용 또는 최소 보완을 우선 검토한다. +- `New&Hot`, `최근 댓글이 많은 오디오`는 기존 `CreatorChannelAudioContentListItem`의 row/태그 표시 규칙을 참고한다. +- 오디오 item 탭 시 기존 content detail 진입 guard 흐름을 재사용한다. + +## 4. Non-Goals +- `추천 시리즈` 섹션은 이번 범위에서 구현하지 않는다. +- `#키워드의 오디오` 섹션은 이번 범위에서 구현하지 않는다. +- API 스펙에 없는 추천 시리즈 상세 카드, 키워드 추천 API, 키워드별 더보기 화면을 임의로 추가하지 않는다. +- 기존 홈 `추천`/`랭킹`/`팔로잉` 탭 구조를 변경하지 않는다. +- 기존 legacy `SodaLive/Sources/Content/**` 화면을 리팩터링하지 않는다. +- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다. +- 외부 라이브러리를 추가하지 않는다. +- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다. + +## 5. Target Users +- 메인 콘텐츠 탭에서 새 오디오와 추천 오디오를 빠르게 탐색하려는 사용자 +- 무료/포인트/신규/오리지널 여부를 보고 오디오 콘텐츠를 선택하려는 사용자 +- 댓글 반응이 많은 오디오를 확인하고 상세로 이동하려는 사용자 +- 보이스온 오리지널 시리즈를 별도 섹션에서 발견하려는 사용자 + +## 6. User Stories +- 사용자는 콘텐츠 탭의 추천 화면에서 최신 오디오를 가로 카드 목록으로 보고 싶다. +- 사용자는 `New&Hot` 오디오를 리스트 형태로 빠르게 훑어보고 싶다. +- 사용자는 보이스온에서만 제공되는 오리지널 시리즈를 `ONLY` 표시로 구분하고 싶다. +- 사용자는 무료 오디오와 포인트 사용 가능 오디오를 별도 섹션으로 탐색하고 싶다. +- 사용자는 최근 댓글이 많은 오디오와 최신 댓글 작성자 프로필을 함께 보고 싶다. +- 사용자는 추천 오디오를 2열 grid로 탐색하고 마음에 드는 콘텐츠 상세로 이동하고 싶다. + +## 7. Core Requirements + +### 7.1 API +- Method: `GET` +- Path: `/api/v2/audio/recommendations` +- 인증: 기존 V2 API 인증 헤더 패턴을 따른다. +- 응답 래퍼: 기존 관례대로 `ApiResponse` 디코딩을 우선한다. + +### 7.2 Response model +```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 +) +``` + +Swift 모델은 기존 V2 관례에 맞춰 `Decodable` 구조체로 선언한다. `audioContentId`, `seriesId`, `creatorId` 같은 Kotlin `Long` 대응 값은 프로젝트 기존 모델 관례에 맞춰 Swift `Int`를 우선 사용한다. + +### 7.3 Fixed shell and scrolling +- 콘텐츠 탭 title bar는 화면 상단 고정 영역이다. +- 내부 tab bar도 title bar 아래 고정 영역이다. +- Figma `24:6741` 배너부터 하위 콘텐츠만 세로 `ScrollView` 안에 둔다. +- 하단 main tab bar와 mini player 영역은 기존 `MainView` 구조를 유지한다. + +### 7.4 Section visibility +- 섹션별 배열이 비어 있으면 해당 섹션은 렌더링하지 않는다. +- 모든 구현 대상 섹션이 비어 있으면 empty state를 표시한다. +- API 실패 또는 디코딩 실패 시 사용자 노출 문구를 `I18n`에 둔다. + +### 7.5 구현 대상 섹션 +- 배너: `banners` +- 새로 올라온 오디오: `latestAudios` +- New&Hot: `newAndHotAudios` +- 오직 보이스온에서만: `originalSeries` +- 무료 오디오: `freeAudios` +- 포인트 오디오: `pointAudios` +- 최근 댓글이 많은 오디오: `mostCommentedAudios` +- 추천 오디오: `recommendedAudios` + +### 7.6 제외 대상 섹션 +- 추천 시리즈: Figma `24:6770` +- 키워드의 오디오: Figma `24:6829` + +`originalSeries`는 이번 범위에서 `오직 보이스온에서만!` 섹션에만 사용한다. 추천 시리즈 카드형 상세 UI는 만들지 않는다. + +### 7.7 Tag display +- `isOriginalSeries == true`이면 original audio/series 태그를 표시한다. +- `isFirstContent == true`이면 first 태그를 표시한다. +- `isPointAvailable == true`이면 point 태그를 표시한다. +- `price == 0`이면 free 태그를 표시한다. +- `isAdult == true`이면 adult shield 태그를 표시한다. +- `오직 보이스온에서만!`의 `ONLY` 표시는 `ic_series_original` + `img_new_only` asset 조합을 사용한다. + +### 7.8 Navigation +- 오디오 item 탭은 `audioContentId`로 기존 content detail 진입 흐름을 사용한다. +- 배너 탭은 `eventItem`, `creatorId`, `seriesId`, `link`를 명시적으로 분기한다. +- `eventItem`은 기존 event detail 흐름이 있으면 재사용한다. +- `creatorId`는 기존 creator detail guard callback을 재사용한다. +- `seriesId`는 `AppStep.seriesDetail(seriesId:)` route로 연결한다. +- `AudioBannerResponse.link`는 먼저 `AppDeepLinkHandler.handle(urlString:)`로 deep link 여부를 확인한다. +- `AudioBannerResponse.link`가 deep link로 처리되지 않으면 `UIApplication.shared.open`으로 외부 브라우저를 연다. + +## 8. UX / UI Expectations +- 전체 배경은 기존 V2 dark UI 기준을 따른다. +- 섹션 title은 Figma 텍스트를 우선 사용하되 신규 사용자 노출 문자열은 `I18n`에 둔다. +- 배너는 Figma `24:6741` 기준으로 가로 carousel과 `01 / 20` page indicator를 표시한다. +- 일반 오디오 카드 섹션은 가로 스크롤 카드 목록으로 표시한다. +- `New&Hot`은 3개 row를 한 묶음으로 구성하고 묶음 단위로 가로 스크롤한다. +- `최근 댓글이 많은 오디오`는 오디오 row와 댓글 preview bubble을 하나의 카드로 묶어 가로 스크롤한다. +- `최근 댓글이 많은 오디오`의 댓글 preview bubble에는 nickname을 표시하지 않는다. Figma `24:6820` 기준으로 `latestCommentWriterProfileImageUrl`과 댓글 텍스트 영역만 사용한다. +- `추천 오디오`는 2열 grid로 표시한다. +- 텍스트가 길면 한 줄 또는 Figma 기준 line limit로 말줄임 처리한다. +- 카드/row의 루트 레이아웃은 가능한 부모 width, grid column, aspect ratio 기반으로 구성한다. + +## 9. Technical Constraints +- 신규 기능 파일은 `SodaLive/Sources/V2/Main/Content/Recommendation/**` 하위에 둔다. +- 콘텐츠 탭 root는 `SodaLive/Sources/V2/Main/Content/**` 하위에 둔다. +- 여러 화면에서 재사용할 명확한 근거가 생긴 컴포넌트만 `SodaLive/Sources/V2/Component/**`로 승격한다. +- 기존 `MainHomeRecommendation` API/ViewModel을 직접 확장하지 않는다. +- 기존 `AudioContentCard`, `BannerCarousel`, `SectionTitle`, `TextTabBar`, `HomeTitleBar` 재사용을 먼저 검토한다. +- 신규 사용자 노출 문구는 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja로 추가한다. +- Figma localhost asset URL은 앱 코드에 사용하지 않는다. +- 외부 라이브러리는 추가하지 않는다. + +## 10. Success Criteria +- `.content` 탭에서 placeholder 대신 콘텐츠 탭 root가 표시된다. +- 콘텐츠 탭 내부 `추천` 탭 진입 시 `GET /api/v2/audio/recommendations` 호출이 발생한다. +- title bar와 내부 tab bar는 고정되고, 배너부터 하위 섹션만 세로 스크롤된다. +- API 응답의 구현 대상 배열이 각 Figma 섹션에 매핑된다. +- `추천 시리즈`, `#키워드의 오디오` 섹션은 구현되지 않는다. +- `오직 보이스온에서만!` 섹션은 `ic_series_original` + `img_new_only` 조합으로 `ONLY` 표시를 노출한다. +- 오디오 item 탭 시 기존 content detail guard 흐름을 통해 상세로 이동한다. +- `AudioBannerResponse.seriesId`와 `OriginalSeriesResponse.seriesId` 탭 시 `seriesDetail`로 이동한다. +- `AudioBannerResponse.link`는 deep link 우선 처리 후, deep link가 아니면 외부 브라우저로 열린다. +- 빈 섹션은 숨겨지고, 전체 empty/API 실패 상태는 사용자에게 안내된다. +- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`가 성공한다. + +## 11. Open Questions +- 현재 확정된 요구사항 기준으로 미결정 사항은 없다. + +## 12. Verification Notes +- 2026-07-05: Figma `get_design_context`와 `get_screenshot`으로 `24:6737`, `24:6741`, `24:6751`, `24:6758`, `24:6745`, `24:6770`, `24:6807`, `24:6813`, `24:6820`, `24:6829`, `24:6842`를 확인했다. +- 2026-07-05: `MainView`, `MainHomeView`, `AudioContentCard`, `BannerCarousel`, `CreatorChannelAudioContentListItem`, `CreatorChannelSeriesListItem`을 확인해 재사용 후보를 정리했다. +- 2026-07-05: 최초 문서 작성 시 `plan-task.md`만 작성되어 신규 기능 문서 정책과 맞지 않았다. 본 PRD를 추가해 정책 누락을 보완한다. +- 2026-07-05: 사용자 확인으로 댓글 preview bubble에는 nickname을 표시하지 않고, 배너 `link`는 deep link 우선 후 외부 브라우저로 열며, `seriesId`는 `seriesDetail` route로 이동하도록 확정했다.