# PRD: 메인 홈 추천 탭 UI와 API 연동 ## 1. Overview 메인 홈 화면의 `추천` 탭을 Figma 디자인 기준으로 구성하고, 신규 API `GET /api/v2/home/recommendations` 응답을 섹션별 UI에 연동한다. 기존 홈 API에 필드를 추가하지 않고 홈 추천 전용 API, Repository, ViewModel, 화면 조립 계층을 새로 만든다. `MainHomeView`는 홈 상단 공통 shell과 추천/랭킹/팔로잉 탭 전환만 담당하고, 탭 아래 콘텐츠는 `Recommendation`, `Ranking`, `Following` 하위 폴더의 전용 View로 분리한다. 이미 생성된 V2 공용 widget과 저장소 컴포넌트로 처리 가능한 UI는 재사용하고, 추천 탭에서만 쓰이는 조립 UI는 `SodaLive/Sources/V2/Main/Home/Recommendation/Components/**` 아래에 둔다. ## 2. Problem - 추천 탭 전용 데이터가 기존 홈 API와 섞이면 응답/화면 책임이 커지고 회귀 위험이 커진다. - Figma 추천 탭은 라이브, 배너, 최근 활동, 최근 데뷔, AI 캐릭터, 장르 크리에이터, 응원 크리에이터, 인기 커뮤니티, 사업자 정보 등 여러 독립 섹션으로 구성된다. - 섹션별 UI가 많으므로 한 화면 파일에 직접 구현하면 유지보수와 검증이 어려워진다. - `FeedCommunityView`는 추천 탭 요구에 맞춰 키워드 영역 제거, 이미지 표시, 유료/구매 상태 UI가 필요하다. - 사업자 정보는 외부 라이브러리 없이 최대 3줄 말줄임표, 더보기, 접기 동작을 제공해야 한다. ## 3. Goals - `추천` 탭 진입 시 `GET /api/v2/home/recommendations`를 호출하고 응답 데이터로 화면을 구성한다. - 기존 API에 추천 endpoint를 추가하지 않고 신규 `MainHomeApi`, Repository, ViewModel 계층을 만든다. - Figma 확인 시 이미 생성된 widget이나 기존 V2 공용 컴포넌트로 처리 가능한 부분은 재사용한다. - `MainHomeView`를 추천/랭킹/팔로잉 탭 shell로 유지하고, 추천 탭 콘텐츠는 `MainHomeRecommendationView`에서 조립한다. - 랭킹/팔로잉 탭은 `MainHomeRankingView`, `MainHomeFollowingView`로 별도 파일/폴더를 준비해 이후 구현이 MainHome shell에 누적되지 않게 한다. - 섹션별 UI를 작은 단위의 Custom Widget으로 분리한다. - `RecommendedActivityType` 서버 enum 값을 앱 enum으로 변환하고 I18n 문구로 표시한다. - 빈 데이터 섹션은 제목과 컨테이너를 표시하지 않는다. - 사업자 정보 섹션은 화면 width를 채우고, 기본 3줄 말줄임표와 더보기/접기 전환을 지원한다. - `FeedCommunityView`는 추천 탭 요구에 맞게 이미지와 유료 커뮤니티 포스트 UI를 지원한다. ## 4. Non-Goals - `추천 필모그래피` 섹션은 만들지 않는다. - `또 다른 모습` 섹션은 만들지 않는다. - 기존 홈 API 또는 기존 `HomeApi`에 `/api/v2/home/recommendations`를 추가하지 않는다. - 외부 라이브러리를 추가하지 않는다. - Figma 로컬 asset URL 또는 Figma 웹 URL을 앱 코드에 직접 사용하지 않는다. - 서버 응답 스펙에 없는 개별 팔로우 상태, 페이지네이션, 정렬/필터 기능은 이번 범위에 포함하지 않는다. - Figma `node-id=309-19775`의 `구매완료` 버튼은 구현하지 않는다. ## 5. Target Users - 앱 홈에서 추천 라이브, 크리에이터, 콘텐츠, 커뮤니티를 빠르게 탐색하는 사용자 - 최근 활동 타입과 추천 콘텐츠 상태를 현재 앱 언어와 디자인에 맞게 확인하려는 사용자 - 사업자 정보를 짧게 확인하고 필요할 때 전체 내용을 펼쳐 보려는 사용자 ## 6. User Stories - 사용자는 홈 `추천` 탭에서 현재 라이브 중인 크리에이터와 추천 콘텐츠를 한 화면에서 보고 싶다. - 사용자는 최근 활동한 크리에이터의 활동 타입을 `라이브`, `오디오`, `커뮤니티`처럼 이해 가능한 문구로 보고 싶다. - 사용자는 이미지가 포함된 커뮤니티 포스트와 유료/구매 상태를 카드에서 구분하고 싶다. - 사용자는 사업자 정보를 기본적으로 짧게 보고, 필요할 때 더보기/접기로 전환하고 싶다. - 사용자는 로그인이 필요하거나 민감 콘텐츠 제한이 필요한 상세 화면에 진입할 때 기존 홈과 같은 로그인/본인인증/콘텐츠 보기 설정 안내를 받고 싶다. ## 7. Core Requirements ### 7.1 추천 홈 데이터 조회 #### API - Method: `GET` - Path: `/api/v2/home/recommendations` - 기존 API에 추가하지 않고 신규 API 타입으로 만든다. - 인증: 기존 인증 헤더 패턴과 동일하게 `Authorization: Bearer {token}` 사용 - 응답 래퍼: 기존 관례대로 `ApiResponse` 디코딩 #### Response ```kotlin data class HomeRecommendationResponse( val lives: List, val banners: List, val recentlyActiveCreators: List, val recentDebutCreators: List, val aiCharacters: List, val genreCreators: List, val cheerCreators: List, val popularCommunityPosts: List ) data class HomeLiveItem( val roomId: Long, val creatorNickname: String, val creatorProfileImage: String ) data class HomeActiveCreatorItem( val creatorNickname: String, val creatorProfileImage: String, val activityType: String, val activityAt: String, val targetId: Long? ) data class HomeCreatorItem( val creatorId: Long, val creatorNickname: String, val creatorProfileImage: String ) data class HomeAiCharacterItem( val characterId: Long, val creatorId: Long, val name: String, val description: String, val profileImage: String?, val totalChatCount: Long, val originalWorkTitle: String? ) data class HomeGenreCreatorGroupItem( val genreName: String, val creators: List ) #### RecommendationBannerResponse ```kotlin data class RecommendationBannerResponse( val imageUrl: String, val eventItem: EventItem?, val creatorId: Long?, val seriesId: Long?, val link: String? ) ``` 배너 탭 이동은 `eventItem`, `creatorId`, `seriesId`, `link` 순서로 첫 non-null 값을 사용한다. - `eventItem` -> 이벤트 상세 - `creatorId` -> 크리에이터 상세 - `seriesId` -> 시리즈 상세 - `link` -> 외부 URL 열기 data class HomePopularCommunityPostItem( val postId: Long, val creatorId: Long, val creatorNickname: String, val creatorProfileImage: String?, val imageUrl: String?, val audioUrl: String?, val content: String, val price: Int, val createdAt: String, val likeCount: Long, val commentCount: Long, val existOrdered: Boolean ) ``` Swift 모델은 위 필드명을 우선 기준으로 `Decodable`을 구성한다. 서버 nullable 필드는 Swift optional로 선언한다. 현재 코드에 과거 추정 필드명이 이미 있다면 신규 스펙 필드명을 추가하거나 보정하고, 실제 사용 UI는 신규 스펙 필드명을 기준으로 매핑한다. ### 7.2 활동 타입 다국어 표시 서버 enum: ```kotlin enum class RecommendedActivityType(val code: String) { LIVE("LIVE"), AUDIO("AUDIO"), COMMUNITY("COMMUNITY"), LIVE_REPLAY("LIVE_REPLAY") } ``` 앱 변환: - `LIVE`, `LIVE_REPLAY` -> `I18n.HomeRecommendation.activityLive` - `AUDIO` -> `I18n.HomeRecommendation.activityAudio` - `COMMUNITY` -> `I18n.HomeRecommendation.activityCommunity` - 알 수 없는 값은 서버 코드를 그대로 노출하지 않고 보조 문구를 숨긴다. 다국어 기본 문구: - ko: `라이브`, `오디오`, `커뮤니티` - en: `Live`, `Audio`, `Community` - ja: `ライブ`, `オーディオ`, `コミュニティ` ### 7.3 추천 탭 상세 진입 guard 기본 방향: - 추천 API 호출과 추천 탭 섹션 노출 자체는 로그인/본인인증/민감 콘텐츠 보기 설정으로 막지 않는다. - guard는 라이브, 크리에이터, 오디오 콘텐츠, AI 캐릭터, 커뮤니티 등 사용자가 상세 화면으로 진입하는 탭 액션 앞에서 적용한다. - 신규 `MainHomeView`에 Bootpay 인증 UI와 인증 상태를 중복 구현하지 않고, V2 `MainView`의 기존 `token`, `auth`, `isShowAuthView`, `isShowAuthConfirmView`, `pendingAction`, `authConfirmDialog`, `authView` 인프라를 재사용한다. 진입 규칙: - 토큰이 비어 있으면 `AppState.shared.setAppStep(step: .login)`으로 로그인 화면으로 이동한다. - 민감/성인 콘텐츠 진입이 필요한 항목은 한국 사용자이고 `auth == false`이면 원래 이동 액션을 `pendingAction`에 저장하고 본인인증 안내 dialog를 표시한다. - 본인인증 성공 후 `pendingAction`을 실행해 원래 상세 화면으로 이동한다. - 민감 콘텐츠 보기 설정이 꺼져 있으면 `AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)`로 안내 문구를 예약하고 `AppState.shared.setAppStep(step: .contentViewSettings)`로 이동한다. - 위 조건을 통과한 경우에만 `liveDetail`, `creatorDetail`, `contentDetail`, `characterDetail` 등 실제 상세 이동을 실행한다. - 추천 탭 현재 라이브 아이템은 `roomId`가 항상 존재하는 값으로 취급하고, 아이템 탭 시 상세 화면을 거치지 않고 기존 `LiveViewModel.enterLiveRoom(roomId:)` 흐름을 호출해 방 상세 조회, 입장 전 결제 확인, 비밀방 비밀번호 dialog, `LiveRoomViewV2` 입장을 재사용한다. 라이브 항목 주의사항: - 기존 홈의 라이브 guard는 `roomId`와 `isAdult`를 함께 받아 성인 여부를 판단한다. - 현재 `HomeLiveItem` 신규 응답 스펙에는 `isAdult`가 없으므로, 라이브 성인 guard를 앱에서 적용하려면 서버 응답에 `isAdult` 또는 동등한 필드가 필요하다. - 해당 필드가 없으면 앱에서 성인 여부를 임의 추정하지 않고, 서버 필터링 전제인지 확인한다. ### 7.4 FeedCommunityView 참조 Figma: - 유료이고 구매하지 않은 UI: `node-id=309-19774` - 유료인데 구매함 또는 무료 UI: `node-id=309-19775` 요구사항: - 키워드 영역을 제거한다. - 커뮤니티 포스트 이미지를 추가한다. - `imageUrl == nil`이면 이미지 영역 없이 본문 중심으로 표시한다. - `imageUrl != nil && price > 0 && existOrdered == false`이면 유료 미구매 상태 UI를 표시한다. - `imageUrl != nil && (price <= 0 || existOrdered == true)`이면 이미지를 일반 노출한다. - `node-id=309-19775`의 `구매완료` 버튼은 뺀다. - 작성자, 본문, 생성 시간, 좋아요 수, 댓글 수를 표시한다. - `audioUrl`은 이번 추천 탭 카드에서 표시 대상이 확정되지 않았으므로 재생 UI를 추가하지 않는다. ### 7.5 사업자 정보 더보기/접기 요구사항: - UI 가장 마지막에 사업자 정보 섹션을 배치한다. - width는 화면에 채운다. - 외부 라이브러리를 사용하지 않는다. - 기본 상태는 최대 3줄 표시, 말줄임표 적용, `더보기` 액션 제공 - `더보기` 터치 시 전체 표시로 전환하고 `접기` 액션 제공 - `접기` 터치 시 다시 3줄 말줄임표 상태로 돌아간다. - 실제 텍스트가 3줄 이하이면 더보기/접기 버튼은 숨긴다. 구현 방향: - SwiftUI `Text`와 `lineLimit(isExpanded ? nil : 3)` 기반으로 구현한다. - 줄 수 판정은 제한/무제한 높이 비교 또는 Geometry 기반 측정 방식을 사용한다. - 사업자 정보 wrapper는 `MainHomeBusinessInfoSection`, 재사용 가능한 텍스트 UI는 `ExpandableTextView`로 분리한다. ## 8. UX / UI Expectations ### 8.1 Figma 기준 - 추천 화면: `node-id=24-5514` - FeedCommunityView 유료 미구매: `node-id=309-19774` - FeedCommunityView 유료 구매함 또는 무료: `node-id=309-19775` Figma 확인 시 이미 생성해 둔 widget 또는 저장소 내 공용 컴포넌트로 구현 가능한 UI는 재사용한다. 단, Figma asset URL을 코드에 직접 넣지 않고 프로젝트 asset 또는 기존 이미지 로딩 패턴을 사용한다. ### 8.2 화면 구성 아래 도식은 구현 대상만 포함한다. `추천 필모그래피`, `또 다른 모습`은 제외한다. ```text MainHomeView └─ HomeTitleBar 재사용 └─ 홈 상단 탭(추천/랭킹/팔로잉) └─ 선택된 탭 콘텐츠 ├─ MainHomeRecommendationView │ └─ ScrollView │ ├─ 현재 라이브 섹션 │ ├─ 배너 섹션 │ ├─ 최근 활동 크리에이터 섹션 │ ├─ 최근 데뷔한 크리에이터 섹션 │ ├─ AI 캐릭터 섹션 │ ├─ 장르의 크리에이터 섹션 │ ├─ 최근 응원이 많은 크리에이터 섹션 │ ├─ 인기 커뮤니티 섹션 │ └─ 사업자 정보 섹션 ├─ MainHomeRankingView └─ MainHomeFollowingView └─ MainTabBarView는 기존 MainView 구조에서 유지 ``` ### 8.3 재사용 컴포넌트 후보 - `SodaLive/Sources/V2/Component/HomeTitleBar.swift` - `SodaLive/Sources/V2/Component/SectionTitle.swift` - `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` - `SodaLive/Sources/V2/Component/Card/AiCharacterCard.swift` - `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` - `SodaLive/Sources/V2/Component/Creator/CreatorProfileGrid.swift` - `SodaLive/Sources/V2/Component/Creator/CreatorProfileItem.swift` - `SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift` 이미 존재하는 컴포넌트가 Figma 요구를 일부만 충족하면 새 컴포넌트를 중복 생성하지 않고 필요한 범위만 보완한다. ### 8.4 컴포넌트 위치 기준 - 홈 상단 공통 shell: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` - 추천 탭 루트: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` - 추천 탭 API, Repository, ViewModel, 모델: `SodaLive/Sources/V2/Main/Home/Recommendation/**` - 추천 탭에서만 쓰는 섹션 wrapper: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/**` - 랭킹 탭 루트: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift` - 팔로잉 탭 루트: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift` - 여러 페이지에서 재사용 가능한 Card/Banner/Text/Button/Creator widget: `SodaLive/Sources/V2/Component/**` - 기존 로직 수정이 아닌 신규 View/ViewModel/Repository는 `SodaLive/Sources/V2/**` 아래에 둔다. ## 9. Technical Constraints - 앱 소스 변경은 `SodaLive/Sources/**`에서 수행한다. - `Pods/**`, `generated/**`, `build/**`는 직접 수정하지 않는다. - 기존 `HomeApi`에 추천 API를 추가하지 않는다. - 외부 라이브러리를 추가하지 않는다. - 이미지 로딩은 기존 앱 이미지 컴포넌트/패턴을 따른다. - 인증 헤더는 기존 `UserDefaultsKey.token` 기반 패턴을 따른다. - 서버 `Long` 값은 Swift에서 기존 라우팅/모델 관례와 맞는 `Int` 또는 `Int64`로 사용한다. - 사용자 노출 문구는 가능한 `I18n`에 추가한다. - 상세 진입 guard는 `MainHomeView` 내부에 인증 UI를 중복 구현하지 않고, V2 `MainView`의 기존 인증/민감 콘텐츠 guard 인프라를 재사용한다. - `MainHomeView`에는 추천 탭의 섹션 조립을 직접 누적하지 않고, 탭별 콘텐츠 View를 조합하는 역할만 둔다. - Open Questions에 남긴 항목은 임의 구현하지 않고 확인 후 반영한다. ## 10. Success Criteria - 추천 탭에서 `GET /api/v2/home/recommendations`를 호출하고 `success == true` 응답 데이터를 섹션별로 렌더링한다. - 응답 배열이 비어 있는 섹션은 화면에 표시하지 않는다. - `추천 필모그래피`, `또 다른 모습`은 코드와 화면에 포함하지 않는다. - `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY` 활동 타입이 I18n 문구로 표시된다. - `FeedCommunityView`에서 키워드 영역이 제거되고, 이미지/유료 미구매/유료 구매함 또는 무료 상태가 조건에 맞게 표시된다. - `FeedCommunityView`에 `구매완료` 버튼이 표시되지 않는다. - 사업자 정보는 마지막 섹션에서 화면 width를 채우고, 기본 3줄 말줄임표, 더보기, 전체 표시, 접기 전환이 동작한다. - 상세 진입 탭 액션은 기존 홈과 같은 로그인, 한국 사용자 본인인증, 민감 콘텐츠 보기 설정 guard를 통과한 뒤에만 실행된다. - `MainHomeView`는 추천/랭킹/팔로잉 탭 shell 역할만 하고, 추천 탭 섹션 조립은 `MainHomeRecommendationView`에 분리되어 있다. - 반복 UI는 기존 widget 또는 공용 컴포넌트를 재사용하고 불필요하게 중복 구현하지 않는다. - 빌드가 성공하고, 가능하면 ViewModel 디코딩/상태 전환 검증을 수행한다. ## 11. Open Questions - `RecommendationBannerResponse`는 변경된 백엔드 스펙 기준으로 `eventItem`, `creatorId`, `seriesId`, `link` 순서의 이동 규칙을 사용한다. - 각 섹션별 최대 표시 개수와 가로/세로 스크롤 정책이 Figma 기준 그대로인지, 서버 응답 전체를 모두 표시해야 하는지 확인이 필요하다. - `HomeLiveItem`에 민감/성인 라이브 여부를 판단할 `isAdult` 또는 동등한 필드가 추가되는지, 아니면 서버에서 추천 라이브를 사전 필터링하는지 확인이 필요하다. - 커뮤니티 `audioUrl`을 추천 탭 카드에서 표시/재생해야 하는지 확인이 필요하다. - 각 카드 터치 시 상세 이동 대상이 모두 정의되어 있는지 확인이 필요하다. ## 12. Verification Plan - 문서 검증: API URL, 응답 모델, 제외 섹션, FeedCommunityView, 사업자 정보 요구가 PRD와 계획 문서에 반영되었는지 확인한다. - 정적 검증: 신규 추천 API가 기존 `HomeApi`에 추가되지 않았고, Figma URL이 앱 코드에 직접 포함되지 않았는지 확인한다. - 빌드 검증: `docs/agent-guides/build-test-verification.md` 기준으로 가능한 iOS 빌드 또는 최소 정적 검증을 실행한다. - 기능 검증: 추천 API 성공/실패, 빈 섹션, 활동 타입 I18n, 상세 진입 guard, 커뮤니티 카드 상태, 사업자 정보 더보기/접기를 확인한다.