chore(home): 추천 홈 기준을 정리한다

This commit is contained in:
Yu Sung
2026-06-26 19:23:53 +09:00
parent 8d6e32f5fb
commit 6b5176eba7
4 changed files with 626 additions and 520 deletions

View File

@@ -1,48 +1,61 @@
# PRD: 메인 홈 추천 UI와 API 연동
# PRD: 메인 홈 추천 UI와 API 연동
## 1. Overview
메인 홈의 `추천` 탭을 Figma 디자인 기준으로 구성하고, 신규 홈 추천 API(`/api/v2/home/recommendations`)와 모두 팔로우 API(`/api/v2/home/recommendations/creators/follow`)를 연동한다.
메인 홈 화면`추천` 탭을 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에 추천 탭 전용 응답을 추가하지 않고 신규 V2 API로 분리해야 한다.
- Figma 기준 추천 탭에는 라이브, 배너, 최근 활동 크리에이터, 데뷔/첫 오디오/AI 캐릭터/장르/응원/커뮤니티/사업자 정보 등 여러 반복 UI가 포함되어 있다.
- 반복 UI가 많아 단일 화면에 직접 구현하면 유지보수 비용이 커진다.
- 사업자 정보 텍스트는 외부 라이브러리 없이 3줄 말줄임표와 더보기/접기 토글을 제공해야 한다.
- 추천 탭 전용 데이터가 기존 홈 API와 섞이면 응답/화면 책임이 커지고 회귀 위험이 커진다.
- Figma 추천 탭 라이브, 배너, 최근 활동, 최근 데뷔, 첫 오디오, AI 캐릭터, 장르 크리에이터, 응원 크리에이터, 인기 커뮤니티, 사업자 정보 등 여러 독립 섹션으로 구성된다.
- 섹션별 UI가 많으므로 한 화면 파일에 직접 구현하면 유지보수와 검증이 어려워진다.
- `FeedCommunityView`는 추천 탭 요구에 맞춰 키워드 영역 제거, 이미지 표시, 유료/구매 상태 UI가 필요하다.
- `AudioContentCardView`는 첫 오디오 콘텐츠 응답의 `isPointAvailable`와 콘텐츠 상태에 따라 태그 표시 조건을 명확히 분기해야 한다.
- 사업자 정보는 외부 라이브러리 없이 최대 3줄 말줄임표, 더보기, 접기 동작을 제공해야 한다.
## 3. Goals
- `추천` 탭 진입 시 `/api/v2/home/recommendations`를 호출하고 응답 데이터로 화면을 구성한다.
- 기존 `HomeApi`에 케이스를 추가하지 않고 신규 API/Repository/ViewModel 계층을 만든다.
- Figma에서 확인한 기존 widget과 저장소 내 V2 공용 컴포넌트 가능한 범위에서 재사용한다.
- 반복되는 UI는 Custom Widget으로 분리해 재사용 가능하게 한다.
- 모두 팔로우 API 호출이 정상 완료되고 `success == true`이면 버튼 문구를 `모두 팔로우 완료`로 변경한다.
- `RecommendedActivityType` 서버 enum 값은 앱 enum으로 변환하고 다국어 문구로 표시한다.
- 빈 섹션 데이터는 제목과 빈 컨테이너를 노출하지 않아 화면 밀도를 유지한다.
- `추천` 탭 진입 시 `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를 지원한다.
- `AudioContentCardView`는 point/original/free/FIRST 시각요소를 조건에 맞게 표시하고 그 외 상태는 숨긴다.
## 4. Non-Goals
- `추천 필모그래피` 섹션은 만들지 않는다.
- `또 다른 모습` 섹션은 만들지 않는다.
- 기존 홈 API(`/api/home`) 또는 기존 `HomeApi`추천 API를 추가하지 않는다.
- 기존 홈 API 또는 기존 `HomeApi``/api/v2/home/recommendations`를 추가하지 않는다.
- 외부 라이브러리를 추가하지 않는다.
- 서버 응답 스펙에 없는 팔로우 개별 상태, 페이지네이션, 정렬/필터 기능은 이번 범위에 포함하지 않는다.
- Figma 로컬 asset URL을 앱 코드에 직접 사용하지 않는다.
- Figma 로컬 asset URL 또는 Figma 웹 URL을 앱 코드에 직접 사용하지 않는다.
- 서버 응답 스펙에 없는 개별 팔로우 상태, 페이지네이션, 정렬/필터 기능은 이번 범위에 포함하지 않는다.
- Figma `node-id=309-19775``구매완료` 버튼은 구현하지 않는다.
## 5. Target Users
- 앱 홈에서 추천 크리에이터, 라이브, 콘텐츠, 커뮤니티를 빠르게 탐색하는 일반 사용자
- 추천된 크리에이터 그룹을 한 번에 팔로우하려는 사용자
- 앱 홈에서 추천 라이브, 크리에이터, 콘텐츠, 커뮤니티를 빠르게 탐색하는 사용자
- 최근 활동 타입과 추천 콘텐츠 상태를 현재 앱 언어와 디자인에 맞게 확인하려는 사용자
- 사업자 정보를 짧게 확인하고 필요할 때 전체 내용을 펼쳐 보려는 사용자
## 6. User Stories
- 사용자는 홈 `추천` 탭에서 현재 라이브 중인 크리에이터와 추천 콘텐츠를 한 화면에서 보고 싶다.
- 사용자는 관심 있는 장르/응원 크리에이터 그룹을 한 번에 팔로우하고 싶다.
- 사용자는 사업자 정보를 기본적으로 짧게 보고, 필요할 때 전체 내용을 펼쳐 보고 싶다.
- 사용자는 최근 활동 타입을 한국어/영어/일본어 등 현재 앱 언어에 맞게 보고 싶다.
- 사용자는 최근 활동한 크리에이터의 활동 타입을 `라이브`, `오디오`, `커뮤니티`처럼 이해 가능한 문구로 보고 싶다.
- 사용자는 이미지가 포함된 커뮤니티 포스트와 유료/구매 상태를 카드에서 구분하고 싶다.
- 사용자는 첫 오디오 콘텐츠 카드에서 포인트 가능 여부, 오리지널 작품, 무료 여부, 첫 콘텐츠 여부를 시각적으로 구분하고 싶다.
- 사용자는 사업자 정보를 기본적으로 짧게 보고, 필요할 때 더보기/접기로 전환하고 싶다.
- 사용자는 로그인이 필요하거나 민감 콘텐츠 제한이 필요한 상세 화면에 진입할 때 기존 홈과 같은 로그인/본인인증/콘텐츠 보기 설정 안내를 받고 싶다.
## 7. Core Features
## 7. Core Requirements
### 7.1 추천 홈 데이터 조회
#### API
- Method: `GET`
- Path: `/api/v2/home/recommendations`
- 기존 API에 추가하지 않고 신규 API 타입으로 만든다.
- 인증: 기존 인증 헤더 패턴과 동일하게 `Authorization: Bearer {token}` 사용
- 응답 래퍼: 기존 관례대로 `ApiResponse<HomeRecommendationResponse>` 디코딩
@@ -50,7 +63,7 @@
```kotlin
data class HomeRecommendationResponse(
val lives: List<HomeLiveItem>,
val banners: List<HomeBannerItem>,
val banners: List<RecommendationBannerResponse>,
val recentlyActiveCreators: List<HomeActiveCreatorItem>,
val recentDebutCreators: List<HomeCreatorItem>,
val firstAudioContents: List<HomeFirstAudioContentItem>,
@@ -59,33 +72,73 @@ data class HomeRecommendationResponse(
val cheerCreators: List<HomeCreatorItem>,
val popularCommunityPosts: List<HomePopularCommunityPostItem>
)
```
Swift 모델은 위 필드명을 그대로 `Decodable`로 구성한다. 서버 nullable 필드는 Swift optional로 선언한다.
data class HomeLiveItem(
val roomId: Long,
val creatorNickname: String,
val creatorProfileImage: String
)
### 7.2 모두 팔로우 하기
data class HomeActiveCreatorItem(
val creatorNickname: String,
val creatorProfileImage: String,
val activityType: String,
val activityAt: String,
val targetId: Long?
)
#### API
- Method: `POST`
- Path: `/api/v2/home/recommendations/creators/follow`
- Request body:
```kotlin
data class FollowRecommendedCreatorsRequest(
val creatorIds: List<Long>?
data class HomeCreatorItem(
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImage: String
)
data class HomeFirstAudioContentItem(
val contentId: Long,
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImage: String,
val title: String,
val price: Int,
val coverImage: String?,
@JsonProperty("isPointAvailable")
val isPointAvailable: Boolean
)
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>
)
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
)
```
- 응답 래퍼: 기존 관례대로 `ApiResponseWithoutData` 디코딩
#### 동작
- 장르 크리에이터 그룹과 최근 응원이 많은 크리에이터 섹션의 `모두 팔로우하기` 버튼에서 호출한다.
- 요청 `creatorIds`는 해당 섹션에 표시된 크리에이터의 `creatorId` 목록을 사용한다.
- API 호출 중 중복 터치를 막는다.
- 호출 완료 후 `success == true`이면 해당 섹션 버튼 상태를 완료로 전환한다.
- 완료 상태 버튼 문구는 `모두 팔로우 완료`로 표시한다.
- 완료 상태 버튼 아이콘은 `ic_new_following`을 사용한다.
- 실패 시 기존 앱 에러 표시 관례에 맞춰 공통 에러 또는 서버 message를 노출하고 버튼 상태는 변경하지 않는다.
Swift 모델은 위 필드명을 우선 기준으로 `Decodable`을 구성한다. 서버 nullable 필드는 Swift optional로 선언한다. 현재 코드에 과거 추정 필드명이 이미 있다면 신규 스펙 필드명을 추가하거나 보정하고, 실제 사용 UI는 신규 스펙 필드명을 기준으로 매핑한다.
### 7.3 활동 타입 다국어 표시
### 7.2 활동 타입 다국어 표시
서버 enum:
```kotlin
@@ -101,200 +154,172 @@ enum class RecommendedActivityType(val code: String) {
- `LIVE`, `LIVE_REPLAY` -> `I18n.HomeRecommendation.activityLive`
- `AUDIO` -> `I18n.HomeRecommendation.activityAudio`
- `COMMUNITY` -> `I18n.HomeRecommendation.activityCommunity`
- 알 수 없는 값은 빈 문자열 또는 서버 코드 직접 표시 대신 해당 아이템의 보조 문구를 숨긴다.
- 알 수 없는 값은 서버 코드를 그대로 노출하지 않고 보조 문구를 숨긴다.
다국어 기본 문구:
- ko: `라이브`, `오디오`, `커뮤니티`
- en: `Live`, `Audio`, `Community`
- ja: `ライブ`, `オーディオ`, `コミュニティ`
### 7.4 사업자 정보 더보기/접기
### 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` 등 실제 상세 이동을 실행한다.
라이브 항목 주의사항:
- 기존 홈의 라이브 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 AudioContentCardView
`HomeFirstAudioContentItem` 기반 표시 조건:
- `isPointAvailable == true`이면 `ic_content_tag_point`를 쓰는 `ImageView`를 표시한다.
- 오리지널 작품이면 `ic_content_tag_original`를 쓰는 `ImageView`를 표시한다.
- `price == 0`이면 `무료` `TextView`를 표시한다.
- 크리에이터의 첫 번째 콘텐츠이면 `FIRST` 글자를 포함하는 `LinearLayout`을 표시한다.
- 위 조건에 해당하지 않는 시각요소는 모두 `GONE` 처리한다.
구현 메모:
- iOS 구현 시 기존 `AudioContentCard` 또는 `AudioContentCardView`가 있으면 우선 재사용/확장한다.
- Android 용어인 `ImageView`, `TextView`, `LinearLayout`, `GONE`은 iOS에서는 각각 SwiftUI `Image`, `Text`, 컨테이너 View, 조건부 미렌더링으로 해석한다.
- 오리지널 작품/첫 번째 콘텐츠 여부가 현재 응답에 명시되어 있지 않으면 서버 필드 확인 전까지 임의 추정하지 않는다.
### 7.6 사업자 정보 더보기/접기
요구사항:
- UI 가장 마지막에 사업자 정보 섹션을 배치한다.
- width는 화면에 채운다.
- 외부 라이브러리를 사용하지 않는다.
- 기본 상태는 최대 3줄 표시, 말줄임표 적용, `더보기` 액션 제공
- `더보기` 터치 시 전체 표시로 전환하고 `접기` 액션 제공
- `접기` 터치 시 다시 3줄 말줄임표 상태로 돌아간다.
- 실제 텍스트가 3줄 이하이면 더보기/접기 버튼은 숨긴다.
구현 방향:
- SwiftUI `Text``lineLimit(isExpanded ? nil : 3)`를 사용한다.
- 더보기/접기 버튼은 텍스트 하단 우측 또는 Figma 사업자 정보 섹션 내 자연스러운 위치에 배치한다.
- 실제 텍스트가 3줄 이하이면 더보기 버튼은 숨긴다. 줄 수 판정은 Geometry 기반 측정 또는 제한/무제한 높이 비교 방식으로 구현한다.
### 7.5 커뮤니티 포스트 카드
참조 Figma:
- Text Only: `node-id=446-9688`
- Text + Img, 유료 + 구매하지 않음: `node-id=446-9690`
- Text + Img, 유료/무료 + 구매함: `node-id=446-9691`
요구사항:
- `CommunityPostCard`는 다른 페이지에서도 재사용 가능하도록 `SodaLive/Sources/V2/Component/Card` 아래에 둔다.
- 텍스트 전용, 이미지 포함, 유료 이미지 잠금 상태를 지원한다.
- `imageUrl == nil`이면 Text Only variant로 표시한다.
- `imageUrl != nil && price > 0 && existOrdered == false`이면 이미지 영역에 blur/lock/pay capsule을 표시한다.
- `imageUrl != nil && (price <= 0 || existOrdered == true)`이면 이미지를 일반 노출한다.
- 유료/무료 + 구매함 Figma 카드의 우측 상단 `구매완료` 캡슐은 구현하지 않는다.
- 본문, 작성자, 생성 시간, 좋아요 수, 댓글 수는 표시한다.
- SwiftUI `Text``lineLimit(isExpanded ? nil : 3)` 기반으로 구현한다.
- 줄 수 판정은 제한/무제한 높이 비교 또는 Geometry 기반 측정 방식을 사용한다.
- 사업자 정보 wrapper는 `MainHomeBusinessInfoSection`, 재사용 가능한 텍스트 UI는 `ExpandableTextView`로 분리한다.
## 8. UX / UI Expectations
### 8.1 Figma 기준 화면 구성
참조 Figma:
### 8.1 Figma 기준
- 추천 화면: `node-id=24-5514`
- 모두 팔로우 완료 버튼: `node-id=24-9092`
- FeedCommunityView 유료 미구매: `node-id=309-19774`
- FeedCommunityView 유료 구매함 또는 무료: `node-id=309-19775`
Figma 확인 결과 추천 화면은 검정 배경, 상단 홈 타이틀/탭, 하단 메인 탭바 사이에 세로 스크롤 콘텐츠로 구성된다. 주요 섹션은 카드/가로 스크롤/그리드 조합이며, 반복 프로필과 버튼은 재사용 위젯화가 필요하다.
Figma 확인 시 이미 생성해 둔 widget 또는 저장소 내 공용 컴포넌트로 구현 가능한 UI는 재사용한다. 단, Figma asset URL을 코드에 직접 넣지 않고 프로젝트 asset 또는 기존 이미지 로딩 패턴을 사용한다.
### 8.2 UI 배치 도식
### 8.2 화면 구성
아래 도식은 구현 대상만 포함한다. `추천 필모그래피`, `또 다른 모습`은 제외한다.
```text
MainHomeView
└─ HomeTitleBar 재사용
└─ 홈 상단 탭(추천/랭킹/팔로잉)
└─ ScrollView
├─ 현재 라이브 섹션
│ └─ MainHomeLiveSection 신규
MainHomeLiveItem 신규
├─ 배너 섹션
└─ BannerCarousel 신규
├─ 최근 활동 크리에이터 섹션
└─ MainHomeActiveCreatorSection 신규
MainHomeActiveCreatorItem 신규
├─ 최근 데뷔한 크리에이터 섹션
├─ SectionTitle 재사용
└─ CreatorProfileGrid 재사용
│ └─ CreatorProfileItem 재사용
├─ 처음 만나는 오디오 섹션
│ ├─ SectionTitle 재사용
│ └─ AudioContentCard 재사용/확장
├─ AI 캐릭터 섹션
│ ├─ SectionTitle 재사용
│ └─ AiCharacterCard 신규
├─ 장르의 크리에이터 섹션
│ └─ MainHomeCreatorGroupSection 신규
│ ├─ SectionTitle 재사용(size 보정 필요 시 신규 variant)
│ ├─ MainHomeCreatorGrid 신규
│ ├─ CreatorProfileItem 재사용
│ └─ FollowAllButton 재사용
├─ 최근 응원이 많은 크리에이터 섹션
│ └─ MainHomeCreatorGroupSection 재사용
├─ 인기 커뮤니티 섹션
│ ├─ SectionTitle 재사용
│ └─ CommunityPostCard 재사용
└─ 사업자 정보 섹션
└─ MainHomeBusinessInfoSection 신규
└─ ExpandableTextView 재사용
└─ MainTabBarView 재사용 가능 여부 확인 후 적용
└─ 선택된 탭 콘텐츠
├─ 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/AudioContentCard.swift`: `firstAudioContents` 카드 기본 구조
- `SodaLive/Sources/V2/Main/MainTabBarView.swift`: 하단 탭바가 V2 홈 구조와 맞는 경우 재사용
- `SodaLive/Sources/Chat/Character/Banner/AutoSlideCharacterBannerView.swift`: 배너 carousel 패턴 참고. 홈 배너 타입과 이동 규칙이 달라 직접 재사용 여부는 계획 단계에서 재확인한다.
- `SodaLive/Sources/Explorer/Profile/CreatorCommunity/CreatorCommunityItemView.swift`: 커뮤니티 카드의 작성자/본문/리액션 패턴 참고. 이번 홈 추천 응답 타입과 더보기 요구가 달라 직접 재사용보다는 신규 위젯 생성이 우선이다.
- `SodaLive/Sources/V2/Component/HomeTitleBar.swift`
- `SodaLive/Sources/V2/Component/SectionTitle.swift`
- `SodaLive/Sources/V2/Component/AudioContentCard.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`
### 8.4 신규 파일/그룹 후보
이미 존재하는 컴포넌트가 Figma 요구를 일부만 충족하면 새 컴포넌트를 중복 생성하지 않고 필요한 범위만 보완한다.
`MainView`의 홈 탭에서 표시되는 페이지 조립 계층은 `SodaLive/Sources/V2/Main/Home` 아래에 둔다.
```text
SodaLive/Sources/V2/Main/Home
├─ MainHomeView.swift
├─ MainHomeViewModel.swift
├─ Repository
│ ├─ MainHomeApi.swift
│ └─ MainHomeRepository.swift
├─ Models
│ ├─ MainHomeRecommendationResponse.swift
│ ├─ FollowRecommendedCreatorsRequest.swift
│ └─ RecommendedActivityType.swift
└─ Components
├─ MainHomeLiveSection.swift
├─ MainHomeLiveItem.swift
├─ MainHomeActiveCreatorSection.swift
├─ MainHomeActiveCreatorItem.swift
├─ MainHomeRecentDebutCreatorSection.swift
├─ MainHomeFirstAudioContentSection.swift
├─ MainHomeAiCharacterSection.swift
├─ MainHomeGenreCreatorSection.swift
├─ MainHomeCheerCreatorSection.swift
├─ MainHomeCreatorGroupSection.swift
├─ MainHomeCreatorGrid.swift
├─ MainHomePopularCommunitySection.swift
└─ MainHomeBusinessInfoSection.swift
```
`MainHome`에서만 사용하는 섹션 조립 컴포넌트는 `SodaLive/Sources/V2/Main/Home/Components` 아래에 둔다. 여러 페이지에서 재사용 가능성이 있는 UI widget은 `SodaLive/Sources/V2/Component` 아래에서 형태별 폴더에 둔다. 공용 widget은 특정 페이지나 API 이름 접두사를 붙이지 않고, 가능한 한 API 모델에 직접 의존하지 않으며 표시용 프로퍼티 또는 작은 display model을 받아 재사용성을 확보한다.
```text
SodaLive/Sources/V2/Component
├─ Banner
│ └─ BannerCarousel.swift
├─ Card
│ ├─ AiCharacterCard.swift
│ └─ CommunityPostCard.swift
├─ Creator
│ ├─ CreatorProfileGrid.swift
│ └─ CreatorProfileItem.swift
├─ Button
│ └─ FollowAllButton.swift
└─ Text
└─ ExpandableTextView.swift
```
단, 구현 중 특정 widget이 홈 탭에서만 의미가 있고 재사용성이 없다고 판단되면 `SodaLive/Sources/V2/Main/Home/Components` 안에 유지한다. 반대로 이미 존재하는 공용 컴포넌트로 충분한 경우 신규 파일을 만들지 않는다.
### 8.5 컴포넌트 위치 결정 기준
- `BannerCarousel`: 다른 페이지에서도 배너 carousel로 재사용 가능하므로 `SodaLive/Sources/V2/Component/Banner`에 둔다.
- 방금 활동한 크리에이터 UI: MainHome에서만 사용하므로 `MainHomeActiveCreatorSection`, `MainHomeActiveCreatorItem``SodaLive/Sources/V2/Main/Home/Components`에 둔다.
- 현재 라이브 UI: 별도 재사용 요구가 없으므로 `MainHomeLiveSection`, `MainHomeLiveItem``SodaLive/Sources/V2/Main/Home/Components`에 둔다.
- 최근 데뷔한 크리에이터 UI: 다른 페이지에서도 재사용 가능하므로 공용 `CreatorProfileGrid`, `CreatorProfileItem`을 사용하고, 섹션 조립만 `MainHomeRecentDebutCreatorSection`에 둔다.
- `AiCharacterCard`: 다른 페이지에서도 캐릭터 카드로 재사용 가능하므로 `SodaLive/Sources/V2/Component/Card`에 둔다.
- 장르/응원이 많은 크리에이터: 그리드 그룹 구조는 MainHome 전용이므로 `MainHomeCreatorGroupSection`, `MainHomeCreatorGrid``SodaLive/Sources/V2/Main/Home/Components`에 둔다. 개별 크리에이터 아이템만 `CreatorProfileItem`으로 재사용한다.
- 커뮤니티 섹션: 섹션 조립은 `MainHomePopularCommunitySection`에 두고, `CommunityPostCard`는 다른 페이지에서도 재사용 가능하므로 `SodaLive/Sources/V2/Component/Card`에 둔다.
- 사업자 정보 섹션: 섹션 wrapper는 `MainHomeBusinessInfoSection`으로 `SodaLive/Sources/V2/Main/Home/Components`에 둔다. 3줄 말줄임표, 더보기, 접기를 담당하는 텍스트 UI는 다른 곳에서도 사용할 수 있는 `ExpandableTextView`로 분리해 `SodaLive/Sources/V2/Component/Text`에 둔다.
### 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/**`에서 수행한다.
- `MainView` 홈 탭에서 표시되는 페이지 루트, ViewModel, Repository, API, Models와 MainHome 전용 섹션 컴포넌트는 `SodaLive/Sources/V2/Main/Home/**` 아래에 작성한다.
- 여러 페이지에서 재사용 가능한 UI widget은 `SodaLive/Sources/V2/Component/**` 아래에 형태별 폴더로 작성한다.
- 순수 공용성이 더 큰 컴포넌트는 구현 시점에 `SodaLive/Sources/V2/Component/**`의 더 적합한 하위 그룹으로 이동할 수 있다.
- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다.
- 앱 소스 변경은 `SodaLive/Sources/**`에서 수행한다.
- `Pods/**`, `generated/**`, `build/**`는 직접 수정하지 않는다.
- 기존 `HomeApi`에 추천 API를 추가하지 않는다.
- 외부 라이브러리를 추가하지 않는다.
- 이미지 로딩은 기존 앱에서 사용하는 이미지 컴포넌트/패턴을 따른다.
- 이미지 로딩은 기존 앱 이미지 컴포넌트/패턴을 따른다.
- 인증 헤더는 기존 `UserDefaultsKey.token` 기반 패턴을 따른다.
- `creatorId`, `liveRoomId`, `bannerId`, `postId` 등 서버 Long 값은 Swift에서 `Int` 또는 `Int64` 중 기존 라우팅/모델 관례와 맞는 타입을 사용한다. 계획 단계에서 실제 이동 대상 API 타입과 맞춰 확정한다.
- API 날짜 문자열(`beginDateTime`, `activityAt`, `releaseDate`, `createdAt`)은 기존 날짜 포맷 유틸이 있으면 재사용한다.
- 서버 `Long` 값은 Swift에서 기존 라우팅/모델 관례와 맞는 `Int` 또는 `Int64`로 사용한다.
- 사용자 노출 문구는 가능한 `I18n`에 추가한다.
- 상세 진입 guard는 `MainHomeView` 내부에 인증 UI를 중복 구현하지 않고, V2 `MainView`의 기존 인증/민감 콘텐츠 guard 인프라를 재사용한다.
- `MainHomeView`에는 추천 탭의 섹션 조립을 직접 누적하지 않고, 탭별 콘텐츠 View를 조합하는 역할만 둔다.
- Open Questions에 남긴 항목은 임의 구현하지 않고 확인 후 반영한다.
## 10. Success Criteria
- 추천 탭에서 `/api/v2/home/recommendations`를 호출하고 `success == true` 응답 데이터를 섹션별로 렌더링한다.
- 추천 탭에서 `GET /api/v2/home/recommendations`를 호출하고 `success == true` 응답 데이터를 섹션별로 렌더링한다.
- 응답 배열이 비어 있는 섹션은 화면에 표시하지 않는다.
- 제외 섹션인 `추천 필모그래피`, `또 다른 모습`은 코드와 화면에 포함하지 않는다.
- 모두 팔로우 API 성공 시 해당 버튼이 Figma 완료 디자인에 맞게 `모두 팔로우 완료``ic_new_following` 상태로 변경된다.
- 사업자 정보는 기본 3줄 말줄임표, 더보기, 전체 표시, 접기 전환이 동작한다.
- `추천 필모그래피`, `또 다른 모습`은 코드와 화면에 포함하지 않는다.
- `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY` 활동 타입이 I18n 문구로 표시된다.
- 반복 UI가 Custom Widget으로 분리되어 같은 프로필/그룹/버튼 구조를 중복 구현하지 않는다.
- 빌드가 성공하고, 가능하면 ViewModel 단위의 응답 디코딩 및 모두 팔로우 성공 상태 테스트가 통과한다.
- `FeedCommunityView`에서 키워드 영역이 제거되고, 이미지/유료 미구매/유료 구매함 또는 무료 상태가 조건에 맞게 표시된다.
- `FeedCommunityView``구매완료` 버튼이 표시되지 않는다.
- `AudioContentCardView`에서 point/original/free/FIRST 시각요소가 조건에 맞게 표시되고 그 외에는 숨겨진다.
- 사업자 정보는 마지막 섹션에서 화면 width를 채우고, 기본 3줄 말줄임표, 더보기, 전체 표시, 접기 전환이 동작한다.
- 상세 진입 탭 액션은 기존 홈과 같은 로그인, 한국 사용자 본인인증, 민감 콘텐츠 보기 설정 guard를 통과한 뒤에만 실행된다.
- `MainHomeView`는 추천/랭킹/팔로잉 탭 shell 역할만 하고, 추천 탭 섹션 조립은 `MainHomeRecommendationView`에 분리되어 있다.
- 반복 UI는 기존 widget 또는 공용 컴포넌트를 재사용하고 불필요하게 중복 구현하지 않는다.
- 빌드가 성공하고, 가능하면 ViewModel 디코딩/상태 전환 검증을 수행한다.
## 11. Metrics
- 추천 API 성공/실패 여부
- 모두 팔로우 API 호출 성공/실패 여부
- 추천 탭 첫 로딩 완료 시간
- 모두 팔로우 버튼 터치 후 완료 상태 전환 여부
## 12. Open Questions
- 배너 `type`별 이동 규칙(`eventId`, `creatorId`, `seriesId`, `link`)을 어떤 기존 라우팅과 연결할지 확인이 필요하다.
## 11. Open Questions
- `RecommendationBannerResponse`의 필드 목록과 배너 `type`별 이동 규칙 확인이 필요하다.
- 각 섹션별 최대 표시 개수와 가로/세로 스크롤 정책이 Figma 기준 그대로인지, 서버 응답 전체를 모두 표시해야 하는지 확인이 필요하다.
- `creatorIds == null` 요청을 허용해야 하는지, 앱에서는 빈 배열/비어 있는 섹션일 때 버튼을 숨기는 것으로 제한할지 확인이 필요하다.
- 모두 팔로우 완료 상태를 앱 세션 동안만 유지할지, 추천 API 재조회 후에도 서버 상태 기반으로 유지할지 확인이 필요하다.
- `HomeFirstAudioContentItem`에서 오리지널 작품 여부와 크리에이터의 첫 번째 콘텐츠 여부를 판단할 서버 필드가 추가되는지 확인이 필요하다.
- `HomeLiveItem`에 민감/성인 라이브 여부를 판단할 `isAdult` 또는 동등한 필드가 추가되는지, 아니면 서버에서 추천 라이브를 사전 필터링하는지 확인이 필요하다.
- 커뮤니티 `audioUrl`을 추천 탭 카드에서 표시/재생해야 하는지 확인이 필요하다.
- 각 카드 터치 시 상세 이동 대상이 모두 정의되어 있는지 확인이 필요하다.
## 13. Verification Plan
- PRD 검증: 요구사항, 제외 범위, API URL, 응답 모델, Figma 노드가 문서에 반영되었는지 확인한다.
- 구현 후 빌드 검증: `docs/agent-guides/build-test-verification.md` 기준 명령으로 iOS 빌드 또는 가능한 최소 검증을 실행한다.
- 구현 후 기능 검증: 추천 API 성공/실패, 빈 섹션, 모두 팔로우 성공/실패, 사업자 정보 더보기/접기, 활동 타입 I18n 표시를 확인한다.
## 12. Verification Plan
- 문서 검증: API URL, 응답 모델, 제외 섹션, FeedCommunityView, AudioContentCardView, 사업자 정보 요구가 PRD와 계획 문서에 반영되었는지 확인한다.
- 정적 검증: 신규 추천 API가 기존 `HomeApi`에 추가되지 않았고, Figma URL이 앱 코드에 직접 포함되지 않았는지 확인한다.
- 빌드 검증: `docs/agent-guides/build-test-verification.md` 기준으로 가능한 iOS 빌드 또는 최소 정적 검증을 실행한다.
- 기능 검증: 추천 API 성공/실패, 빈 섹션, 활동 타입 I18n, 상세 진입 guard, 커뮤니티 카드 상태, 오디오 카드 태그 상태, 사업자 정보 더보기/접기를 확인한다.