From 6b5176eba729430aa85c22a04106dda6f2ebbd64 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 26 Jun 2026 19:23:53 +0900 Subject: [PATCH] =?UTF-8?q?chore(home):=20=EC=B6=94=EC=B2=9C=20=ED=99=88?= =?UTF-8?q?=20=EA=B8=B0=EC=A4=80=EC=9D=84=20=EC=A0=95=EB=A6=AC=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive.xcodeproj/project.pbxproj | 13 +- .../Sources/V2/Component/HomeTitleBar.swift | 2 - .../plan-task.md | 714 ++++++++++-------- docs/20260602_메인_홈_추천_UI_API_연동/prd.md | 417 +++++----- 4 files changed, 626 insertions(+), 520 deletions(-) diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 5bd9541e..bf8987e5 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -2409,6 +2409,10 @@ F7F1421DBAB56E2F6891625E /* DefaultTitleBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DefaultTitleBar.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 401A3B942FEE61E000A5BBA3 /* docs */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = docs; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 40FE0E812A8361DF00252293 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -4542,6 +4546,7 @@ 40FE0E7B2A8361DF00252293 = { isa = PBXGroup; children = ( + 401A3B942FEE61E000A5BBA3 /* docs */, 40FE0EAF2A83648600252293 /* SodaLive-dev.entitlements */, 40FE0E862A8361DF00252293 /* SodaLive */, B3B6DC93A319880FD916CE87 /* generated */, @@ -4909,6 +4914,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 401A3B942FEE61E000A5BBA3 /* docs */, + ); name = SodaLive; packageProductDependencies = ( 40B28A1F2A83775F008BE463 /* FirebaseAnalytics */, @@ -4952,6 +4960,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 401A3B942FEE61E000A5BBA3 /* docs */, + ); name = "SodaLive-dev"; packageProductDependencies = ( 40B28A292A83779E008BE463 /* FirebaseAnalytics */, diff --git a/SodaLive/Sources/V2/Component/HomeTitleBar.swift b/SodaLive/Sources/V2/Component/HomeTitleBar.swift index 7d0cb41a..95a9c932 100644 --- a/SodaLive/Sources/V2/Component/HomeTitleBar.swift +++ b/SodaLive/Sources/V2/Component/HomeTitleBar.swift @@ -15,8 +15,6 @@ struct HomeTitleBar: View { var body: some View { TitleBar { Image("img_text_logo_v2") - .resizable() - .scaledToFit() } trailing: { menu } diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md b/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md index 49327f8d..3b430c09 100644 --- a/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md +++ b/docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md @@ -1,319 +1,388 @@ -# 메인 홈 추천 UI와 API 연동 구현 계획 +# 메인 홈 추천 탭 UI와 API 연동 구현 계획 ## 기준 문서 - PRD: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` - Figma 추천 화면: `24:5514` -- Figma 모두 팔로우 완료 버튼: `24:9092` -- Figma 커뮤니티 카드: - - Text Only: `446:9688` - - Text + Img, 유료 + 구매하지 않음: `446:9690` - - Text + Img, 유료/무료 + 구매함: `446:9691` +- Figma FeedCommunityView 유료 미구매: `309:19774` +- Figma FeedCommunityView 유료 구매함 또는 무료: `309:19775` - 코드 스타일: `docs/agent-guides/code-style.md` - 빌드/검증: `docs/agent-guides/build-test-verification.md` -## 작업 원칙 +## 재분류 원칙 -- PRD의 성공 기준과 제외 범위를 벗어나지 않는다. -- `추천 필모그래피`, `또 다른 모습` 섹션은 만들지 않는다. -- 기존 `SodaLive/Sources/Home/HomeApi.swift`에는 추천 API를 추가하지 않는다. -- 신규 페이지 루트, ViewModel, Repository, API, Models, MainHome 전용 컴포넌트는 `SodaLive/Sources/V2/Main/Home/**` 아래에 작성한다. -- 여러 페이지에서 재사용 가능한 UI widget은 `SodaLive/Sources/V2/Component/**` 아래에 형태별 폴더로 작성한다. +- 2026-06-26 기준으로 기존 Phase와 완료 체크 상태는 구현 순서 판단에 사용하지 않는다. +- 이전 검증 기록은 하단에 보존하되, 새 체크리스트는 신규 PRD 요구사항 기준으로 다시 작성한다. +- 화면 UI는 섹션당 하나의 Phase로 나누어 작게 작업한다. +- `MainHomeView`는 홈 상단 공통 shell과 추천/랭킹/팔로잉 탭 전환만 담당한다. +- 추천/랭킹/팔로잉 탭 아래 콘텐츠는 각각 `Recommendation`, `Ranking`, `Following` 하위 폴더에 별도 View로 배치한다. +- 기존에 이미 생성된 widget과 파일이 있으면 새로 만들지 않고 확인/보완한다. +- `추천 필모그래피`, `또 다른 모습`, 사업자 정보 외 추가 하단 정보 섹션은 만들지 않는다. +- 기존 `SodaLive/Sources/Home/HomeApi.swift`에는 `/api/v2/home/recommendations`를 추가하지 않는다. - 외부 라이브러리는 추가하지 않는다. -- Figma localhost asset URL은 앱 코드에 사용하지 않는다. -- 서버/기획 확인이 필요한 이동 규칙과 최대 표시 개수는 임의 확장하지 않고, 현재 화면 렌더링과 API 연동에 필요한 최소 동작만 구현한다. -- 수정 후 검증 기록은 이 문서 하단에 누적한다. -## 구현 대상 파일 +## 주요 대상 파일 -### 생성 +### 확인/보완 - `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` - - `MainView` 홈 탭에서 표시할 실제 홈 추천 페이지 루트. -- `SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift` - - 추천 API 로딩/에러/섹션 데이터/모두 팔로우 완료 상태 관리. -- `SodaLive/Sources/V2/Main/Home/Repository/MainHomeApi.swift` - - `/api/v2/home/recommendations`, `/api/v2/home/recommendations/creators/follow` 전용 Moya `TargetType`. -- `SodaLive/Sources/V2/Main/Home/Repository/MainHomeRepository.swift` - - `MainHomeApi` 호출을 `AnyPublisher`로 제공. -- `SodaLive/Sources/V2/Main/Home/Models/MainHomeRecommendationResponse.swift` - - `HomeRecommendationResponse`와 하위 응답 모델. -- `SodaLive/Sources/V2/Main/Home/Models/FollowRecommendedCreatorsRequest.swift` - - 모두 팔로우 요청 모델. -- `SodaLive/Sources/V2/Main/Home/Models/RecommendedActivityType.swift` - - 서버 활동 타입 enum 변환과 I18n 표시. -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeLiveSection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeLiveItem.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeActiveCreatorSection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeActiveCreatorItem.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeRecentDebutCreatorSection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeFirstAudioContentSection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeAiCharacterSection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeGenreCreatorSection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeCheerCreatorSection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeCreatorGroupSection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeCreatorGrid.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomePopularCommunitySection.swift` -- `SodaLive/Sources/V2/Main/Home/Components/MainHomeBusinessInfoSection.swift` -- `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` -- `SodaLive/Sources/V2/Component/Card/AiCharacterCard.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationRepository.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift` +- `SodaLive/Sources/V2/Component/AudioContentCard.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/Button/FollowAllButton.swift` - `SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift` - -### 수정 - -- `SodaLive/Sources/V2/Main/MainView.swift` - - `.home` 탭에서 `MainPlaceholderTabView` 대신 `MainHomeView` 표시. - `SodaLive/Sources/I18n/I18n.swift` - - 홈 추천 섹션 제목, 활동 타입, 모두 팔로우 문구, 더보기/접기 문구 추가. - `SodaLive.xcodeproj/project.pbxproj` - - 신규 Swift 파일이 Xcode 빌드 대상에 포함되지 않는 경우에만 수정. -- `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` - - 체크리스트와 검증 기록 유지. + +### 생성 후보 + +없는 경우에만 생성한다. + +- `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeLiveSection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBannerSection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeRecentDebutCreatorSection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeFirstAudioContentSection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeGenreCreatorSection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeCheerCreatorSection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomePopularCommunitySection.swift` +- `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBusinessInfoSection.swift` +- `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift` +- `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift` ## TASK 체크리스트 -### Phase 1: 구현 기준과 데이터 계층 준비 +### Phase 1: 기준 스펙과 기존 구현 점검 -- [x] **Task 1.1: 구현 전 구조 확인** +- [ ] **Task 1.1: 문서/코드 기준점 확인** - 대상 파일: - - 확인: `SodaLive/Sources/V2/Main/MainView.swift` - - 확인: `SodaLive/Sources/V2/Component/SectionTitle.swift` - - 확인: `SodaLive/Sources/V2/Component/AudioContentCard.swift` - - 확인: `SodaLive/Sources/V2/Component/HomeTitleBar.swift` - - 확인: `SodaLive/Sources/V2/Main/MainTabBarView.swift` - - 확인: `SodaLive/Sources/Home/HomeApi.swift` - - 확인: `SodaLive/Sources/I18n/I18n.swift` - - 작업 내용: - - `.home` 탭이 현재 `MainPlaceholderTabView`를 표시하는지 확인한다. - - 기존 V2 공용 컴포넌트 재사용 범위를 확인한다. - - 기존 `HomeApi.swift`를 수정하지 않는 것을 기준으로 신규 `MainHomeApi` 위치를 확정한다. - - `I18n.swift`의 기존 enum 배치 방식과 `pick(ko:en:ja:)` 사용 방식을 확인한다. - - 검증 기준: - - 실행 명령: `rg "case \\.home|MainPlaceholderTabView|enum HomeApi|enum I18n" SodaLive/Sources/V2/Main SodaLive/Sources/Home SodaLive/Sources/I18n/I18n.swift` - - 기대 결과: `.home` placeholder 위치, 기존 `HomeApi`, `I18n` 구조를 확인할 수 있다. - - 수동 확인: 신규 추천 API를 기존 `HomeApi.swift`에 추가하지 않아야 한다. - -- [x] **Task 1.2: 추천 API 모델 작성** - - 대상 파일: - - 생성: `SodaLive/Sources/V2/Main/Home/Models/MainHomeRecommendationResponse.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Models/FollowRecommendedCreatorsRequest.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Models/RecommendedActivityType.swift` - - 작업 내용: - - `MainHomeRecommendationResponse.swift`에 `HomeRecommendationResponse`와 하위 응답 모델을 `Decodable`로 작성한다. - - 서버 nullable 필드는 Swift optional로 선언한다. - - 서버 Long 값은 우선 기존 앱 라우팅 관례와 맞추기 쉬운 `Int`로 작성하고, 빌드 또는 호출부에서 `Int64`가 필요하면 보정한다. - - `FollowRecommendedCreatorsRequest`는 `creatorIds: [Int]?`를 갖는 `Encodable`로 작성한다. - - `RecommendedActivityType`은 `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`, unknown 처리를 제공한다. - - 검증 기준: - - 실행 명령: `rg "struct HomeRecommendationResponse|struct FollowRecommendedCreatorsRequest|enum RecommendedActivityType" SodaLive/Sources/V2/Main/Home/Models` - - 기대 결과: 세 모델/enum 정의가 검색된다. - - 수동 확인: PRD의 응답 필드명이 Swift 모델에 누락 없이 반영되어야 한다. - -- [x] **Task 1.3: I18n 문구 추가** - - 대상 파일: - - 수정: `SodaLive/Sources/I18n/I18n.swift` - - 작업 내용: - - `I18n.HomeRecommendation` enum을 추가한다. - - 활동 타입 문구를 추가한다. - - `activityLive`: ko `라이브`, en `Live`, ja `ライブ` - - `activityAudio`: ko `오디오`, en `Audio`, ja `オーディオ` - - `activityCommunity`: ko `커뮤니티`, en `Community`, ja `コミュニティ` - - 섹션 제목 문구를 추가한다. - - 현재 라이브 - - 방금 활동한 크리에이터 - - 최근 데뷔한 크리에이터 - - 처음 만나는 오디오 - - AI 캐릭터 - - 장르의 크리에이터 - - 최근 응원이 많은 크리에이터 - - 인기 커뮤니티 - - 버튼/텍스트 문구를 추가한다. - - `followAll`: `모두 팔로우하기` - - `followAllCompleted`: `모두 팔로우 완료` - - `more`: `더보기` - - `collapse`: `접기` - - 검증 기준: - - 실행 명령: `rg "enum HomeRecommendation|activityLive|followAllCompleted|collapse" SodaLive/Sources/I18n/I18n.swift` - - 기대 결과: 홈 추천 I18n enum과 핵심 문구가 검색된다. - - 수동 확인: 신규 사용자 노출 문자열이 하드코딩 View 텍스트로 흩어지지 않아야 한다. - -- [x] **Task 1.4: 신규 API와 Repository 작성** - - 대상 파일: - - 생성: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeApi.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeRepository.swift` - - 확인: `SodaLive/Sources/Home/HomeApi.swift` - - 확인: `SodaLive/Sources/Home/HomeTabRepository.swift` - - 작업 내용: - - `MainHomeApi`에 `getRecommendations`, `followRecommendedCreators(request:)` 케이스를 작성한다. - - `getRecommendations`는 `GET /api/v2/home/recommendations`로 작성한다. - - `followRecommendedCreators(request:)`는 `POST /api/v2/home/recommendations/creators/follow`로 작성한다. - - 인증 헤더는 기존 패턴대로 `Authorization: Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))`를 사용한다. - - `MainHomeRepository`에서 위 두 API를 `requestPublisher`로 감싼다. - - 기존 `HomeApi.swift`, `HomeTabRepository.swift`는 수정하지 않는다. - - 검증 기준: - - 실행 명령: `rg "/api/v2/home/recommendations|followRecommendedCreators|getRecommendations" SodaLive/Sources/V2/Main/Home/Repository` - - 기대 결과: 신규 API 경로와 repository 메서드가 신규 폴더에서만 검색된다. - - 실행 명령: `rg "/api/v2/home/recommendations" SodaLive/Sources/Home` - - 기대 결과: 검색 결과가 없어야 한다. - -### Phase 2: ViewModel 상태와 공용 UI 컴포넌트 작성 - -- [x] **Task 2.1: MainHomeViewModel 작성** - - 대상 파일: - - 생성: `SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift` - - 확인: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeRepository.swift` - - 확인: `SodaLive/Sources/V2/Main/Home/Models/MainHomeRecommendationResponse.swift` - - 작업 내용: - - `@Published` 상태를 작성한다. - - `isLoading` - - `isShowPopup` - - `errorMessage` - - `recommendations` - - 장르/응원 그룹별 모두 팔로우 완료 상태 - - 모두 팔로우 호출 중 상태 - - `fetchRecommendations()`에서 `ApiResponse`를 디코딩한다. - - `success == true`이고 `data`가 있으면 화면 상태를 갱신한다. - - 실패, 디코딩 실패, 네트워크 실패는 기존 앱 관례대로 `errorMessage`와 `isShowPopup`으로 처리한다. - - `followAll(creatorIds:completionKey:)`에서 `ApiResponseWithoutData`를 디코딩한다. - - 모두 팔로우 성공 시 해당 섹션 완료 상태만 true로 바꾼다. - - 빈 `creatorIds`인 경우 API를 호출하지 않고 버튼을 숨기거나 비활성화하는 방식으로 처리한다. - - 검증 기준: - - 실행 명령: `rg "final class MainHomeViewModel|fetchRecommendations|followAll|ApiResponse|ApiResponseWithoutData" SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift` - - 기대 결과: ViewModel 상태와 API 처리 메서드가 검색된다. - - 수동 확인: 실패 처리에서 빈 `catch`를 사용하지 않아야 한다. - -- [x] **Task 2.2: 공용 `ExpandableTextView` 작성** - - 대상 파일: - - 생성: `SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift` - - 작업 내용: - - `text`, `lineLimit`, `moreTitle`, `collapseTitle`, `font`, `foregroundColor`를 입력으로 받는다. - - 기본 상태는 `lineLimit(3)`과 말줄임표를 적용한다. - - `더보기` 터치 시 전체 표시로 전환한다. - - `접기` 터치 시 다시 3줄 제한 상태로 전환한다. - - 외부 라이브러리를 사용하지 않는다. - - 텍스트가 3줄 이하이면 더보기/접기 버튼을 숨긴다. - - 검증 기준: - - 실행 명령: `rg "struct ExpandableTextView|lineLimit|moreTitle|collapseTitle|isExpanded" SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift` - - 기대 결과: 확장/접기 상태와 3줄 제한 구현 키워드가 검색된다. - - 수동 확인: 외부 라이브러리 import 없이 `SwiftUI` 기반으로 구현되어야 한다. - -- [x] **Task 2.3: 공용 Creator 컴포넌트 작성** - - 대상 파일: - - 생성: `SodaLive/Sources/V2/Component/Creator/CreatorProfileItem.swift` - - 생성: `SodaLive/Sources/V2/Component/Creator/CreatorProfileGrid.swift` - - 작업 내용: - - `CreatorProfileItem`은 프로필 이미지, 이름, 선택적 보조 텍스트, 아이템 탭 액션을 입력으로 받는다. - - `CreatorProfileItem`은 API 응답 모델에 직접 의존하지 않는다. - - `CreatorProfileGrid`는 표시 모델 배열, 열/간격, 탭 액션을 입력으로 받는다. - - 최근 데뷔 크리에이터와 장르/응원 그룹의 개별 아이템에 재사용할 수 있게 작성한다. - - 검증 기준: - - 실행 명령: `rg "struct CreatorProfileItem|struct CreatorProfileGrid|creatorNickname|HomeCreatorItem|HomeRecommendationResponse" SodaLive/Sources/V2/Component/Creator` - - 기대 결과: `CreatorProfileItem`, `CreatorProfileGrid`는 검색되고 API 응답 타입 의존은 없어야 한다. - - 수동 확인: 공용 컴포넌트 타입명에 `MainHome` 또는 `HomeRecommendation` 접두사를 붙이지 않아야 한다. - -- [x] **Task 2.4: 공용 Button/Banner/Card 컴포넌트 작성** - - 대상 파일: - - 생성: `SodaLive/Sources/V2/Component/Button/FollowAllButton.swift` - - 생성: `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` - - 생성: `SodaLive/Sources/V2/Component/Card/AiCharacterCard.swift` - - 생성: `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` - - 작업 내용: - - `FollowAllButton`은 기본 상태 `모두 팔로우하기`, 완료 상태 `모두 팔로우 완료`, 완료 아이콘 `ic_new_following`, 호출 중 중복 터치 방지를 지원한다. - - `BannerCarousel`은 배너 이미지 URL 목록과 탭 액션을 입력으로 받는다. - - `AiCharacterCard`는 캐릭터명, 설명, 프로필 이미지, 채팅 수, 원작 제목을 입력으로 받는다. - - `CommunityPostCard`는 Text Only, Text + Img 잠금, Text + Img 노출 variant를 `imageUrl`, `price`, `existOrdered` 기준으로 분기한다. - - `CommunityPostCard`는 `구매완료` 캡슐을 구현하지 않는다. - - 검증 기준: - - 실행 명령: `rg "struct FollowAllButton|ic_new_following|struct BannerCarousel|struct AiCharacterCard|struct CommunityPostCard|구매완료" SodaLive/Sources/V2/Component` - - 기대 결과: 네 공용 컴포넌트와 `ic_new_following`은 검색되고, `구매완료` 캡슐 구현 텍스트는 production UI로 남지 않아야 한다. - - 수동 확인: 공용 컴포넌트가 특정 API 응답 모델을 직접 받지 않아야 한다. - -### Phase 3: MainHome 전용 섹션과 페이지 조립 - -- [ ] **Task 3.1: MainHome 전용 섹션 컴포넌트 작성** - - 대상 파일: - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeLiveSection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeLiveItem.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeActiveCreatorSection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeActiveCreatorItem.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeRecentDebutCreatorSection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeFirstAudioContentSection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeAiCharacterSection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeGenreCreatorSection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeCheerCreatorSection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeCreatorGroupSection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeCreatorGrid.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomePopularCommunitySection.swift` - - 생성: `SodaLive/Sources/V2/Main/Home/Components/MainHomeBusinessInfoSection.swift` - - 작업 내용: - - 라이브/최근 활동/최근 데뷔/첫 오디오/AI 캐릭터/장르/응원/커뮤니티/사업자 정보 섹션을 작성한다. - - `MainHomeRecentDebutCreatorSection`은 공용 `CreatorProfileGrid`를 사용한다. - - `MainHomeFirstAudioContentSection`은 기존 `AudioContentCard`를 사용한다. - - `MainHomeAiCharacterSection`은 공용 `AiCharacterCard`를 사용한다. - - `MainHomeGenreCreatorSection`, `MainHomeCheerCreatorSection`은 `MainHomeCreatorGroupSection`을 조합한다. - - `MainHomePopularCommunitySection`은 공용 `CommunityPostCard`를 사용한다. - - `MainHomeBusinessInfoSection`은 공용 `ExpandableTextView`를 사용한다. - - 각 섹션은 데이터 배열이 비어 있으면 렌더링하지 않는다. - - 검증 기준: - - 실행 명령: `rg "struct MainHome.*Section|struct MainHome.*Item|CreatorProfileGrid|AudioContentCard|AiCharacterCard|CommunityPostCard|ExpandableTextView" SodaLive/Sources/V2/Main/Home/Components` - - 기대 결과: MainHome 전용 섹션과 공용 컴포넌트 조합이 검색된다. - - 수동 확인: `추천 필모그래피`, `또 다른 모습` 섹션 파일이나 View를 만들지 않아야 한다. - -- [ ] **Task 3.2: MainHomeView 작성** - - 대상 파일: - - 생성: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` - - 확인: `SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift` - - 확인: `SodaLive/Sources/V2/Component/HomeTitleBar.swift` - - 작업 내용: - - `HomeTitleBar`, 상단 탭 UI, 세로 `ScrollView`, 각 섹션 컴포넌트를 조합한다. - - `.onAppear`에서 최초 `fetchRecommendations()`를 호출한다. - - 로딩/에러 표시 방식은 기존 앱 관례를 따른다. - - 배너/카드 탭 액션은 PRD의 미결정 항목이므로 구현 범위에서는 비워 두거나 기존 라우팅이 명확한 경우에만 연결한다. - - 사업자 정보 텍스트는 Figma의 사업자 정보 문구를 사용한다. - - 검증 기준: - - 실행 명령: `rg "struct MainHomeView|HomeTitleBar|ScrollView|fetchRecommendations|MainHomeBusinessInfoSection" SodaLive/Sources/V2/Main/Home/MainHomeView.swift` - - 기대 결과: 페이지 루트와 주요 조합 요소가 검색된다. - - 수동 확인: `MainTabBarView`는 `MainView`의 safe area inset에서 유지하고, `MainHomeView` 내부에서 중복 생성하지 않아야 한다. - -- [ ] **Task 3.3: MainView 홈 탭 연결** - - 대상 파일: - - 수정: `SodaLive/Sources/V2/Main/MainView.swift` - - 작업 내용: - - `.home` 분기에서 `MainPlaceholderTabView(title: MainTab.home.title)`를 `MainHomeView()`로 교체한다. - - 다른 탭의 placeholder 동작은 변경하지 않는다. - - 기존 `legacyHomeViewModel`의 이벤트 팝업/회원 정보 초기화 흐름은 변경하지 않는다. - - 검증 기준: - - 실행 명령: `rg "case \\.home|MainHomeView|MainPlaceholderTabView" SodaLive/Sources/V2/Main/MainView.swift` - - 기대 결과: `.home` 분기는 `MainHomeView()`를 표시하고, 다른 탭에는 기존 placeholder가 남아 있다. - - 수동 확인: 홈 탭 외 content/chat/my 분기를 불필요하게 수정하지 않아야 한다. - -### Phase 4: 프로젝트 포함과 정적 점검 - -- [ ] **Task 4.1: Xcode 프로젝트 포함 여부 확인** - - 대상 파일: - - 확인: `SodaLive.xcodeproj/project.pbxproj` + - 확인: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` - 확인: `SodaLive/Sources/V2/Main/Home/**` - 확인: `SodaLive/Sources/V2/Component/**` - 작업 내용: - - 신규 Swift 파일이 Xcode 빌드 대상에 포함되는지 확인한다. - - 파일 시스템 동기화 방식으로 자동 포함되면 `SodaLive.xcodeproj/project.pbxproj`를 수정하지 않는다. - - 프로젝트 파일 수정이 필요한 경우에만 `SodaLive.xcodeproj/project.pbxproj`를 갱신한다. + - PRD의 신규 API 응답 필드와 현재 Swift 모델 필드 차이를 확인한다. + - 현재 `SodaLive/Sources/V2/Main/Home` 바로 아래에 있는 추천 전용 파일을 `Recommendation` 하위 폴더로 이동/이름 변경할 대상을 확인한다. + - Figma 기준으로 재사용 가능한 기존 widget을 확인한다. + - 이미 생성된 파일은 유지하고 필요한 보완만 계획한다. + - 검증 기준: + - 실행 명령: `find SodaLive/Sources/V2/Main/Home SodaLive/Sources/V2/Component -maxdepth 4 -type f | sort` + - 기대 결과: 현재 생성된 V2 홈/공용 컴포넌트 목록을 확인할 수 있다. + +- [ ] **Task 1.2: 제외 범위 점검** + - 대상 파일: + - 확인: `SodaLive/Sources/V2/Main/Home/**` + - 확인: `SodaLive/Sources/V2/Component/**` + - 작업 내용: + - `추천 필모그래피`, `또 다른 모습` 섹션 파일이나 View를 만들지 않는다. + - Figma URL이나 localhost asset URL이 앱 코드에 직접 들어가지 않도록 확인한다. + - 검증 기준: + - 실행 명령: `rg "추천 필모그래피|또 다른 모습|figma.com|localhost:3845" SodaLive/Sources/V2/Main/Home SodaLive/Sources/V2/Component` + - 기대 결과: 검색 결과가 없어야 한다. + +### Phase 2: 신규 API와 응답 모델 + +- [ ] **Task 2.1: 추천 API endpoint 확인/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationRepository.swift` + - 이동/정리: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeApi.swift` + - 이동/정리: `SodaLive/Sources/V2/Main/Home/Repository/MainHomeRepository.swift` + - 확인: `SodaLive/Sources/Home/HomeApi.swift` + - 작업 내용: + - `GET /api/v2/home/recommendations`를 신규 API 타입에만 둔다. + - 추천 API 타입/Repository는 `Recommendation/Repository` 아래에 둔다. + - 기존 `HomeApi`에는 endpoint를 추가하지 않는다. + - 인증 헤더는 기존 token 패턴을 따른다. + - 검증 기준: + - 실행 명령: `rg "/api/v2/home/recommendations|getRecommendations" SodaLive/Sources/V2/Main/Home/Recommendation/Repository SodaLive/Sources/Home` + - 기대 결과: 신규 경로는 `SodaLive/Sources/V2/Main/Home/Recommendation/Repository`에서만 검색된다. + +- [ ] **Task 2.2: 신규 Response 모델 보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` + - 이동/정리: `SodaLive/Sources/V2/Main/Home/Models/MainHomeRecommendationResponse.swift` + - 작업 내용: + - `HomeRecommendationResponse` 필드를 PRD와 맞춘다. + - 추천 응답 모델은 `Recommendation/Models` 아래에 둔다. + - `banners` 타입은 `RecommendationBannerResponse` 또는 Swift 대응 타입으로 명확히 한다. + - `HomeLiveItem.roomId`, `creatorProfileImage`, `HomeActiveCreatorItem.targetId`, `HomeFirstAudioContentItem.price`, `coverImage`, `isPointAvailable`, `HomeAiCharacterItem.totalChatCount`, `originalWorkTitle`, `HomePopularCommunityPostItem.audioUrl` 누락 여부를 확인한다. + - 과거 추정 필드는 실제 사용 경로가 없으면 제거하거나 호환 필요 시 명확히 분리한다. + - 검증 기준: + - 실행 명령: `rg "roomId|creatorProfileImage|targetId|price|coverImage|isPointAvailable|totalChatCount|originalWorkTitle|audioUrl" SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` + - 기대 결과: PRD 핵심 필드가 모델에 반영되어야 한다. + +### Phase 3: 활동 타입과 I18n + +- [ ] **Task 3.1: RecommendedActivityType 변환 보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift` + - 이동/정리: `SodaLive/Sources/V2/Main/Home/Models/RecommendedActivityType.swift` + - 확인/보완: `SodaLive/Sources/I18n/I18n.swift` + - 작업 내용: + - `LIVE`, `LIVE_REPLAY`는 `라이브` 계열 문구로 표시한다. + - `AUDIO`는 `오디오`, `COMMUNITY`는 `커뮤니티` 계열 문구로 표시한다. + - 알 수 없는 값은 서버 코드를 그대로 표시하지 않는다. + - ko/en/ja 문구를 I18n에 둔다. + - 검증 기준: + - 실행 명령: `rg "LIVE_REPLAY|activityLive|activityAudio|activityCommunity|ライブ|オーディオ|コミュニティ" SodaLive/Sources/V2/Main/Home/Recommendation/Models/RecommendedActivityType.swift SodaLive/Sources/I18n/I18n.swift` + - 기대 결과: enum 변환과 I18n 문구가 확인된다. + +### Phase 4: MainHomeView 탭 shell과 탭별 루트 View + +- [ ] **Task 4.1: MainHomeView 탭 shell 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - 확인: `SodaLive/Sources/V2/Component/HomeTitleBar.swift` + - 작업 내용: + - `MainHomeView`는 `HomeTitleBar`, 추천/랭킹/팔로잉 상단 탭, 선택된 탭 콘텐츠 조합만 담당한다. + - 추천 탭 섹션 조립과 추천 API 호출을 `MainHomeView`에 직접 넣지 않는다. + - 하단 `MainTabBarView`는 기존 `MainView` 구조에서 유지하고 `MainHomeView` 내부에 중복 생성하지 않는다. + - 검증 기준: + - 실행 명령: `rg "struct MainHomeView|HomeTitleBar|MainHomeRecommendationView|MainHomeRankingView|MainHomeFollowingView|MainTabBarView|fetchRecommendations" SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift` + - 기대 결과: `MainHomeView`는 탭별 루트 View를 조합하고, `fetchRecommendations`를 직접 호출하지 않으며, 탭바 중복 생성은 없어야 한다. + +- [ ] **Task 4.2: 추천/랭킹/팔로잉 하위 폴더와 루트 View 준비** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift` + - 이동/정리: `SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift` + - 작업 내용: + - 추천 탭 콘텐츠 조립은 `MainHomeRecommendationView`에서 담당한다. + - 추천 API 로딩/에러/섹션 상태는 `MainHomeRecommendationViewModel`에서 담당한다. + - 랭킹/팔로잉 탭은 이번 추천 API 범위 밖이므로 placeholder 또는 최소 루트 View만 준비하고 추천 섹션 코드를 넣지 않는다. + - 기존 `MainHomeViewModel`이 추천 전용 상태만 가진다면 `MainHomeRecommendationViewModel`로 이동/이름 변경한다. + - 검증 기준: + - 실행 명령: `find SodaLive/Sources/V2/Main/Home -maxdepth 3 -type f | sort` + - 기대 결과: `Recommendation`, `Ranking`, `Following` 하위 폴더의 루트 View 파일이 확인된다. + +- [ ] **Task 4.3: MainView 홈 탭 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/MainView.swift` + - 작업 내용: + - `.home` 분기에서 placeholder 대신 `MainHomeView()`를 표시한다. + - 다른 탭 분기와 기존 이벤트 팝업/회원 정보 초기화 흐름은 변경하지 않는다. + - 검증 기준: + - 실행 명령: `rg "case \\.home|MainHomeView|MainPlaceholderTabView" SodaLive/Sources/V2/Main/MainView.swift` + - 기대 결과: `.home`은 `MainHomeView()`를 표시하고, 다른 탭은 기존 동작을 유지한다. + +- [ ] **Task 4.4: 추천 탭 상세 진입 guard 적용** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/MainView.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` + - 확인: `SodaLive/Sources/Main/Home/HomeView.swift` + - 확인: `SodaLive/Sources/Home/HomeTabView.swift` + - 작업 내용: + - 신규 추천 탭의 API 호출과 섹션 노출 자체는 막지 않고, 라이브/크리에이터/오디오 콘텐츠/AI 캐릭터/커뮤니티 등 상세 진입 탭 액션 앞에서 guard를 적용한다. + - `MainHomeView`와 `MainHomeRecommendationView`는 직접 로그인/본인인증/민감 콘텐츠 설정 로직을 복사하지 않고, `MainView`에서 전달받은 callback으로 탭 이벤트만 올린다. + - `MainView`에 이미 있는 `token`, `auth`, `isShowAuthView`, `isShowAuthConfirmView`, `pendingAction`, `authConfirmDialog`, `authView` 인프라를 재사용한다. + - 토큰이 비어 있으면 `AppState.shared.setAppStep(step: .login)`으로 이동한다. + - 민감/성인 콘텐츠 진입이 필요한 항목은 한국 사용자이고 `auth == false`이면 `pendingAction`에 원래 이동 액션을 저장하고 `isShowAuthConfirmView = true`로 본인인증 dialog를 띄운다. + - 민감 콘텐츠 보기 설정이 꺼져 있으면 `AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)` 후 `AppState.shared.setAppStep(step: .contentViewSettings)`로 이동한다. + - guard 통과 후에만 `liveDetail`, `creatorDetail`, `contentDetail`, `characterDetail` 등 실제 상세 이동을 실행한다. + - 라이브 항목에 성인 여부 guard를 적용하려면 `HomeLiveItem` 응답에 `isAdult` 또는 동등한 필드가 필요하므로, 필드가 없으면 서버 필터링 전제인지 확인하고 임의 판단하지 않는다. + - 검증 기준: + - 실행 명령: `rg "pendingAction|isShowAuthConfirmView|setPendingContentSettingsGuideMessage|adultContentEnableGuide|contentViewSettings|MainHomeView\\(|MainHomeRecommendationView\\(" SodaLive/Sources/V2/Main/MainView.swift SodaLive/Sources/V2/Main/Home` + - 기대 결과: 신규 추천 탭 상세 진입 callback이 기존 인증/민감 콘텐츠 guard 인프라를 재사용한다. + - 수동 확인: `MainHomeView`, `MainHomeRecommendationView` 내부에 Bootpay 인증 UI를 중복 구현하지 않아야 한다. + +### Phase 5: 현재 라이브 섹션 + +- [ ] **Task 5.1: 현재 라이브 섹션 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeLiveSection.swift` + - 작업 내용: + - `HomeLiveItem.roomId`, `creatorNickname`, `creatorProfileImage`로 라이브 아이템을 표시한다. + - 데이터가 비어 있으면 섹션 제목과 컨테이너를 표시하지 않는다. + - 아이템 터치 이동은 라우팅 규칙이 명확한 경우에만 연결한다. + - 검증 기준: + - 실행 명령: `rg "MainHomeLiveSection|roomId|creatorNickname|creatorProfileImage" SodaLive/Sources/V2/Main/Home/Recommendation` + - 기대 결과: 라이브 섹션이 신규 응답 필드를 기준으로 구성된다. + +### Phase 6: 배너 섹션 + +- [ ] **Task 6.1: 추천 배너 섹션 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBannerSection.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/Banner/BannerCarousel.swift` + - 확인/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` + - 작업 내용: + - `banners: List`를 표시할 Swift 모델을 확정한다. + - 기존 `BannerCarousel`로 충분하면 재사용하고, 홈 전용 wrapper만 작성한다. + - 배너 이동 규칙은 미확정이면 탭 액션을 임의 구현하지 않는다. + - 검증 기준: + - 실행 명령: `rg "RecommendationBannerResponse|HomeBanner|BannerCarousel|MainHomeBannerSection" SodaLive/Sources/V2/Main/Home/Recommendation SodaLive/Sources/V2/Component/Banner` + - 기대 결과: 배너 응답 모델과 화면 조합이 확인된다. + +### Phase 7: 최근 활동 크리에이터 섹션 + +- [ ] **Task 7.1: 최근 활동 크리에이터 섹션 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeActiveCreatorSection.swift` + - 작업 내용: + - `creatorNickname`, `creatorProfileImage`, `activityType`, `activityAt`, `targetId`를 사용한다. + - `activityType`은 `RecommendedActivityType` 변환 결과를 표시한다. + - 데이터가 비어 있으면 섹션을 표시하지 않는다. + - 검증 기준: + - 실행 명령: `rg "MainHomeActiveCreatorSection|activityType|activityAt|targetId|RecommendedActivityType" SodaLive/Sources/V2/Main/Home/Recommendation` + - 기대 결과: 최근 활동 섹션이 신규 응답 필드와 활동 타입 변환을 사용한다. + +### Phase 8: 최근 데뷔한 크리에이터 섹션 + +- [ ] **Task 8.1: 최근 데뷔 크리에이터 섹션 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeRecentDebutCreatorSection.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/Creator/CreatorProfileGrid.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/Creator/CreatorProfileItem.swift` + - 작업 내용: + - `HomeCreatorItem.creatorId`, `creatorNickname`, `creatorProfileImage`를 사용한다. + - 기존 `CreatorProfileGrid`와 `CreatorProfileItem`을 우선 재사용한다. + - 데이터가 비어 있으면 섹션을 표시하지 않는다. + - 검증 기준: + - 실행 명령: `rg "MainHomeRecentDebutCreatorSection|CreatorProfileGrid|CreatorProfileItem|creatorProfileImage" SodaLive/Sources/V2/Main/Home/Recommendation SodaLive/Sources/V2/Component/Creator` + - 기대 결과: 최근 데뷔 섹션이 공용 Creator widget을 재사용한다. + +### Phase 9: 처음 만나는 오디오 섹션 + +- [ ] **Task 9.1: AudioContentCardView 조건부 태그 보완** + - 대상 파일: + - 확인/보완: `SodaLive/Sources/V2/Component/AudioContentCard.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeFirstAudioContentSection.swift` + - 작업 내용: + - `HomeFirstAudioContentItem.isPointAvailable == true`이면 `ic_content_tag_point` 시각요소를 표시한다. + - 오리지널 작품이면 `ic_content_tag_original` 시각요소를 표시한다. + - `price == 0`이면 `무료` 텍스트를 표시한다. + - 크리에이터의 첫 번째 콘텐츠이면 `FIRST` 표시를 보여준다. + - 조건에 해당하지 않는 요소는 렌더링하지 않는다. + - 오리지널/첫 콘텐츠 판단 필드가 응답에 없으면 임의 추정하지 않고 Open Question으로 유지한다. + - 검증 기준: + - 실행 명령: `rg "ic_content_tag_point|ic_content_tag_original|isPointAvailable|무료|FIRST|MainHomeFirstAudioContentSection" SodaLive/Sources/V2` + - 기대 결과: 오디오 카드 조건부 시각요소와 섹션 조합이 확인된다. + +### Phase 10: AI 캐릭터 섹션 + +- [ ] **Task 10.1: AI 캐릭터 섹션 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeAiCharacterSection.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/Card/AiCharacterCard.swift` + - 작업 내용: + - `characterId`, `creatorId`, `name`, `description`, `profileImage`, `totalChatCount`, `originalWorkTitle`를 사용한다. + - 기존 `AiCharacterCard`로 충분하면 재사용한다. + - 데이터가 비어 있으면 섹션을 표시하지 않는다. + - 검증 기준: + - 실행 명령: `rg "MainHomeAiCharacterSection|AiCharacterCard|totalChatCount|originalWorkTitle|profileImage" SodaLive/Sources/V2` + - 기대 결과: AI 캐릭터 섹션이 신규 응답 필드를 기준으로 구성된다. + +### Phase 11: 장르의 크리에이터 섹션 + +- [ ] **Task 11.1: 장르 크리에이터 그룹 섹션 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeGenreCreatorSection.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeCreatorGroupSection.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/Creator/CreatorProfileGrid.swift` + - 작업 내용: + - `HomeGenreCreatorGroupItem.genreName`과 `creators`를 사용한다. + - 그룹별 크리에이터 목록은 기존 Creator widget을 재사용한다. + - 모두 팔로우 API가 이번 범위에 남아 있다면 현재 섹션의 `creatorId` 목록으로만 호출한다. + - 데이터가 비어 있으면 섹션을 표시하지 않는다. + - 검증 기준: + - 실행 명령: `rg "MainHomeGenreCreatorSection|MainHomeCreatorGroupSection|genreName|creators|followAll" SodaLive/Sources/V2/Main/Home/Recommendation` + - 기대 결과: 장르 그룹 섹션과 그룹 재사용 구조가 확인된다. + +### Phase 12: 최근 응원이 많은 크리에이터 섹션 + +- [ ] **Task 12.1: 응원 크리에이터 섹션 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeCheerCreatorSection.swift` + - 확인/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeCreatorGroupSection.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/Creator/CreatorProfileGrid.swift` + - 작업 내용: + - `cheerCreators`의 `creatorId`, `creatorNickname`, `creatorProfileImage`를 사용한다. + - 장르 섹션과 같은 그룹 UI를 재사용한다. + - 모두 팔로우 API가 이번 범위에 남아 있다면 응원 섹션의 `creatorId` 목록으로만 호출한다. + - 데이터가 비어 있으면 섹션을 표시하지 않는다. + - 검증 기준: + - 실행 명령: `rg "MainHomeCheerCreatorSection|cheerCreators|MainHomeCreatorGroupSection|CreatorProfileGrid" SodaLive/Sources/V2/Main/Home/Recommendation` + - 기대 결과: 응원 섹션이 장르 그룹 UI를 재사용한다. + +### Phase 13: 인기 커뮤니티 섹션과 FeedCommunityView + +- [ ] **Task 13.1: FeedCommunityView/CommunityPostCard 요구 보완** + - 대상 파일: + - 확인/보완: `SodaLive/Sources/V2/Component/Card/CommunityPostCard.swift` + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomePopularCommunitySection.swift` + - 작업 내용: + - 키워드 영역을 제거한다. + - `imageUrl`이 있으면 이미지를 표시한다. + - `price > 0 && existOrdered == false`이면 유료 미구매 UI를 표시한다. + - `price <= 0 || existOrdered == true`이면 일반 이미지로 표시한다. + - `구매완료` 버튼은 표시하지 않는다. + - `creatorProfileImage`, `createdAt`, `likeCount`, `commentCount`를 표시한다. + - `audioUrl` 재생 UI는 확정 전까지 추가하지 않는다. + - 검증 기준: + - 실행 명령: `rg "MainHomePopularCommunitySection|CommunityPostCard|imageUrl|audioUrl|existOrdered|구매완료" SodaLive/Sources/V2` + - 기대 결과: 커뮤니티 섹션과 카드 조건 분기가 확인되고 `구매완료` 표시 구현은 없어야 한다. + +### Phase 14: 사업자 정보 섹션 + +- [ ] **Task 14.1: 사업자 정보 더보기/접기 섹션 작성/보완** + - 대상 파일: + - 생성/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/Components/MainHomeBusinessInfoSection.swift` + - 확인/보완: `SodaLive/Sources/V2/Component/Text/ExpandableTextView.swift` + - 작업 내용: + - 추천 탭 UI 마지막에 사업자 정보 섹션을 배치한다. + - 섹션 width는 화면에 채운다. + - 기본 3줄 말줄임표와 `더보기`를 표시한다. + - 더보기 터치 시 전체 표시와 `접기`를 제공한다. + - 외부 라이브러리를 사용하지 않는다. + - 검증 기준: + - 실행 명령: `rg "MainHomeBusinessInfoSection|ExpandableTextView|lineLimit|더보기|접기" SodaLive/Sources/V2` + - 기대 결과: 사업자 정보 섹션과 확장 텍스트 구현이 확인된다. + +### Phase 15: 섹션 조립 순서와 빈 섹션 처리 + +- [ ] **Task 15.1: MainHomeRecommendationView에 섹션 순서 반영** + - 대상 파일: + - 확인/보완: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` + - 작업 내용: + - 섹션 순서를 PRD와 맞춘다. + - 현재 라이브, 배너, 최근 활동, 최근 데뷔, 처음 만나는 오디오, AI 캐릭터, 장르, 응원, 인기 커뮤니티, 사업자 정보 순서로 배치한다. + - 각 데이터 배열이 비어 있으면 섹션 제목과 컨테이너까지 숨긴다. + - 검증 기준: + - 실행 명령: `rg "MainHomeLiveSection|MainHomeBannerSection|MainHomeActiveCreatorSection|MainHomeRecentDebutCreatorSection|MainHomeFirstAudioContentSection|MainHomeAiCharacterSection|MainHomeGenreCreatorSection|MainHomeCheerCreatorSection|MainHomePopularCommunitySection|MainHomeBusinessInfoSection" SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` + - 기대 결과: 모든 구현 대상 섹션이 올바른 순서로 조립된다. + +### Phase 16: 프로젝트 포함과 빌드 검증 + +- [ ] **Task 16.1: Xcode 프로젝트 포함 여부 확인** + - 대상 파일: + - 확인/보완: `SodaLive.xcodeproj/project.pbxproj` + - 확인: `SodaLive/Sources/V2/Main/Home/**` + - 확인: `SodaLive/Sources/V2/Component/**` + - 작업 내용: + - 신규 Swift 파일이 빌드 대상에 포함되는지 확인한다. + - 파일 시스템 동기화 방식으로 자동 포함되면 프로젝트 파일을 수정하지 않는다. + - 프로젝트 파일 수정이 필요한 경우에만 최소 변경한다. - 검증 기준: - 실행 명령: `plutil -lint SodaLive.xcodeproj/project.pbxproj` - - 기대 결과: 프로젝트 파일을 수정했거나 확인이 필요할 때 `OK`가 출력된다. - - 수동 확인: 프로젝트 파일 수정이 불필요하면 변경하지 않아야 한다. + - 기대 결과: `OK` -- [ ] **Task 4.2: 정적 점검** +- [ ] **Task 16.2: 정적 점검과 빌드** - 대상 파일: - 확인: `SodaLive/Sources/V2/Main/Home/**` - 확인: `SodaLive/Sources/V2/Component/**` - 확인: `SodaLive/Sources/Home/**` - 작업 내용: - 제외 섹션, 기존 홈 API 오염, Figma URL, 공백 오류를 확인한다. + - 가능한 빌드 명령을 실행한다. - 검증 기준: - 실행 명령: `rg "추천 필모그래피|또 다른 모습" SodaLive/Sources/V2/Main/Home SodaLive/Sources/V2/Component` - 기대 결과: 검색 결과가 없어야 한다. @@ -323,61 +392,64 @@ - 기대 결과: 검색 결과가 없어야 한다. - 실행 명령: `git diff --check` - 기대 결과: 출력 없이 성공한다. - -### Phase 5: 빌드와 기능 검증 - -- [ ] **Task 5.1: 빌드 검증** - - 대상 파일: - - 확인: `SodaLive.xcworkspace` - - 확인: `SodaLive.xcodeproj` - - 확인: `SodaLive/Sources/V2/Main/Home/**` - - 확인: `SodaLive/Sources/V2/Component/**` - - 작업 내용: - - 기본 빌드를 실행한다. - - 필요 시 운영 스킴 빌드를 실행한다. - - 테스트 액션 확인이 필요하면 테스트 명령을 실행하고, 스킴 미구성 시 결과를 검증 기록에 남긴다. - - 검증 기준: - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` - 기대 결과: `BUILD SUCCEEDED` - - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` - - 기대 결과: `BUILD SUCCEEDED` - - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test` - - 기대 결과: 테스트가 실행되거나, 스킴 미구성 메시지를 검증 기록에 남긴다. -- [ ] **Task 5.2: 기능 검증** - - 대상 파일: - - 확인: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift` - - 확인: `SodaLive/Sources/V2/Main/Home/MainHomeViewModel.swift` - - 확인: `SodaLive/Sources/V2/Main/Home/Components/**` - - 확인: `SodaLive/Sources/V2/Component/**` - - 작업 내용: - - 추천 API 성공/실패, 빈 섹션, 모두 팔로우 성공/실패, 사업자 정보 확장/접기, 활동 타입 I18n, 커뮤니티 카드 variant를 확인한다. - - 검증 기준: - - 수동 확인: 추천 API 성공 시 각 섹션이 응답 데이터로 표시된다. - - 수동 확인: 빈 배열 섹션은 제목과 컨테이너까지 숨겨진다. - - 수동 확인: 모두 팔로우 버튼 터치 후 API 성공 시 해당 버튼만 `모두 팔로우 완료`로 바뀐다. - - 수동 확인: 모두 팔로우 실패 시 버튼 상태가 변경되지 않고 오류가 표시된다. - - 수동 확인: 사업자 정보 기본 3줄 말줄임표, 더보기, 전체 표시, 접기가 동작한다. - - 수동 확인: 활동 타입 `LIVE`, `LIVE_REPLAY`, `AUDIO`, `COMMUNITY`가 I18n 문구로 표시된다. - - 수동 확인: 커뮤니티 카드 3개 variant가 조건에 맞게 표시되고 `구매완료` 캡슐이 표시되지 않는다. +### Phase 17: 문서와 검증 기록 갱신 -### Phase 6: 문서와 체크리스트 갱신 - -- [ ] **Task 6.1: 문서/체크리스트 갱신** +- [ ] **Task 17.1: PRD/계획 문서 갱신** - 대상 파일: - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md` - 수정: `docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` - 작업 내용: - - 구현 중 확정된 배너 이동 규칙, 표시 개수, 타입 보정 사항이 있으면 PRD를 먼저 갱신한다. - - 완료한 task 체크박스를 실제 상태에 맞게 `- [x]`로 갱신한다. + - 구현 중 확정된 배너 이동 규칙, 표시 개수, 오디오 태그 판단 필드, 커뮤니티 `audioUrl` 표시 여부가 있으면 PRD를 먼저 갱신한다. + - 완료한 task 체크박스를 실제 상태에 맞게 갱신한다. - 실행한 검증 명령과 결과를 이 문서 하단 검증 기록에 누적한다. - 검증 기준: - 실행 명령: `rg "### Phase [0-9]+:|- \\[[ x]\\] \\*\\*Task [0-9]+\\.[0-9]+:" docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` - 기대 결과: phase heading과 `Task N.N` 형식 체크박스가 검색된다. - - 수동 확인: 새 문서를 만들지 않고 기존 PRD/계획 문서에 이어서 기록해야 한다. ## 검증 기록 +### 2026-06-26 MainHome 탭별 폴더 구조 반영 + +- 목적: `MainHomeView`에 추천/랭킹/팔로잉 콘텐츠 조립이 누적되지 않도록 탭별 하위 폴더와 루트 View 구조를 PRD/계획 문서에 반영 +- 수행 내용: + - `prd.md`에 `MainHomeView`는 홈 상단 shell, 추천/랭킹/팔로잉 탭 콘텐츠는 별도 View로 분리한다는 기준 추가 + - 추천 탭 루트/모델/API/ViewModel/섹션 경로를 `SodaLive/Sources/V2/Main/Home/Recommendation/**` 기준으로 변경 + - 랭킹/팔로잉 탭 루트 파일 후보를 `SodaLive/Sources/V2/Main/Home/Ranking/MainHomeRankingView.swift`, `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift`로 추가 + - Phase 4를 `MainHomeView 탭 shell과 탭별 루트 View`로 재정의하고, 추천 섹션 조립은 `MainHomeRecommendationView`에서 처리하도록 변경 +- 검증: + - `rg "MainHomeRecommendationView|MainHomeRankingView|MainHomeFollowingView|Recommendation/Components|Recommendation/Repository|Recommendation/Models" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 탭별 루트 View와 추천 탭 하위 경로 반영 확인 + - `rg "### Phase [0-9]+:|- \\[[ x]\\] \\*\\*Task [0-9]+\\.[0-9]+:" docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, Phase/Task 형식 확인 + - `git diff --check -- docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 출력 없이 성공 확인 + +### 2026-06-26 상세 진입 guard 요구사항 추가 + +- 목적: 신규 추천 탭의 상세 진입 시 기존 홈과 같은 로그인/본인인증/민감 콘텐츠 보기 설정 guard를 적용하도록 PRD와 계획 문서 보완 +- 수행 내용: + - `prd.md`에 `추천 탭 상세 진입 guard` 요구사항 추가 + - `plan-task.md`의 Phase 4에 `Task 4.3: 추천 탭 상세 진입 guard 적용` 추가 + - `MainHomeView`는 탭 이벤트만 올리고, V2 `MainView`의 기존 인증 인프라를 재사용하는 방향으로 명시 + - `HomeLiveItem`에 `isAdult` 또는 동등한 필드가 필요하다는 Open Question 반영 +- 검증: + - `rg "추천 탭 상세 진입 guard|pendingAction|isShowAuthConfirmView|setPendingContentSettingsGuideMessage|HomeLiveItem.*isAdult|Task 4.3" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, guard 요구사항과 Task 반영 확인 + - `rg "### Phase [0-9]+:|- \\[[ x]\\] \\*\\*Task [0-9]+\\.[0-9]+:" docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, Phase/Task 형식 확인 + - `git diff --check -- docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 출력 없이 성공 확인 + +### 2026-06-26 PRD/계획 문서 재분류 + +- 목적: 신규 API 응답 스펙, FeedCommunityView, AudioContentCardView, 사업자 정보 요구사항을 PRD와 계획 문서에 반영하고, 기존 Phase를 섹션 단위 Phase로 재분류 +- 수행 내용: + - `prd.md`를 신규 응답 스펙 기준으로 갱신 + - `plan-task.md`의 기존 Phase/완료 상태를 새 작업 분류 기준으로 재작성 + - UI 작업은 현재 라이브, 배너, 최근 활동, 최근 데뷔, 처음 만나는 오디오, AI 캐릭터, 장르, 응원, 인기 커뮤니티, 사업자 정보 섹션별 Phase로 분리 + - 기존 검증 기록은 아래에 보존 +- 검증: + - `rg "GET /api/v2/home/recommendations|RecommendationBannerResponse|FeedCommunityView|AudioContentCardView|추천 필모그래피|또 다른 모습|node-id=309-19774|node-id=309-19775" docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 신규 API/응답 타입/FeedCommunityView/AudioContentCardView/제외 섹션/Figma 노드 반영 확인 + - `rg "### Phase [0-9]+:|- \\[[ x]\\] \\*\\*Task [0-9]+\\.[0-9]+:" docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, Phase heading과 Task 체크박스 형식 확인 + - `git diff --check -- docs/20260602_메인_홈_추천_UI_API_연동/prd.md docs/20260602_메인_홈_추천_UI_API_연동/plan-task.md` 실행, 출력 없이 성공 확인 + ### 2026-06-02 계획 문서 생성 - 목적: `docs/20260602_메인_홈_추천_UI_API_연동/prd.md`를 기준으로 구현 전 계획/TASK 문서 작성 diff --git a/docs/20260602_메인_홈_추천_UI_API_연동/prd.md b/docs/20260602_메인_홈_추천_UI_API_연동/prd.md index 4048895e..b8029c85 100644 --- a/docs/20260602_메인_홈_추천_UI_API_연동/prd.md +++ b/docs/20260602_메인_홈_추천_UI_API_연동/prd.md @@ -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` 디코딩 @@ -50,7 +63,7 @@ ```kotlin data class HomeRecommendationResponse( val lives: List, - val banners: List, + val banners: List, val recentlyActiveCreators: List, val recentDebutCreators: List, val firstAudioContents: List, @@ -59,33 +72,73 @@ data class HomeRecommendationResponse( val cheerCreators: List, val popularCommunityPosts: List ) -``` -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? +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 +) + +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, 커뮤니티 카드 상태, 오디오 카드 태그 상태, 사업자 정보 더보기/접기를 확인한다.