18 KiB
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<HomeRecommendationResponse>디코딩
Response
data class HomeRecommendationResponse(
val lives: List<HomeLiveItem>,
val banners: List<RecommendationBannerResponse>,
val recentlyActiveCreators: List<HomeActiveCreatorItem>,
val recentDebutCreators: List<HomeCreatorItem>,
val aiCharacters: List<HomeAiCharacterItem>,
val genreCreators: List<HomeGenreCreatorGroupItem>,
val cheerCreators: List<HomeCreatorItem>,
val popularCommunityPosts: List<HomePopularCommunityPostItem>
)
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<HomeCreatorItem>
)
#### 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 isLiked: Boolean, 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.activityLiveAUDIO->I18n.HomeRecommendation.activityAudioCOMMUNITY->I18n.HomeRecommendation.activityCommunity- 알 수 없는 값은 서버 코드를 그대로 노출하지 않고 보조 문구를 숨긴다.
다국어 기본 문구:
- ko:
라이브,오디오,커뮤니티 - en:
Live,Audio,Community - ja:
ライブ,オーディオ,コミュニティ
7.3 추천 탭 상세 진입 guard
기본 방향:
- 추천 API 호출과 추천 탭 섹션 노출 자체는 로그인/본인인증/민감 콘텐츠 보기 설정으로 막지 않는다.
- guard는 라이브, 크리에이터, 오디오 콘텐츠, AI 캐릭터, 커뮤니티 등 사용자가 상세 화면으로 진입하는 탭 액션 앞에서 적용한다.
- 신규
MainHomeView에 Bootpay 인증 UI와 인증 상태를 중복 구현하지 않고, V2MainView의 기존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:
- 인기 커뮤니티 섹션:
node-id=24-5645 - Text Only Feed:
node-id=567-17875 - Text + Image Feed:
node-id=567-17878
요구사항:
- 키워드 영역을 제거한다.
- 인기 커뮤니티 섹션은 가로 캐러셀이 아니라 세로 피드 목록으로 배치한다.
- 커뮤니티 포스트 이미지를 추가한다.
imageUrl == nil이면 이미지 영역 없이 본문 중심으로 표시한다.imageUrl != nil && price > 0 && existOrdered == false이면 유료 미구매 상태 UI를 표시한다.imageUrl != nil && (price <= 0 || existOrdered == true)이면 이미지를 일반 노출한다.audioUrl이 있으면 이미지 중앙에 재생/일시정지 버튼을 표시하고 기존 커뮤니티 오디오 재생 흐름을 재사용한다.- 유료 미구매 커뮤니티 포스트는 기존 커뮤니티 구매 API로 구매 가능해야 한다.
구매완료버튼은 표시하지 않는다.- 작성자, 본문, 생성 시간, 좋아요 수, 댓글 수를 표시한다.
- 생성 시간은 UTC 값을 디바이스 Timezone 기준 상대 날짜로 표시한다.
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 화면 구성
아래 도식은 구현 대상만 포함한다. 추천 필모그래피, 또 다른 모습은 제외한다.
MainHomeView
└─ HomeTitleBar 재사용
└─ 홈 상단 탭(추천/랭킹/팔로잉)
└─ 선택된 탭 콘텐츠
├─ MainHomeRecommendationView
│ └─ ScrollView
│ ├─ 현재 라이브 섹션
│ ├─ 배너 섹션
│ ├─ 최근 활동 크리에이터 섹션
│ ├─ 최근 데뷔한 크리에이터 섹션
│ ├─ AI 캐릭터 섹션
│ ├─ 장르의 크리에이터 섹션
│ ├─ 최근 응원이 많은 크리에이터 섹션
│ ├─ 인기 커뮤니티 섹션
│ └─ 사업자 정보 섹션
├─ MainHomeRankingView
└─ MainHomeFollowingView
└─ MainTabBarView는 기존 MainView 구조에서 유지
8.3 재사용 컴포넌트 후보
SodaLive/Sources/V2/Component/HomeTitleBar.swiftSodaLive/Sources/V2/Component/SectionTitle.swiftSodaLive/Sources/V2/Component/Banner/BannerCarousel.swiftSodaLive/Sources/V2/Component/Card/AiCharacterCard.swiftSodaLive/Sources/V2/Component/Card/CommunityPostCard.swiftSodaLive/Sources/V2/Component/Creator/CreatorProfileGrid.swiftSodaLive/Sources/V2/Component/Creator/CreatorProfileItem.swiftSodaLive/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를 중복 구현하지 않고, V2MainView의 기존 인증/민감 콘텐츠 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, 커뮤니티 카드 상태, 사업자 정보 더보기/접기를 확인한다.