From 981c02a8e1dadc00547744001297e065e4cbfe74 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Tue, 7 Jul 2026 11:54:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(content):=20=EC=B6=94=EC=B2=9C=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=EB=B3=B4=EA=B8=B0=EB=A5=BC=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive.xcodeproj/project.pbxproj | 638 ++++++++++-------- SodaLive/Sources/App/AppStep.swift | 2 + SodaLive/Sources/ContentView.swift | 5 + .../AudioContentThumbnailCard.swift | 14 + .../Content/All/MainContentAllViewModel.swift | 19 +- .../V2/Main/Content/MainContentView.swift | 16 +- .../Overview/ContentOverviewView.swift | 108 +++ .../Overview/ContentOverviewViewModel.swift | 118 ++++ .../Models/ContentOverviewPageResponse.swift | 27 + .../Repository/ContentOverviewApi.swift | 44 ++ .../ContentOverviewRepository.swift | 16 + ...ainContentAudioHorizontalCardSection.swift | 3 +- .../MainContentAudioListCarouselSection.swift | 3 +- .../MainContentVoiceOnOnlySection.swift | 3 +- .../MainContentRecommendationView.swift | 10 + SodaLive/Sources/V2/Main/MainView.swift | 9 +- .../plan-task.md | 217 ++++++ .../20260707_메인_콘텐츠_추천_전체보기/prd.md | 211 ++++++ 18 files changed, 1169 insertions(+), 294 deletions(-) create mode 100644 SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewView.swift create mode 100644 SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewViewModel.swift create mode 100644 SodaLive/Sources/V2/Main/Content/Overview/Models/ContentOverviewPageResponse.swift create mode 100644 SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewApi.swift create mode 100644 SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewRepository.swift create mode 100644 docs/20260707_메인_콘텐츠_추천_전체보기/plan-task.md create mode 100644 docs/20260707_메인_콘텐츠_추천_전체보기/prd.md diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 65e731ba..55f8f432 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -7,66 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - C0A711000000000000000001 /* MainContentAllTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000001 /* MainContentAllTabResponse.swift */; }; - C0A712000000000000000001 /* MainContentAllTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000001 /* MainContentAllTabResponse.swift */; }; - C0A713000000000000000001 /* MainContentAllTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000001 /* MainContentAllTabResponse.swift */; }; - C0A711000000000000000002 /* MainContentAllApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000002 /* MainContentAllApi.swift */; }; - C0A712000000000000000002 /* MainContentAllApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000002 /* MainContentAllApi.swift */; }; - C0A713000000000000000002 /* MainContentAllApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000002 /* MainContentAllApi.swift */; }; - C0A711000000000000000003 /* MainContentAllRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000003 /* MainContentAllRepository.swift */; }; - C0A712000000000000000003 /* MainContentAllRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000003 /* MainContentAllRepository.swift */; }; - C0A713000000000000000003 /* MainContentAllRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000003 /* MainContentAllRepository.swift */; }; - C0A711000000000000000004 /* MainContentAllViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000004 /* MainContentAllViewModel.swift */; }; - C0A712000000000000000004 /* MainContentAllViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000004 /* MainContentAllViewModel.swift */; }; - C0A713000000000000000004 /* MainContentAllViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000004 /* MainContentAllViewModel.swift */; }; - C0A711000000000000000005 /* MainContentAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000005 /* MainContentAllView.swift */; }; - C0A712000000000000000005 /* MainContentAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000005 /* MainContentAllView.swift */; }; - C0A713000000000000000005 /* MainContentAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000005 /* MainContentAllView.swift */; }; - C0A711000000000000000006 /* MainContentSeriesThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */; }; - C0A712000000000000000006 /* MainContentSeriesThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */; }; - C0A713000000000000000006 /* MainContentSeriesThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */; }; - C0A701000000000000000001 /* MainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000001 /* MainContentView.swift */; }; - C0A701000000000000000002 /* MainContentTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000002 /* MainContentTab.swift */; }; - C0A701000000000000000003 /* MainContentRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000003 /* MainContentRecommendationView.swift */; }; - C0A701000000000000000004 /* MainContentRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */; }; - C0A701000000000000000005 /* AudioRecommendationsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000005 /* AudioRecommendationsResponse.swift */; }; - C0A701000000000000000006 /* MainContentRecommendationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000006 /* MainContentRecommendationApi.swift */; }; - C0A701000000000000000007 /* MainContentRecommendationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000007 /* MainContentRecommendationRepository.swift */; }; - C0A701000000000000000008 /* MainContentAudioBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000008 /* MainContentAudioBannerSection.swift */; }; - C0A701000000000000000009 /* MainContentAudioHorizontalCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */; }; - C0A70100000000000000000A /* MainContentAudioListCarouselSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */; }; - C0A70100000000000000000B /* MainContentVoiceOnOnlySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */; }; - C0A70100000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */; }; - C0A70100000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */; }; - C0A70100000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */; }; - C0A702000000000000000001 /* MainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000001 /* MainContentView.swift */; }; - C0A702000000000000000002 /* MainContentTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000002 /* MainContentTab.swift */; }; - C0A702000000000000000003 /* MainContentRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000003 /* MainContentRecommendationView.swift */; }; - C0A702000000000000000004 /* MainContentRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */; }; - C0A702000000000000000005 /* AudioRecommendationsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000005 /* AudioRecommendationsResponse.swift */; }; - C0A702000000000000000006 /* MainContentRecommendationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000006 /* MainContentRecommendationApi.swift */; }; - C0A702000000000000000007 /* MainContentRecommendationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000007 /* MainContentRecommendationRepository.swift */; }; - C0A702000000000000000008 /* MainContentAudioBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000008 /* MainContentAudioBannerSection.swift */; }; - C0A702000000000000000009 /* MainContentAudioHorizontalCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */; }; - C0A70200000000000000000A /* MainContentAudioListCarouselSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */; }; - C0A70200000000000000000B /* MainContentVoiceOnOnlySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */; }; - C0A70200000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */; }; - C0A70200000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */; }; - C0A70200000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */; }; - C0A703000000000000000001 /* MainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000001 /* MainContentView.swift */; }; - C0A703000000000000000002 /* MainContentTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000002 /* MainContentTab.swift */; }; - C0A703000000000000000003 /* MainContentRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000003 /* MainContentRecommendationView.swift */; }; - C0A703000000000000000004 /* MainContentRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */; }; - C0A703000000000000000005 /* AudioRecommendationsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000005 /* AudioRecommendationsResponse.swift */; }; - C0A703000000000000000006 /* MainContentRecommendationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000006 /* MainContentRecommendationApi.swift */; }; - C0A703000000000000000007 /* MainContentRecommendationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000007 /* MainContentRecommendationRepository.swift */; }; - C0A703000000000000000008 /* MainContentAudioBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000008 /* MainContentAudioBannerSection.swift */; }; - C0A703000000000000000009 /* MainContentAudioHorizontalCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */; }; - C0A70300000000000000000A /* MainContentAudioListCarouselSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */; }; - C0A70300000000000000000B /* MainContentVoiceOnOnlySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */; }; - C0A70300000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */; }; - C0A70300000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */; }; - C0A70300000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */; }; 00C796627A6758D271215EC5 /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; }; 042BEE0C973700FB5F4540CD /* I18n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826D626B7714E5C28ED31074 /* I18n.swift */; }; 0470E68C6F302A131264E3AF /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; }; @@ -1700,42 +1640,6 @@ 753741799DEF278DC4C515AA /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 8069DA64D0A280105DF612B6 /* KakaoSDKAuth */; }; 7A1DDAAE227BF176BDEF1CB9 /* TextTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61608194B084224DB6B78B5 /* TextTabBar.swift */; }; 7AB453CE38CA0B3D612E9467 /* AudioContentCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E92246F5C1254CBED72AD9AD /* AudioContentCard.swift */; }; - C0A704000000000000000001 /* AudioContentListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A702000000000000000000 /* AudioContentListRow.swift */; }; - C0A704000000000000000002 /* AudioContentListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A702000000000000000000 /* AudioContentListRow.swift */; }; - C0A704000000000000000003 /* AudioContentListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A702000000000000000000 /* AudioContentListRow.swift */; }; - C0A706000000000000000001 /* AudioContentThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A706000000000000000000 /* AudioContentThumbnailCard.swift */; }; - C0A706000000000000000002 /* AudioContentThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A706000000000000000000 /* AudioContentThumbnailCard.swift */; }; - C0A706000000000000000003 /* AudioContentThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A706000000000000000000 /* AudioContentThumbnailCard.swift */; }; - C0A721000000000000000001 /* AudioRankingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000001 /* AudioRankingResponse.swift */; }; - C0A721000000000000000002 /* MainContentRankingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000002 /* MainContentRankingApi.swift */; }; - C0A721000000000000000003 /* MainContentRankingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000003 /* MainContentRankingRepository.swift */; }; - C0A721000000000000000004 /* MainContentRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000004 /* MainContentRankingViewModel.swift */; }; - C0A721000000000000000005 /* MainContentRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000005 /* MainContentRankingView.swift */; }; - C0A721000000000000000006 /* MainContentRankChangeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000006 /* MainContentRankChangeBadge.swift */; }; - C0A721000000000000000007 /* MainContentAudioRankingTopCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */; }; - C0A721000000000000000008 /* MainContentAudioRankingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000008 /* MainContentAudioRankingRow.swift */; }; - C0A721000000000000000009 /* MainContentAudioRankingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */; }; - C0A72100000000000000000A /* MainContentAudioRankingThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */; }; - C0A722000000000000000001 /* AudioRankingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000001 /* AudioRankingResponse.swift */; }; - C0A722000000000000000002 /* MainContentRankingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000002 /* MainContentRankingApi.swift */; }; - C0A722000000000000000003 /* MainContentRankingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000003 /* MainContentRankingRepository.swift */; }; - C0A722000000000000000004 /* MainContentRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000004 /* MainContentRankingViewModel.swift */; }; - C0A722000000000000000005 /* MainContentRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000005 /* MainContentRankingView.swift */; }; - C0A722000000000000000006 /* MainContentRankChangeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000006 /* MainContentRankChangeBadge.swift */; }; - C0A722000000000000000007 /* MainContentAudioRankingTopCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */; }; - C0A722000000000000000008 /* MainContentAudioRankingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000008 /* MainContentAudioRankingRow.swift */; }; - C0A722000000000000000009 /* MainContentAudioRankingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */; }; - C0A72200000000000000000A /* MainContentAudioRankingThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */; }; - C0A723000000000000000001 /* AudioRankingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000001 /* AudioRankingResponse.swift */; }; - C0A723000000000000000002 /* MainContentRankingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000002 /* MainContentRankingApi.swift */; }; - C0A723000000000000000003 /* MainContentRankingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000003 /* MainContentRankingRepository.swift */; }; - C0A723000000000000000004 /* MainContentRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000004 /* MainContentRankingViewModel.swift */; }; - C0A723000000000000000005 /* MainContentRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000005 /* MainContentRankingView.swift */; }; - C0A723000000000000000006 /* MainContentRankChangeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000006 /* MainContentRankChangeBadge.swift */; }; - C0A723000000000000000007 /* MainContentAudioRankingTopCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */; }; - C0A723000000000000000008 /* MainContentAudioRankingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000008 /* MainContentAudioRankingRow.swift */; }; - C0A723000000000000000009 /* MainContentAudioRankingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */; }; - C0A72300000000000000000A /* MainContentAudioRankingThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */; }; 7B2EE8B4132668F0267B10CB /* UserProfileChannelDonationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214C32BF44DF384FD6925E0B /* UserProfileChannelDonationView.swift */; }; 7BED9B1CFDC729EC4AF42B35 /* Radius.swift in Sources */ = {isa = PBXBuildFile; fileRef = E31B21FA615404F2E9E7839F /* Radius.swift */; }; 80209D5731C339163084890C /* LanguageSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716260E8214A329EE07CAAFA /* LanguageSettingsViewModel.swift */; }; @@ -1789,6 +1693,117 @@ BEAAA523CAD3198065A557BC /* EntityInfo-SodaLive-dev.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0ED95AC08C6481751AE9D97 /* EntityInfo-SodaLive-dev.generated.swift */; }; BF715734F77555C1A633DD90 /* PushNotificationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C21D69F717F8D80644F6AEE /* PushNotificationListView.swift */; }; C050596536F58A453B15461D /* EntityInfo-SodaLive.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E138A248EE3AFAFAF01E245 /* EntityInfo-SodaLive.generated.swift */; }; + C0A701000000000000000001 /* MainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000001 /* MainContentView.swift */; }; + C0A701000000000000000002 /* MainContentTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000002 /* MainContentTab.swift */; }; + C0A701000000000000000003 /* MainContentRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000003 /* MainContentRecommendationView.swift */; }; + C0A701000000000000000004 /* MainContentRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */; }; + C0A701000000000000000005 /* AudioRecommendationsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000005 /* AudioRecommendationsResponse.swift */; }; + C0A701000000000000000006 /* MainContentRecommendationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000006 /* MainContentRecommendationApi.swift */; }; + C0A701000000000000000007 /* MainContentRecommendationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000007 /* MainContentRecommendationRepository.swift */; }; + C0A701000000000000000008 /* MainContentAudioBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000008 /* MainContentAudioBannerSection.swift */; }; + C0A701000000000000000009 /* MainContentAudioHorizontalCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */; }; + C0A70100000000000000000A /* MainContentAudioListCarouselSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */; }; + C0A70100000000000000000B /* MainContentVoiceOnOnlySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */; }; + C0A70100000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */; }; + C0A70100000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */; }; + C0A70100000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */; }; + C0A702000000000000000001 /* MainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000001 /* MainContentView.swift */; }; + C0A702000000000000000002 /* MainContentTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000002 /* MainContentTab.swift */; }; + C0A702000000000000000003 /* MainContentRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000003 /* MainContentRecommendationView.swift */; }; + C0A702000000000000000004 /* MainContentRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */; }; + C0A702000000000000000005 /* AudioRecommendationsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000005 /* AudioRecommendationsResponse.swift */; }; + C0A702000000000000000006 /* MainContentRecommendationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000006 /* MainContentRecommendationApi.swift */; }; + C0A702000000000000000007 /* MainContentRecommendationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000007 /* MainContentRecommendationRepository.swift */; }; + C0A702000000000000000008 /* MainContentAudioBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000008 /* MainContentAudioBannerSection.swift */; }; + C0A702000000000000000009 /* MainContentAudioHorizontalCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */; }; + C0A70200000000000000000A /* MainContentAudioListCarouselSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */; }; + C0A70200000000000000000B /* MainContentVoiceOnOnlySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */; }; + C0A70200000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */; }; + C0A70200000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */; }; + C0A70200000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */; }; + C0A703000000000000000001 /* MainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000001 /* MainContentView.swift */; }; + C0A703000000000000000002 /* MainContentTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000002 /* MainContentTab.swift */; }; + C0A703000000000000000003 /* MainContentRecommendationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000003 /* MainContentRecommendationView.swift */; }; + C0A703000000000000000004 /* MainContentRecommendationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */; }; + C0A703000000000000000005 /* AudioRecommendationsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000005 /* AudioRecommendationsResponse.swift */; }; + C0A703000000000000000006 /* MainContentRecommendationApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000006 /* MainContentRecommendationApi.swift */; }; + C0A703000000000000000007 /* MainContentRecommendationRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000007 /* MainContentRecommendationRepository.swift */; }; + C0A703000000000000000008 /* MainContentAudioBannerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000008 /* MainContentAudioBannerSection.swift */; }; + C0A703000000000000000009 /* MainContentAudioHorizontalCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */; }; + C0A70300000000000000000A /* MainContentAudioListCarouselSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */; }; + C0A70300000000000000000B /* MainContentVoiceOnOnlySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */; }; + C0A70300000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */; }; + C0A70300000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */; }; + C0A70300000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */; }; + C0A704000000000000000001 /* AudioContentListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A702000000000000000000 /* AudioContentListRow.swift */; }; + C0A704000000000000000002 /* AudioContentListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A702000000000000000000 /* AudioContentListRow.swift */; }; + C0A704000000000000000003 /* AudioContentListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A702000000000000000000 /* AudioContentListRow.swift */; }; + C0A706000000000000000001 /* AudioContentThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A706000000000000000000 /* AudioContentThumbnailCard.swift */; }; + C0A706000000000000000002 /* AudioContentThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A706000000000000000000 /* AudioContentThumbnailCard.swift */; }; + C0A706000000000000000003 /* AudioContentThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A706000000000000000000 /* AudioContentThumbnailCard.swift */; }; + C0A711000000000000000001 /* MainContentAllTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000001 /* MainContentAllTabResponse.swift */; }; + C0A711000000000000000002 /* MainContentAllApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000002 /* MainContentAllApi.swift */; }; + C0A711000000000000000003 /* MainContentAllRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000003 /* MainContentAllRepository.swift */; }; + C0A711000000000000000004 /* MainContentAllViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000004 /* MainContentAllViewModel.swift */; }; + C0A711000000000000000005 /* MainContentAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000005 /* MainContentAllView.swift */; }; + C0A711000000000000000006 /* MainContentSeriesThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */; }; + C0A712000000000000000001 /* MainContentAllTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000001 /* MainContentAllTabResponse.swift */; }; + C0A712000000000000000002 /* MainContentAllApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000002 /* MainContentAllApi.swift */; }; + C0A712000000000000000003 /* MainContentAllRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000003 /* MainContentAllRepository.swift */; }; + C0A712000000000000000004 /* MainContentAllViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000004 /* MainContentAllViewModel.swift */; }; + C0A712000000000000000005 /* MainContentAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000005 /* MainContentAllView.swift */; }; + C0A712000000000000000006 /* MainContentSeriesThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */; }; + C0A713000000000000000001 /* MainContentAllTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000001 /* MainContentAllTabResponse.swift */; }; + C0A713000000000000000002 /* MainContentAllApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000002 /* MainContentAllApi.swift */; }; + C0A713000000000000000003 /* MainContentAllRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000003 /* MainContentAllRepository.swift */; }; + C0A713000000000000000004 /* MainContentAllViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000004 /* MainContentAllViewModel.swift */; }; + C0A713000000000000000005 /* MainContentAllView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000005 /* MainContentAllView.swift */; }; + C0A713000000000000000006 /* MainContentSeriesThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */; }; + C0A731000000000000000001 /* ContentOverviewPageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000001 /* ContentOverviewPageResponse.swift */; }; + C0A731000000000000000002 /* ContentOverviewApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000002 /* ContentOverviewApi.swift */; }; + C0A731000000000000000003 /* ContentOverviewRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000003 /* ContentOverviewRepository.swift */; }; + C0A731000000000000000004 /* ContentOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000004 /* ContentOverviewViewModel.swift */; }; + C0A731000000000000000005 /* ContentOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000005 /* ContentOverviewView.swift */; }; + C0A732000000000000000001 /* ContentOverviewPageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000001 /* ContentOverviewPageResponse.swift */; }; + C0A732000000000000000002 /* ContentOverviewApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000002 /* ContentOverviewApi.swift */; }; + C0A732000000000000000003 /* ContentOverviewRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000003 /* ContentOverviewRepository.swift */; }; + C0A732000000000000000004 /* ContentOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000004 /* ContentOverviewViewModel.swift */; }; + C0A732000000000000000005 /* ContentOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000005 /* ContentOverviewView.swift */; }; + C0A733000000000000000001 /* ContentOverviewPageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000001 /* ContentOverviewPageResponse.swift */; }; + C0A733000000000000000002 /* ContentOverviewApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000002 /* ContentOverviewApi.swift */; }; + C0A733000000000000000003 /* ContentOverviewRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000003 /* ContentOverviewRepository.swift */; }; + C0A733000000000000000004 /* ContentOverviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000004 /* ContentOverviewViewModel.swift */; }; + C0A733000000000000000005 /* ContentOverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A730000000000000000005 /* ContentOverviewView.swift */; }; + C0A721000000000000000001 /* AudioRankingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000001 /* AudioRankingResponse.swift */; }; + C0A721000000000000000002 /* MainContentRankingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000002 /* MainContentRankingApi.swift */; }; + C0A721000000000000000003 /* MainContentRankingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000003 /* MainContentRankingRepository.swift */; }; + C0A721000000000000000004 /* MainContentRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000004 /* MainContentRankingViewModel.swift */; }; + C0A721000000000000000005 /* MainContentRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000005 /* MainContentRankingView.swift */; }; + C0A721000000000000000006 /* MainContentRankChangeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000006 /* MainContentRankChangeBadge.swift */; }; + C0A721000000000000000007 /* MainContentAudioRankingTopCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */; }; + C0A721000000000000000008 /* MainContentAudioRankingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000008 /* MainContentAudioRankingRow.swift */; }; + C0A721000000000000000009 /* MainContentAudioRankingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */; }; + C0A72100000000000000000A /* MainContentAudioRankingThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */; }; + C0A722000000000000000001 /* AudioRankingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000001 /* AudioRankingResponse.swift */; }; + C0A722000000000000000002 /* MainContentRankingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000002 /* MainContentRankingApi.swift */; }; + C0A722000000000000000003 /* MainContentRankingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000003 /* MainContentRankingRepository.swift */; }; + C0A722000000000000000004 /* MainContentRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000004 /* MainContentRankingViewModel.swift */; }; + C0A722000000000000000005 /* MainContentRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000005 /* MainContentRankingView.swift */; }; + C0A722000000000000000006 /* MainContentRankChangeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000006 /* MainContentRankChangeBadge.swift */; }; + C0A722000000000000000007 /* MainContentAudioRankingTopCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */; }; + C0A722000000000000000008 /* MainContentAudioRankingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000008 /* MainContentAudioRankingRow.swift */; }; + C0A722000000000000000009 /* MainContentAudioRankingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */; }; + C0A72200000000000000000A /* MainContentAudioRankingThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */; }; + C0A723000000000000000001 /* AudioRankingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000001 /* AudioRankingResponse.swift */; }; + C0A723000000000000000002 /* MainContentRankingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000002 /* MainContentRankingApi.swift */; }; + C0A723000000000000000003 /* MainContentRankingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000003 /* MainContentRankingRepository.swift */; }; + C0A723000000000000000004 /* MainContentRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000004 /* MainContentRankingViewModel.swift */; }; + C0A723000000000000000005 /* MainContentRankingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000005 /* MainContentRankingView.swift */; }; + C0A723000000000000000006 /* MainContentRankChangeBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000006 /* MainContentRankChangeBadge.swift */; }; + C0A723000000000000000007 /* MainContentAudioRankingTopCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */; }; + C0A723000000000000000008 /* MainContentAudioRankingRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000008 /* MainContentAudioRankingRow.swift */; }; + C0A723000000000000000009 /* MainContentAudioRankingEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */; }; + C0A72300000000000000000A /* MainContentAudioRankingThumbnailCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */; }; C0D6E325AEFEEF476E7044AB /* CommunityPostCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE82912F2F111593BE42E33C /* CommunityPostCard.swift */; }; C3CFF5936F1BAC5BC12E9DDB /* UpdateContentPreferenceRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDFAA64C2CA4FCBBF00A024 /* UpdateContentPreferenceRequest.swift */; }; C45F071870A4DB48ABAE8BC5 /* YandexAdSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F064EC9D5711B91979F45C05 /* YandexAdSupport.swift */; }; @@ -2141,36 +2156,6 @@ 401A3FB52FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRecommendationView.swift; sourceTree = ""; }; 401A3FB62FEEAFA000A5BBA3 /* MainHomeRankingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingView.swift; sourceTree = ""; }; 401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingView.swift; sourceTree = ""; }; - C0A710000000000000000001 /* MainContentAllTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllTabResponse.swift; sourceTree = ""; }; - C0A710000000000000000002 /* MainContentAllApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllApi.swift; sourceTree = ""; }; - C0A710000000000000000003 /* MainContentAllRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllRepository.swift; sourceTree = ""; }; - C0A710000000000000000004 /* MainContentAllViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllViewModel.swift; sourceTree = ""; }; - C0A710000000000000000005 /* MainContentAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllView.swift; sourceTree = ""; }; - C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentSeriesThumbnailCard.swift; sourceTree = ""; }; - C0A700000000000000000001 /* MainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentView.swift; sourceTree = ""; }; - C0A700000000000000000002 /* MainContentTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentTab.swift; sourceTree = ""; }; - C0A700000000000000000003 /* MainContentRecommendationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendationView.swift; sourceTree = ""; }; - C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendationViewModel.swift; sourceTree = ""; }; - C0A700000000000000000005 /* AudioRecommendationsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecommendationsResponse.swift; sourceTree = ""; }; - C0A700000000000000000006 /* MainContentRecommendationApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendationApi.swift; sourceTree = ""; }; - C0A700000000000000000007 /* MainContentRecommendationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendationRepository.swift; sourceTree = ""; }; - C0A700000000000000000008 /* MainContentAudioBannerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioBannerSection.swift; sourceTree = ""; }; - C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioHorizontalCardSection.swift; sourceTree = ""; }; - C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioListCarouselSection.swift; sourceTree = ""; }; - C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentVoiceOnOnlySection.swift; sourceTree = ""; }; - C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentMostCommentedAudioSection.swift; sourceTree = ""; }; - C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendedAudioGridSection.swift; sourceTree = ""; }; - C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioEmptyStateView.swift; sourceTree = ""; }; - C0A720000000000000000001 /* AudioRankingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRankingResponse.swift; sourceTree = ""; }; - C0A720000000000000000002 /* MainContentRankingApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankingApi.swift; sourceTree = ""; }; - C0A720000000000000000003 /* MainContentRankingRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankingRepository.swift; sourceTree = ""; }; - C0A720000000000000000004 /* MainContentRankingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankingViewModel.swift; sourceTree = ""; }; - C0A720000000000000000005 /* MainContentRankingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankingView.swift; sourceTree = ""; }; - C0A720000000000000000006 /* MainContentRankChangeBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankChangeBadge.swift; sourceTree = ""; }; - C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioRankingTopCard.swift; sourceTree = ""; }; - C0A720000000000000000008 /* MainContentAudioRankingRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioRankingRow.swift; sourceTree = ""; }; - C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioRankingEmptyStateView.swift; sourceTree = ""; }; - C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioRankingThumbnailCard.swift; sourceTree = ""; }; 401A3FC12FEEB00100A5BBA3 /* MainHomeLiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeLiveSection.swift; sourceTree = ""; }; 401A3FC52FEEB00200A5BBA3 /* MainHomeBannerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeBannerSection.swift; sourceTree = ""; }; 401A3FCC2FEEB00300A5BBA3 /* MainHomeActiveCreatorSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeActiveCreatorSection.swift; sourceTree = ""; }; @@ -2880,6 +2865,43 @@ B7C4F8D708958D3E58AB9A77 /* LanguageOption.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageOption.swift; sourceTree = ""; }; B8F49E8BB9BACFCA8878B70D /* MainTabBarButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarButton.swift; sourceTree = ""; }; C07CBA176DB8BFF3F8D6BCEE /* TitleBar.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TitleBar.swift; sourceTree = ""; }; + C0A700000000000000000001 /* MainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentView.swift; sourceTree = ""; }; + C0A700000000000000000002 /* MainContentTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentTab.swift; sourceTree = ""; }; + C0A700000000000000000003 /* MainContentRecommendationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendationView.swift; sourceTree = ""; }; + C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendationViewModel.swift; sourceTree = ""; }; + C0A700000000000000000005 /* AudioRecommendationsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecommendationsResponse.swift; sourceTree = ""; }; + C0A700000000000000000006 /* MainContentRecommendationApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendationApi.swift; sourceTree = ""; }; + C0A700000000000000000007 /* MainContentRecommendationRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendationRepository.swift; sourceTree = ""; }; + C0A700000000000000000008 /* MainContentAudioBannerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioBannerSection.swift; sourceTree = ""; }; + C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioHorizontalCardSection.swift; sourceTree = ""; }; + C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioListCarouselSection.swift; sourceTree = ""; }; + C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentVoiceOnOnlySection.swift; sourceTree = ""; }; + C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentMostCommentedAudioSection.swift; sourceTree = ""; }; + C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRecommendedAudioGridSection.swift; sourceTree = ""; }; + C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioEmptyStateView.swift; sourceTree = ""; }; + C0A702000000000000000000 /* AudioContentListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContentListRow.swift; sourceTree = ""; }; + C0A706000000000000000000 /* AudioContentThumbnailCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContentThumbnailCard.swift; sourceTree = ""; }; + C0A710000000000000000001 /* MainContentAllTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllTabResponse.swift; sourceTree = ""; }; + C0A710000000000000000002 /* MainContentAllApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllApi.swift; sourceTree = ""; }; + C0A710000000000000000003 /* MainContentAllRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllRepository.swift; sourceTree = ""; }; + C0A710000000000000000004 /* MainContentAllViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllViewModel.swift; sourceTree = ""; }; + C0A710000000000000000005 /* MainContentAllView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAllView.swift; sourceTree = ""; }; + C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentSeriesThumbnailCard.swift; sourceTree = ""; }; + C0A730000000000000000001 /* ContentOverviewPageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOverviewPageResponse.swift; sourceTree = ""; }; + C0A730000000000000000002 /* ContentOverviewApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOverviewApi.swift; sourceTree = ""; }; + C0A730000000000000000003 /* ContentOverviewRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOverviewRepository.swift; sourceTree = ""; }; + C0A730000000000000000004 /* ContentOverviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOverviewViewModel.swift; sourceTree = ""; }; + C0A730000000000000000005 /* ContentOverviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOverviewView.swift; sourceTree = ""; }; + C0A720000000000000000001 /* AudioRankingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRankingResponse.swift; sourceTree = ""; }; + C0A720000000000000000002 /* MainContentRankingApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankingApi.swift; sourceTree = ""; }; + C0A720000000000000000003 /* MainContentRankingRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankingRepository.swift; sourceTree = ""; }; + C0A720000000000000000004 /* MainContentRankingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankingViewModel.swift; sourceTree = ""; }; + C0A720000000000000000005 /* MainContentRankingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankingView.swift; sourceTree = ""; }; + C0A720000000000000000006 /* MainContentRankChangeBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentRankChangeBadge.swift; sourceTree = ""; }; + C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioRankingTopCard.swift; sourceTree = ""; }; + C0A720000000000000000008 /* MainContentAudioRankingRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioRankingRow.swift; sourceTree = ""; }; + C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioRankingEmptyStateView.swift; sourceTree = ""; }; + C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentAudioRankingThumbnailCard.swift; sourceTree = ""; }; C1165E3F4CA1AB019FC85067 /* LanguageRepository.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LanguageRepository.swift; sourceTree = ""; }; C40163E3E9DB325EA984D3C4 /* CreatorChannelFloatingIconButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFloatingIconButton.swift; sourceTree = ""; }; C6B18C00DE256DCC53030630 /* Spacing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Spacing.swift; sourceTree = ""; }; @@ -2916,8 +2938,6 @@ CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveApi.swift; sourceTree = ""; }; CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveRepository.swift; sourceTree = ""; }; CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveViewModel.swift; sourceTree = ""; }; - C0A702000000000000000000 /* AudioContentListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContentListRow.swift; sourceTree = ""; }; - C0A706000000000000000000 /* AudioContentThumbnailCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioContentThumbnailCard.swift; sourceTree = ""; }; CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveStartButton.swift; sourceTree = ""; }; CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabView.swift; sourceTree = ""; }; CC0300012FF0300000A5BBA3 /* CreatorChannelAudioTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioTabResponse.swift; sourceTree = ""; }; @@ -3099,6 +3119,23 @@ path = Home; sourceTree = ""; }; + 25C5D0724F54C0ED7B7BD462 /* Write */ = { + isa = PBXGroup; + children = ( + 25C5D06F4F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteViewModel.swift */, + 25C5D0704F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteView.swift */, + ); + path = Write; + sourceTree = ""; + }; + 25C5D0734F54C0ED7B7BD462 /* Modal */ = { + isa = PBXGroup; + children = ( + 25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.swift */, + ); + path = Modal; + sourceTree = ""; + }; 2D9C7222B946512FD4C4B499 /* Banner */ = { isa = PBXGroup; children = ( @@ -3234,139 +3271,6 @@ path = Like; sourceTree = ""; }; - C0A710000000000000000100 /* All */ = { - isa = PBXGroup; - children = ( - C0A710000000000000000103 /* Components */, - C0A710000000000000000101 /* Models */, - C0A710000000000000000102 /* Repository */, - C0A710000000000000000004 /* MainContentAllViewModel.swift */, - C0A710000000000000000005 /* MainContentAllView.swift */, - ); - path = All; - sourceTree = ""; - }; - C0A710000000000000000101 /* Models */ = { - isa = PBXGroup; - children = ( - C0A710000000000000000001 /* MainContentAllTabResponse.swift */, - ); - path = Models; - sourceTree = ""; - }; - C0A710000000000000000102 /* Repository */ = { - isa = PBXGroup; - children = ( - C0A710000000000000000002 /* MainContentAllApi.swift */, - C0A710000000000000000003 /* MainContentAllRepository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - C0A710000000000000000103 /* Components */ = { - isa = PBXGroup; - children = ( - C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */, - ); - path = Components; - sourceTree = ""; - }; - C0A700000000000000000101 /* Content */ = { - isa = PBXGroup; - children = ( - C0A700000000000000000102 /* Recommendation */, - C0A720000000000000000100 /* Ranking */, - C0A710000000000000000100 /* All */, - C0A700000000000000000001 /* MainContentView.swift */, - C0A700000000000000000002 /* MainContentTab.swift */, - ); - path = Content; - sourceTree = ""; - }; - C0A700000000000000000102 /* Recommendation */ = { - isa = PBXGroup; - children = ( - C0A700000000000000000105 /* Components */, - C0A700000000000000000103 /* Models */, - C0A700000000000000000104 /* Repository */, - C0A700000000000000000003 /* MainContentRecommendationView.swift */, - C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */, - ); - path = Recommendation; - sourceTree = ""; - }; - C0A700000000000000000103 /* Models */ = { - isa = PBXGroup; - children = ( - C0A700000000000000000005 /* AudioRecommendationsResponse.swift */, - ); - path = Models; - sourceTree = ""; - }; - C0A700000000000000000104 /* Repository */ = { - isa = PBXGroup; - children = ( - C0A700000000000000000006 /* MainContentRecommendationApi.swift */, - C0A700000000000000000007 /* MainContentRecommendationRepository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - C0A700000000000000000105 /* Components */ = { - isa = PBXGroup; - children = ( - C0A700000000000000000008 /* MainContentAudioBannerSection.swift */, - C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */, - C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */, - C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */, - C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */, - C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */, - C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */, - ); - path = Components; - sourceTree = ""; - }; - C0A720000000000000000100 /* Ranking */ = { - isa = PBXGroup; - children = ( - C0A720000000000000000103 /* Components */, - C0A720000000000000000101 /* Models */, - C0A720000000000000000102 /* Repository */, - C0A720000000000000000004 /* MainContentRankingViewModel.swift */, - C0A720000000000000000005 /* MainContentRankingView.swift */, - ); - path = Ranking; - sourceTree = ""; - }; - C0A720000000000000000101 /* Models */ = { - isa = PBXGroup; - children = ( - C0A720000000000000000001 /* AudioRankingResponse.swift */, - ); - path = Models; - sourceTree = ""; - }; - C0A720000000000000000102 /* Repository */ = { - isa = PBXGroup; - children = ( - C0A720000000000000000002 /* MainContentRankingApi.swift */, - C0A720000000000000000003 /* MainContentRankingRepository.swift */, - ); - path = Repository; - sourceTree = ""; - }; - C0A720000000000000000103 /* Components */ = { - isa = PBXGroup; - children = ( - C0A720000000000000000006 /* MainContentRankChangeBadge.swift */, - C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */, - C0A720000000000000000008 /* MainContentAudioRankingRow.swift */, - C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */, - C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */, - ); - path = Components; - sourceTree = ""; - }; 401A3F992FEEAFA000A5BBA3 /* Models */ = { isa = PBXGroup; children = ( @@ -5633,23 +5537,6 @@ path = Component; sourceTree = ""; }; - 25C5D0734F54C0ED7B7BD462 /* Modal */ = { - isa = PBXGroup; - children = ( - 25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.swift */, - ); - path = Modal; - sourceTree = ""; - }; - C0A702100000000000000000 /* AudioContent */ = { - isa = PBXGroup; - children = ( - C0A702000000000000000000 /* AudioContentListRow.swift */, - C0A706000000000000000000 /* AudioContentThumbnailCard.swift */, - ); - path = AudioContent; - sourceTree = ""; - }; AA4588B91C0F73344C3DC4A4 /* FanTalk */ = { isa = PBXGroup; children = ( @@ -5663,15 +5550,6 @@ path = FanTalk; sourceTree = ""; }; - 25C5D0724F54C0ED7B7BD462 /* Write */ = { - isa = PBXGroup; - children = ( - 25C5D06F4F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteViewModel.swift */, - 25C5D0704F54C0ED7B7BD462 /* CreatorChannelFanTalkWriteView.swift */, - ); - path = Write; - sourceTree = ""; - }; B068C16B9D7F3F80901622E8 /* Button */ = { isa = PBXGroup; children = ( @@ -5699,6 +5577,177 @@ path = Services; sourceTree = ""; }; + C0A700000000000000000101 /* Content */ = { + isa = PBXGroup; + children = ( + C0A700000000000000000102 /* Recommendation */, + C0A720000000000000000100 /* Ranking */, + C0A710000000000000000100 /* All */, + C0A730000000000000000100 /* Overview */, + C0A700000000000000000001 /* MainContentView.swift */, + C0A700000000000000000002 /* MainContentTab.swift */, + ); + path = Content; + sourceTree = ""; + }; + C0A700000000000000000102 /* Recommendation */ = { + isa = PBXGroup; + children = ( + C0A700000000000000000105 /* Components */, + C0A700000000000000000103 /* Models */, + C0A700000000000000000104 /* Repository */, + C0A700000000000000000003 /* MainContentRecommendationView.swift */, + C0A700000000000000000004 /* MainContentRecommendationViewModel.swift */, + ); + path = Recommendation; + sourceTree = ""; + }; + C0A700000000000000000103 /* Models */ = { + isa = PBXGroup; + children = ( + C0A700000000000000000005 /* AudioRecommendationsResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + C0A700000000000000000104 /* Repository */ = { + isa = PBXGroup; + children = ( + C0A700000000000000000006 /* MainContentRecommendationApi.swift */, + C0A700000000000000000007 /* MainContentRecommendationRepository.swift */, + ); + path = Repository; + sourceTree = ""; + }; + C0A700000000000000000105 /* Components */ = { + isa = PBXGroup; + children = ( + C0A700000000000000000008 /* MainContentAudioBannerSection.swift */, + C0A700000000000000000009 /* MainContentAudioHorizontalCardSection.swift */, + C0A70000000000000000000A /* MainContentAudioListCarouselSection.swift */, + C0A70000000000000000000B /* MainContentVoiceOnOnlySection.swift */, + C0A70000000000000000000C /* MainContentMostCommentedAudioSection.swift */, + C0A70000000000000000000D /* MainContentRecommendedAudioGridSection.swift */, + C0A70000000000000000000F /* MainContentAudioEmptyStateView.swift */, + ); + path = Components; + sourceTree = ""; + }; + C0A702100000000000000000 /* AudioContent */ = { + isa = PBXGroup; + children = ( + C0A702000000000000000000 /* AudioContentListRow.swift */, + C0A706000000000000000000 /* AudioContentThumbnailCard.swift */, + ); + path = AudioContent; + sourceTree = ""; + }; + C0A730000000000000000100 /* Overview */ = { + isa = PBXGroup; + children = ( + C0A730000000000000000101 /* Models */, + C0A730000000000000000102 /* Repository */, + C0A730000000000000000004 /* ContentOverviewViewModel.swift */, + C0A730000000000000000005 /* ContentOverviewView.swift */, + ); + path = Overview; + sourceTree = ""; + }; + C0A730000000000000000101 /* Models */ = { + isa = PBXGroup; + children = ( + C0A730000000000000000001 /* ContentOverviewPageResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + C0A730000000000000000102 /* Repository */ = { + isa = PBXGroup; + children = ( + C0A730000000000000000002 /* ContentOverviewApi.swift */, + C0A730000000000000000003 /* ContentOverviewRepository.swift */, + ); + path = Repository; + sourceTree = ""; + }; + C0A710000000000000000100 /* All */ = { + isa = PBXGroup; + children = ( + C0A710000000000000000103 /* Components */, + C0A710000000000000000101 /* Models */, + C0A710000000000000000102 /* Repository */, + C0A710000000000000000004 /* MainContentAllViewModel.swift */, + C0A710000000000000000005 /* MainContentAllView.swift */, + ); + path = All; + sourceTree = ""; + }; + C0A710000000000000000101 /* Models */ = { + isa = PBXGroup; + children = ( + C0A710000000000000000001 /* MainContentAllTabResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + C0A710000000000000000102 /* Repository */ = { + isa = PBXGroup; + children = ( + C0A710000000000000000002 /* MainContentAllApi.swift */, + C0A710000000000000000003 /* MainContentAllRepository.swift */, + ); + path = Repository; + sourceTree = ""; + }; + C0A710000000000000000103 /* Components */ = { + isa = PBXGroup; + children = ( + C0A710000000000000000006 /* MainContentSeriesThumbnailCard.swift */, + ); + path = Components; + sourceTree = ""; + }; + C0A720000000000000000100 /* Ranking */ = { + isa = PBXGroup; + children = ( + C0A720000000000000000103 /* Components */, + C0A720000000000000000101 /* Models */, + C0A720000000000000000102 /* Repository */, + C0A720000000000000000004 /* MainContentRankingViewModel.swift */, + C0A720000000000000000005 /* MainContentRankingView.swift */, + ); + path = Ranking; + sourceTree = ""; + }; + C0A720000000000000000101 /* Models */ = { + isa = PBXGroup; + children = ( + C0A720000000000000000001 /* AudioRankingResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + C0A720000000000000000102 /* Repository */ = { + isa = PBXGroup; + children = ( + C0A720000000000000000002 /* MainContentRankingApi.swift */, + C0A720000000000000000003 /* MainContentRankingRepository.swift */, + ); + path = Repository; + sourceTree = ""; + }; + C0A720000000000000000103 /* Components */ = { + isa = PBXGroup; + children = ( + C0A720000000000000000006 /* MainContentRankChangeBadge.swift */, + C0A720000000000000000007 /* MainContentAudioRankingTopCard.swift */, + C0A720000000000000000008 /* MainContentAudioRankingRow.swift */, + C0A72000000000000000000A /* MainContentAudioRankingThumbnailCard.swift */, + C0A720000000000000000009 /* MainContentAudioRankingEmptyStateView.swift */, + ); + path = Components; + sourceTree = ""; + }; C46DB3E329B349CBB3F472DF /* Views */ = { isa = PBXGroup; children = ( @@ -6535,6 +6584,11 @@ C0A70100000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */, C0A70100000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */, C0A70100000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */, + C0A731000000000000000001 /* ContentOverviewPageResponse.swift in Sources */, + C0A731000000000000000002 /* ContentOverviewApi.swift in Sources */, + C0A731000000000000000003 /* ContentOverviewRepository.swift in Sources */, + C0A731000000000000000004 /* ContentOverviewViewModel.swift in Sources */, + C0A731000000000000000005 /* ContentOverviewView.swift in Sources */, C0A721000000000000000001 /* AudioRankingResponse.swift in Sources */, C0A721000000000000000002 /* MainContentRankingApi.swift in Sources */, C0A721000000000000000003 /* MainContentRankingRepository.swift in Sources */, @@ -7410,6 +7464,11 @@ C0A70200000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */, C0A70200000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */, C0A70200000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */, + C0A732000000000000000001 /* ContentOverviewPageResponse.swift in Sources */, + C0A732000000000000000002 /* ContentOverviewApi.swift in Sources */, + C0A732000000000000000003 /* ContentOverviewRepository.swift in Sources */, + C0A732000000000000000004 /* ContentOverviewViewModel.swift in Sources */, + C0A732000000000000000005 /* ContentOverviewView.swift in Sources */, C0A722000000000000000001 /* AudioRankingResponse.swift in Sources */, C0A722000000000000000002 /* MainContentRankingApi.swift in Sources */, C0A722000000000000000003 /* MainContentRankingRepository.swift in Sources */, @@ -8325,6 +8384,11 @@ C0A70300000000000000000C /* MainContentMostCommentedAudioSection.swift in Sources */, C0A70300000000000000000D /* MainContentRecommendedAudioGridSection.swift in Sources */, C0A70300000000000000000F /* MainContentAudioEmptyStateView.swift in Sources */, + C0A733000000000000000001 /* ContentOverviewPageResponse.swift in Sources */, + C0A733000000000000000002 /* ContentOverviewApi.swift in Sources */, + C0A733000000000000000003 /* ContentOverviewRepository.swift in Sources */, + C0A733000000000000000004 /* ContentOverviewViewModel.swift in Sources */, + C0A733000000000000000005 /* ContentOverviewView.swift in Sources */, C0A723000000000000000001 /* AudioRankingResponse.swift in Sources */, C0A723000000000000000002 /* MainContentRankingApi.swift in Sources */, C0A723000000000000000003 /* MainContentRankingRepository.swift in Sources */, diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 886499d0..735b7c38 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -175,6 +175,8 @@ enum AppStep { case originalWorkDetail(originalId: Int) case contentAll(isFree: Bool = false, isPointOnly: Bool = false) + + case contentOverview(type: ContentOverviewType = .newAndHotAudio) case seriesMain } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 63b2fae3..4e6f45d7 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -320,6 +320,11 @@ struct AppStepLayerView: View { case .contentAll(let isFree, let isPointOnly): ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly) + case .contentOverview(let type): + ContentOverviewView(type: type) { contentId in + AppState.shared.setAppStep(step: .contentDetail(contentId: contentId)) + } + case .seriesMain: SeriesMainView() diff --git a/SodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swift b/SodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swift index e3cc7040..14dfcfe6 100644 --- a/SodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swift +++ b/SodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swift @@ -190,6 +190,20 @@ extension AudioContentThumbnailCardItem { ) } + init(contentOverviewItem: ContentOverviewItemResponse) { + self.init( + id: contentOverviewItem.contentId, + title: contentOverviewItem.title, + subtitle: contentOverviewItem.creatorNickname, + imageUrl: contentOverviewItem.coverImage, + price: contentOverviewItem.price, + isAdult: contentOverviewItem.isAdult, + isPointAvailable: contentOverviewItem.isPointAvailable, + isFirstContent: contentOverviewItem.isFirstContent, + isOriginalSeries: contentOverviewItem.isOriginalSeries + ) + } + init(mainContentAudio: MainContentAudioResponse) { self.init( id: mainContentAudio.audioContentId, diff --git a/SodaLive/Sources/V2/Main/Content/All/MainContentAllViewModel.swift b/SodaLive/Sources/V2/Main/Content/All/MainContentAllViewModel.swift index 3b507803..ba8bea6a 100644 --- a/SodaLive/Sources/V2/Main/Content/All/MainContentAllViewModel.swift +++ b/SodaLive/Sources/V2/Main/Content/All/MainContentAllViewModel.swift @@ -23,7 +23,7 @@ final class MainContentAllViewModel: ObservableObject { @Published var isShowPopup = false var allowedSorts: [ContentSort] { - selectedType == .free ? [.latest, .popular] : ContentSort.allCases + allowedSorts(for: selectedType) } var shouldShowEmptyState: Bool { @@ -40,6 +40,19 @@ final class MainContentAllViewModel: ObservableObject { fetchContents(page: 0, isNextPage: false) } + func apply(type: MainContentAllType, sort: ContentSort) { + guard allowedSorts(for: type).contains(sort) else { return } + + if selectedType == type, selectedSort == sort, hasLoaded { + return + } + + selectedType = type + selectedSort = sort + clearItems() + fetchFirstPage() + } + func selectType(_ type: MainContentAllType) { guard selectedType != type else { return } @@ -77,6 +90,10 @@ final class MainContentAllViewModel: ObservableObject { fetchNextPageIfPossible() } + private func allowedSorts(for type: MainContentAllType) -> [ContentSort] { + type == .free ? [.latest, .popular] : ContentSort.allCases + } + private func fetchNextPageIfPossible() { guard hasNext, isLoading == false, isLoadingNextPage == false else { return } fetchContents(page: page + 1, isNextPage: true) diff --git a/SodaLive/Sources/V2/Main/Content/MainContentView.swift b/SodaLive/Sources/V2/Main/Content/MainContentView.swift index bbb10c65..636e76da 100644 --- a/SodaLive/Sources/V2/Main/Content/MainContentView.swift +++ b/SodaLive/Sources/V2/Main/Content/MainContentView.swift @@ -7,6 +7,7 @@ struct MainContentView: View { let onTapContent: (Int) -> Void let onTapBanner: (AudioBannerResponse) -> Void let onTapSeries: (Int) -> Void + let onTapNewAndHotAll: () -> Void @State private var selectedTab: MainContentTab = .recommendation @StateObject private var allViewModel = MainContentAllViewModel() @@ -45,7 +46,12 @@ struct MainContentView: View { MainContentRecommendationView( onTapContent: onTapContent, onTapBanner: onTapBanner, - onTapSeries: onTapSeries + onTapSeries: onTapSeries, + onTapLatestAudioAll: { applyAllTab(type: .audio, sort: .latest) }, + onTapNewAndHotAll: onTapNewAndHotAll, + onTapVoiceOnOnlyAll: { applyAllTab(type: .original, sort: .latest) }, + onTapFreeAudioAll: { applyAllTab(type: .free, sort: .popular) }, + onTapPointAudioAll: { applyAllTab(type: .point, sort: .popular) } ) case .ranking: MainContentRankingView(onTapContent: onTapContent) @@ -57,6 +63,11 @@ struct MainContentView: View { ) } } + + private func applyAllTab(type: MainContentAllType, sort: ContentSort) { + selectedTab = .all + allViewModel.apply(type: type, sort: sort) + } } struct MainContentView_Previews: PreviewProvider { @@ -67,7 +78,8 @@ struct MainContentView_Previews: PreviewProvider { onTapNotificationList: {}, onTapContent: { _ in }, onTapBanner: { _ in }, - onTapSeries: { _ in } + onTapSeries: { _ in }, + onTapNewAndHotAll: {} ) } } diff --git a/SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewView.swift b/SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewView.swift new file mode 100644 index 00000000..42146e1e --- /dev/null +++ b/SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewView.swift @@ -0,0 +1,108 @@ +import SwiftUI + +struct ContentOverviewView: View { + @StateObject private var viewModel: ContentOverviewViewModel + let onTapContent: (Int) -> Void + + private let columns = [ + GridItem(.flexible(), spacing: SodaSpacing.s4), + GridItem(.flexible(), spacing: SodaSpacing.s4) + ] + + init( + type: ContentOverviewType = .newAndHotAudio, + onTapContent: @escaping (Int) -> Void + ) { + self._viewModel = StateObject(wrappedValue: ContentOverviewViewModel(type: type)) + self.onTapContent = onTapContent + } + + var body: some View { + VStack(spacing: 0) { + titleBar + + content + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .background(Color.black.ignoresSafeArea()) + .onAppear { + viewModel.fetchFirstPageIfNeeded() + } + .sodaToast( + isPresented: $viewModel.isShowPopup, + message: viewModel.errorMessage, + autohideIn: 2 + ) + } + + private var titleBar: some View { + TitleBar { + HStack(spacing: SodaSpacing.s14) { + Button { + AppState.shared.back() + } label: { + Image("ic_new_bar_back") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + } + .buttonStyle(.plain) + + Text(I18n.MainContentRecommendation.newAndHotSectionTitle) + .appFont(.heading2) + .foregroundColor(.white) + .lineLimit(1) + } + } trailing: { + EmptyView() + } + } + + @ViewBuilder + private var content: some View { + if viewModel.isLoading && viewModel.hasLoaded == false { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if viewModel.shouldShowEmptyState { + MainContentAudioEmptyStateView(message: I18n.MainContentAll.emptyStateMessage) + } else { + ScrollView(showsIndicators: false) { + LazyVGrid(columns: columns, alignment: .leading, spacing: SodaSpacing.s20) { + ForEach(viewModel.items) { item in + Button { + onTapContent(item.contentId) + } label: { + GeometryReader { proxy in + AudioContentThumbnailCard( + item: AudioContentThumbnailCardItem(contentOverviewItem: item), + width: proxy.size.width + ) + } + .aspectRatio(0.78, contentMode: .fit) + } + .buttonStyle(.plain) + .onAppear { + viewModel.fetchNextPageIfNeeded(currentItem: item) + } + } + } + .padding(.horizontal, SodaSpacing.s14) + .padding(.top, SodaSpacing.s8) + .padding(.bottom, SodaSpacing.s20) + + if viewModel.isLoadingNextPage { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .padding(.vertical, SodaSpacing.s20) + } + } + } + } +} + +struct ContentOverviewView_Previews: PreviewProvider { + static var previews: some View { + ContentOverviewView(onTapContent: { _ in }) + } +} diff --git a/SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewViewModel.swift b/SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewViewModel.swift new file mode 100644 index 00000000..198b423f --- /dev/null +++ b/SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewViewModel.swift @@ -0,0 +1,118 @@ +import Foundation +import Combine + +final class ContentOverviewViewModel: ObservableObject { + private let repository = ContentOverviewRepository() + private var subscription = Set() + private let pageSize = 20 + private var latestRequestId = 0 + + let type: ContentOverviewType + + @Published var items = [ContentOverviewItemResponse]() + @Published var page = 0 + @Published var size = 20 + @Published var hasNext = false + @Published var hasLoaded = false + @Published var isLoading = false + @Published var isLoadingNextPage = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + var shouldShowEmptyState: Bool { + hasLoaded && !isLoading && items.isEmpty + } + + init(type: ContentOverviewType = .newAndHotAudio) { + self.type = type + } + + func fetchFirstPageIfNeeded() { + guard !hasLoaded else { return } + fetchFirstPage() + } + + func fetchFirstPage() { + fetchContents(page: 0, isNextPage: false) + } + + func fetchNextPageIfNeeded(currentItem: ContentOverviewItemResponse) { + guard items.last?.contentId == currentItem.contentId else { return } + fetchNextPageIfPossible() + } + + private func fetchNextPageIfPossible() { + guard hasNext, isLoading == false, isLoadingNextPage == false else { return } + fetchContents(page: page + 1, isNextPage: true) + } + + private func fetchContents(page requestPage: Int, isNextPage: Bool) { + latestRequestId += 1 + let requestId = latestRequestId + + if isNextPage { + isLoadingNextPage = true + } else { + isLoading = true + } + + repository.getContents(type: type, page: requestPage, size: pageSize) + .sink { [weak self] result in + guard let self else { return } + guard requestId == self.latestRequestId else { return } + + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + self.applyFailureState() + } + } receiveValue: { [weak self] response in + guard let self else { return } + guard requestId == self.latestRequestId else { return } + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: response.data) + + if let data = decoded.data, decoded.success { + self.applySuccessState(data, isNextPage: isNextPage) + } else { + self.errorMessage = decoded.message ?? I18n.MainContentAll.loadFailedMessage + self.isShowPopup = true + } + } catch { + ERROR_LOG(error.localizedDescription) + self.errorMessage = I18n.MainContentAll.loadFailedMessage + self.isShowPopup = true + } + + self.hasLoaded = true + self.isLoading = false + self.isLoadingNextPage = false + } + .store(in: &subscription) + } + + private func applySuccessState(_ data: ContentOverviewPageResponse, isNextPage: Bool) { + page = data.page + size = data.size + hasNext = data.hasNext + errorMessage = "" + + if isNextPage { + items.append(contentsOf: data.items) + } else { + items = data.items + } + } + + private func applyFailureState() { + errorMessage = I18n.MainContentAll.loadFailedMessage + isShowPopup = true + hasLoaded = true + isLoading = false + isLoadingNextPage = false + } +} diff --git a/SodaLive/Sources/V2/Main/Content/Overview/Models/ContentOverviewPageResponse.swift b/SodaLive/Sources/V2/Main/Content/Overview/Models/ContentOverviewPageResponse.swift new file mode 100644 index 00000000..27b96a37 --- /dev/null +++ b/SodaLive/Sources/V2/Main/Content/Overview/Models/ContentOverviewPageResponse.swift @@ -0,0 +1,27 @@ +import Foundation + +enum ContentOverviewType: String, Decodable, Encodable, Hashable { + case newAndHotAudio = "NEW_AND_HOT_AUDIO" +} + +struct ContentOverviewPageResponse: Decodable { + let type: ContentOverviewType + let items: [ContentOverviewItemResponse] + let page: Int + let size: Int + let hasNext: Bool +} + +struct ContentOverviewItemResponse: Decodable, Identifiable { + let contentId: Int + let title: String + let coverImage: String? + let price: Int + let isPointAvailable: Bool + let creatorNickname: String + let isAdult: Bool + let isFirstContent: Bool + let isOriginalSeries: Bool + + var id: Int { contentId } +} diff --git a/SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewApi.swift b/SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewApi.swift new file mode 100644 index 00000000..325e1291 --- /dev/null +++ b/SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewApi.swift @@ -0,0 +1,44 @@ +import Foundation +import Moya + +enum ContentOverviewApi { + case getContents(type: ContentOverviewType, page: Int, size: Int) +} + +extension ContentOverviewApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .getContents: + return "/api/v2/contents" + } + } + + var method: Moya.Method { + switch self { + case .getContents: + return .get + } + } + + var task: Moya.Task { + switch self { + case .getContents(let type, let page, let size): + return .requestParameters( + parameters: [ + "page": page, + "size": size, + "type": type.rawValue + ], + encoding: URLEncoding.queryString + ) + } + } + + var headers: [String: String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewRepository.swift b/SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewRepository.swift new file mode 100644 index 00000000..c8beaf42 --- /dev/null +++ b/SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewRepository.swift @@ -0,0 +1,16 @@ +import Foundation +import Combine +import CombineMoya +import Moya + +final class ContentOverviewRepository { + private let api = MoyaProvider() + + func getContents( + type: ContentOverviewType, + page: Int, + size: Int + ) -> AnyPublisher { + return api.requestPublisher(.getContents(type: type, page: page, size: size)) + } +} diff --git a/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift b/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift index ec01c2c9..2e7055b1 100644 --- a/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift +++ b/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift @@ -3,12 +3,13 @@ import SwiftUI struct MainContentAudioHorizontalCardSection: View { let title: String let items: [AudioCardResponse] + let action: (() -> Void)? let onTapContent: (Int) -> Void var body: some View { if !items.isEmpty { VStack(alignment: .leading, spacing: SodaSpacing.s12) { - SectionTitle(title: title) + SectionTitle(title: title, action: action) ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: SodaSpacing.s14) { diff --git a/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioListCarouselSection.swift b/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioListCarouselSection.swift index 468b4f32..5c1d7333 100644 --- a/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioListCarouselSection.swift +++ b/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioListCarouselSection.swift @@ -3,6 +3,7 @@ import SwiftUI struct MainContentAudioListCarouselSection: View { let title: String let items: [AudioCardResponse] + let action: (() -> Void)? let onTapContent: (Int) -> Void private var chunks: [[AudioCardResponse]] { @@ -18,7 +19,7 @@ struct MainContentAudioListCarouselSection: View { var body: some View { if !items.isEmpty { VStack(alignment: .leading, spacing: SodaSpacing.s12) { - SectionTitle(title: title) + SectionTitle(title: title, action: action) ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: SodaSpacing.s14) { diff --git a/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentVoiceOnOnlySection.swift b/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentVoiceOnOnlySection.swift index af20ef60..0e77fe6b 100644 --- a/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentVoiceOnOnlySection.swift +++ b/SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentVoiceOnOnlySection.swift @@ -3,12 +3,13 @@ import SwiftUI struct MainContentVoiceOnOnlySection: View { let title: String let items: [OriginalSeriesResponse] + let action: (() -> Void)? let onTapSeries: (Int) -> Void var body: some View { if !items.isEmpty { VStack(alignment: .leading, spacing: SodaSpacing.s12) { - SectionTitle(title: title) + SectionTitle(title: title, action: action) ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: SodaSpacing.s14) { diff --git a/SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift b/SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift index c01ca995..3a359bf4 100644 --- a/SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift +++ b/SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift @@ -4,6 +4,11 @@ struct MainContentRecommendationView: View { let onTapContent: (Int) -> Void let onTapBanner: (AudioBannerResponse) -> Void let onTapSeries: (Int) -> Void + let onTapLatestAudioAll: () -> Void + let onTapNewAndHotAll: () -> Void + let onTapVoiceOnOnlyAll: () -> Void + let onTapFreeAudioAll: () -> Void + let onTapPointAudioAll: () -> Void @StateObject private var viewModel = MainContentRecommendationViewModel() @@ -29,30 +34,35 @@ struct MainContentRecommendationView: View { MainContentAudioHorizontalCardSection( title: I18n.MainContentRecommendation.latestAudioSectionTitle, items: recommendations.latestAudios, + action: onTapLatestAudioAll, onTapContent: onTapContent ) MainContentAudioListCarouselSection( title: I18n.MainContentRecommendation.newAndHotSectionTitle, items: recommendations.newAndHotAudios, + action: onTapNewAndHotAll, onTapContent: onTapContent ) MainContentVoiceOnOnlySection( title: I18n.MainContentRecommendation.voiceOnOnlySectionTitle, items: recommendations.originalSeries, + action: onTapVoiceOnOnlyAll, onTapSeries: onTapSeries ) MainContentAudioHorizontalCardSection( title: I18n.MainContentRecommendation.freeAudioSectionTitle, items: recommendations.freeAudios, + action: onTapFreeAudioAll, onTapContent: onTapContent ) MainContentAudioHorizontalCardSection( title: I18n.MainContentRecommendation.pointAudioSectionTitle, items: recommendations.pointAudios, + action: onTapPointAudioAll, onTapContent: onTapContent ) diff --git a/SodaLive/Sources/V2/Main/MainView.swift b/SodaLive/Sources/V2/Main/MainView.swift index 2295e82e..95f03433 100644 --- a/SodaLive/Sources/V2/Main/MainView.swift +++ b/SodaLive/Sources/V2/Main/MainView.swift @@ -178,7 +178,8 @@ struct MainView: View { onTapNotificationList: handleHomeNotificationListTap, onTapContent: handleRecommendationContentTap, onTapBanner: handleContentAudioBannerTap, - onTapSeries: handleContentOriginalSeriesTap + onTapSeries: handleContentOriginalSeriesTap, + onTapNewAndHotAll: handleContentNewAndHotAllTap ) case .chat: MainPlaceholderTabView(title: MainTab.chat.title) @@ -576,6 +577,12 @@ struct MainView: View { openRecommendationBannerLink(item.link) } + private func handleContentNewAndHotAllTap() { + performRecommendationDetailAction { + appState.setAppStep(step: .contentOverview(type: .newAndHotAudio)) + } + } + private func handleContentOriginalSeriesTap(seriesId: Int) { guard seriesId > 0 else { return } performRecommendationDetailAction { diff --git a/docs/20260707_메인_콘텐츠_추천_전체보기/plan-task.md b/docs/20260707_메인_콘텐츠_추천_전체보기/plan-task.md new file mode 100644 index 00000000..0b4142f5 --- /dev/null +++ b/docs/20260707_메인_콘텐츠_추천_전체보기/plan-task.md @@ -0,0 +1,217 @@ +# 메인 콘텐츠 추천 전체보기 구현 계획 + +## 기준 문서 + +- PRD: `docs/20260707_메인_콘텐츠_추천_전체보기/prd.md` +- 문서 정책: `docs/agent-guides/documentation-policy.md` +- 코드 스타일: `docs/agent-guides/code-style.md` +- 빌드/검증: `docs/agent-guides/build-test-verification.md` + +## 구현할 UI의 Figma URL + +- New&Hot 전체보기 화면: `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=482-15105&m=dev` + +## 확정 요구사항 요약 + +- `처음부터 함께 성장!` 섹션과 `FIRST_AUDIO_CONTENT` 전체보기 진입은 이번 구현 범위에서 제외한다. +- `New&Hot` 전체보기는 탭 외부 전환이므로 `AppStep` route로 추가한다. +- `New&Hot` 전체보기 API는 `GET /api/v2/contents`를 사용한다. +- `New&Hot` 전체보기 기본 query는 `page=0`, `size=20`, `type=NEW_AND_HOT_AUDIO`이다. +- `New&Hot` 전체보기 UI는 Figma 기준 2열 오디오 카드 grid와 back title bar를 제공한다. +- `새로 올라온 오디오`, `오직 보이스온에서만`, `무료 오디오`, `포인트 오디오` 전체보기는 별도 route 없이 하단 `콘텐츠` 탭 내부 상단 `전체` 탭 전환으로 처리한다. +- 기존 `SectionTitle(title:action:)` chevron 표시 기능과 `AudioContentThumbnailCard`를 우선 재사용한다. + +## TASK 체크리스트 + +### Phase 0: 구현 전 문구 결정 + +- [ ] **Task 0.1: 신규 전체보기 실패/빈 목록 문구 확정** + - 대상 파일: + - 확인: `docs/20260707_메인_콘텐츠_추천_전체보기/prd.md` + - 수정 후보: `SodaLive/Sources/I18n/I18n.swift` + - 작업 내용: + - PRD의 Open Questions에 남은 신규 API 실패 toast 문구와 empty state 문구를 확정한다. + - 기존 문구를 재사용할 경우 어떤 namespace의 문구를 재사용할지 명시한다. + - 신규 문구가 필요하면 `I18n`에 추가할 key와 ko/en/ja 문구를 확정한다. + - 검증 기준: + - PRD의 Open Questions가 해소되거나, 구현 계획에 사용할 문구 source가 명확히 기록된다. + +### Phase 1: New&Hot 전체보기 API 모델과 repository + +- [ ] **Task 1.1: ContentOverview API response 모델 추가** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Overview/Models/ContentOverviewPageResponse.swift` + - 작업 내용: + - `ContentOverviewType` enum을 추가한다. + - 이번 범위에서는 `NEW_AND_HOT_AUDIO`만 case로 둔다. + - `ContentOverviewPageResponse`, `ContentOverviewItemResponse`를 추가한다. + - `ContentOverviewItemResponse`는 `Identifiable`을 채택하고 `contentId`를 `id`로 사용한다. + - 검증 기준: + - PRD의 response field가 Swift 모델에 누락 없이 반영된다. + - `FIRST_AUDIO_CONTENT` case는 추가하지 않는다. + +- [ ] **Task 1.2: ContentOverview API와 repository 추가** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewApi.swift` + - 생성: `SodaLive/Sources/V2/Main/Content/Overview/Repository/ContentOverviewRepository.swift` + - 작업 내용: + - `GET /api/v2/contents` endpoint를 추가한다. + - query parameter `page`, `size`, `type`을 `URLEncoding.queryString`으로 전송한다. + - 기존 V2 API와 동일하게 bearer token header를 사용한다. + - repository는 `MoyaProvider`와 `requestPublisher` 패턴을 따른다. + - 검증 기준: + - 기본 호출에서 `page=0`, `size=20`, `type=NEW_AND_HOT_AUDIO`를 보낼 수 있다. + - 기존 `GET /api/v2/audio/contents` API 파일은 수정하지 않는다. + +### Phase 2: New&Hot 전체보기 화면과 페이징 + +- [ ] **Task 2.1: ContentOverview ViewModel 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewViewModel.swift` + - 작업 내용: + - `ContentOverviewRepository`로 첫 페이지와 다음 페이지를 요청한다. + - 기본값은 `type = .newAndHotAudio`, `page = 0`, `pageSize = 20`으로 둔다. + - `items`, `page`, `size`, `hasNext`, `hasLoaded`, `isLoading`, `isLoadingNextPage`, `errorMessage`, `isShowPopup` 상태를 관리한다. + - 첫 페이지 성공은 목록 교체, 다음 페이지 성공은 append로 처리한다. + - 실패 시 첫 페이지/다음 페이지 로딩 상태를 해제하고 toast 또는 empty state 문구를 표시한다. + - 검증 기준: + - `hasNext == true`이고 마지막 item이 노출될 때 다음 page 요청이 가능하다. + - 중복 next page 요청이 발생하지 않는다. + - `FIRST_AUDIO_CONTENT` type 분기는 없다. + +- [ ] **Task 2.2: AudioContentThumbnailCard adapter 추가** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swift` + - 작업 내용: + - `AudioContentThumbnailCardItem`에 `init(contentOverviewItem:)` adapter를 추가한다. + - `contentId`는 `id`, `coverImage`는 `imageUrl`, `creatorNickname`은 `subtitle`로 매핑한다. + - `price`, `isAdult`, `isPointAvailable`, `isFirstContent`, `isOriginalSeries`는 기존 card tag 규칙에 그대로 연결한다. + - 검증 기준: + - 신규 adapter 외 기존 `audioCard`, `mainContentAudio` adapter 동작은 변경하지 않는다. + - New&Hot 카드에서 original/first/point/free/adult tag 표시 조건을 재사용할 수 있다. + +- [ ] **Task 2.3: ContentOverviewView 구현** + - 대상 파일: + - 생성: `SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewView.swift` + - 작업 내용: + - title은 `New&Hot`으로 표시한다. + - 좌측 back chevron은 `AppState.shared.back()`을 호출한다. + - Figma 기준 2열 `LazyVGrid`로 `AudioContentThumbnailCard`를 표시한다. + - 카드 tap 시 `contentId`를 받아 기존 오디오 상세 이동 callback으로 전달한다. + - 첫 로딩, empty state, pagination loading, toast 상태를 기존 V2 콘텐츠 화면 패턴으로 표시한다. + - 검증 기준: + - 화면 진입 시 첫 페이지 요청이 발생한다. + - back chevron tap 시 이전 route로 돌아간다. + - 카드 tap 시 기존 오디오 상세 route로 이동할 수 있는 callback이 호출된다. + +### Phase 3: AppStep route 연결 + +- [ ] **Task 3.1: New&Hot AppStep route 추가** + - 대상 파일: + - 수정: `SodaLive/Sources/App/AppStep.swift` + - 수정: `SodaLive/Sources/ContentView.swift` + - 작업 내용: + - `AppStep`에 `case contentOverview(type: ContentOverviewType = .newAndHotAudio)` route를 추가한다. + - `ContentView`의 `AppStepLayerView` switch에 `ContentOverviewView` mapping을 추가한다. + - route는 이번 범위에서 `NEW_AND_HOT_AUDIO`만 열 수 있게 구성한다. + - 검증 기준: + - `AppState.shared.setAppStep(step:)`로 New&Hot 전체보기 화면을 push할 수 있다. + - `ContentView` switch 누락으로 인한 compile error가 없다. + +- [ ] **Task 3.2: MainView에서 New&Hot route와 오디오 상세 이동 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/MainView.swift` + - 수정: `SodaLive/Sources/V2/Main/Content/MainContentView.swift` + - 작업 내용: + - `MainContentView`에 `onTapNewAndHotAll` callback을 추가한다. + - `MainView`에서 해당 callback을 받아 `AppState.shared.setAppStep(step:)`로 New&Hot 전체보기 route를 연다. + - `ContentOverviewView`의 card tap은 기존 `handleRecommendationContentTap(contentId:)` 흐름을 재사용한다. + - 검증 기준: + - 추천 탭 `New&Hot` chevron tap 시 하단 탭 내부 전환이 아니라 새 `AppStep` route로 진입한다. + - New&Hot 전체보기 card tap 시 기존 오디오 상세 화면으로 진입한다. + +### Phase 4: 추천 탭 섹션 action과 내부 전체 탭 전환 + +- [ ] **Task 4.1: 추천 섹션 컴포넌트에 action parameter 추가** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift` + - 수정: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioListCarouselSection.swift` + - 수정: `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentVoiceOnOnlySection.swift` + - 작업 내용: + - 각 섹션에 optional `action: (() -> Void)? = nil` parameter를 추가한다. + - `SectionTitle(title:)` 호출을 `SectionTitle(title:action:)`로 변경한다. + - item이 비어 섹션이 숨겨질 때는 chevron도 표시하지 않는다. + - 검증 기준: + - action을 전달한 섹션만 chevron이 표시된다. + - 기존 item tap callback은 변경 없이 유지된다. + +- [ ] **Task 4.2: MainContentAllViewModel 외부 적용 API 추가** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Content/All/MainContentAllViewModel.swift` + - 작업 내용: + - 추천 섹션 전체보기 진입을 위한 `apply(type:sort:)` method를 추가한다. + - 목표 타입/정렬이 현재 상태와 다르면 items를 비우고 첫 페이지를 다시 요청한다. + - 목표 타입/정렬이 현재 상태와 같고 이미 로드된 상태라면 불필요한 중복 요청을 피한다. + - `FREE` 타입에 허용되지 않는 정렬을 넣지 않도록 기존 `allowedSorts` 규칙과 충돌하지 않게 한다. + - 검증 기준: + - `.audio + .latest`, `.original + .latest`, `.free + .popular`, `.point + .popular` 적용이 가능하다. + - 기존 타입 chip, sort menu, pagination 동작은 유지된다. + +- [ ] **Task 4.3: MainContentView 내부 전체 탭 전환 연결** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/Main/Content/MainContentView.swift` + - 수정: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift` + - 작업 내용: + - `MainContentRecommendationView`에 섹션별 전체보기 callback을 추가한다. + - `MainContentView`에서 callback을 받아 `selectedTab = .all`로 전환하고 `allViewModel`에 목표 타입/정렬을 적용한다. + - `새로 올라온 오디오`: `.audio`, `.latest` + - `오직 보이스온에서만`: `.original`, `.latest` + - `무료 오디오`: `.free`, `.popular` + - `포인트 오디오`: `.point`, `.popular` + - `New&Hot`: Phase 3의 `onTapNewAndHotAll` callback을 호출한다. + - 검증 기준: + - New&Hot 외 섹션 chevron tap은 `AppStep` route를 추가로 push하지 않고 내부 `전체` 탭으로 전환된다. + - 내부 `전체` 탭 전환 후 타입/정렬이 PRD의 매핑과 일치한다. + +### Phase 5: 문구, 접근성, 회귀 검증 + +- [ ] **Task 5.1: I18n 문구 반영** + - 대상 파일: + - 수정 후보: `SodaLive/Sources/I18n/I18n.swift` + - 작업 내용: + - Task 0.1에서 확정한 New&Hot 전체보기 empty/failure 문구를 반영한다. + - 기존 문구 재사용으로 결정된 경우 불필요한 신규 key를 만들지 않는다. + - 검증 기준: + - 신규 화면에서 표시하는 모든 사용자 노출 문구가 `I18n` 또는 기존 상수 정책과 일치한다. + +- [ ] **Task 5.2: 빌드 검증** + - 대상 파일: + - 확인: `SodaLive.xcworkspace` + - 확인: `SodaLive.xcodeproj` + - 실행 명령: + - `xcodebuild -workspace "SodaLive.xcworkspace" -list` + - `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -sdk iphonesimulator build` + - 검증 기준: + - workspace와 scheme가 확인된다. + - Debug simulator build가 성공한다. + +- [ ] **Task 5.3: 수동 동작 검증** + - 대상 파일: + - 확인: `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift` + - 확인: `SodaLive/Sources/V2/Main/Content/Overview/ContentOverviewView.swift` + - 확인: `SodaLive/Sources/V2/Main/Content/All/MainContentAllView.swift` + - 수동 확인 항목: + - `새로 올라온 오디오` chevron tap 시 `전체 > 오디오 > 최신순` 상태로 전환된다. + - `오직 보이스온에서만` chevron tap 시 `전체 > 오리지널 > 최신순` 상태로 전환된다. + - `무료 오디오` chevron tap 시 `전체 > 무료 > 인기순` 상태로 전환된다. + - `포인트 오디오` chevron tap 시 `전체 > 포인트 > 인기순` 상태로 전환된다. + - `New&Hot` chevron tap 시 신규 `AppStep` route 화면으로 이동한다. + - `New&Hot` 전체보기에서 paging, back, card tap 상세 이동이 동작한다. + - `처음부터 함께 성장!` 섹션과 `FIRST_AUDIO_CONTENT` 진입은 노출되지 않는다. + - 검증 기준: + - PRD의 Success Criteria가 모두 충족된다. + - 기존 추천 탭의 item tap, banner tap, series tap 동작이 회귀하지 않는다. + +## 검증 기록 + +- 2026-07-07: PRD를 기준으로 plan-task 문서를 생성했다. 구현은 아직 수행하지 않았다. diff --git a/docs/20260707_메인_콘텐츠_추천_전체보기/prd.md b/docs/20260707_메인_콘텐츠_추천_전체보기/prd.md new file mode 100644 index 00000000..c6c0057f --- /dev/null +++ b/docs/20260707_메인_콘텐츠_추천_전체보기/prd.md @@ -0,0 +1,211 @@ +# PRD: 메인 콘텐츠 추천 전체보기 + +## 1. Overview +메인 하단 `콘텐츠` 탭의 내부 `추천` 탭에서 일부 콘텐츠 섹션에 전체보기 action을 추가한다. `새로 올라온 오디오`, `오직 보이스온에서만`, `무료 오디오`, `포인트 오디오`는 하단 `콘텐츠` 탭 내부의 상단 `전체` 탭으로 전환해 해당 타입과 정렬 상태를 보여준다. + +`New&Hot`은 기존 전체 탭이 아니라 신규 콘텐츠 전체보기 화면을 만들어 `GET /api/v2/contents` API를 적용한다. `New&Hot`은 탭 외부 화면 전환이므로 `AppStep` route로 추가한다. 신규 화면은 Figma `482:15105` 기준으로 상단 title bar와 2열 오디오 카드 grid를 제공하고, 목록에는 스크롤 페이징을 적용한다. + +Figma 참조: +- 신규 콘텐츠 전체보기 화면: `482:15105`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=482-15105&m=dev` + +## 2. Problem +- 현재 V2 `MainContentRecommendationView`의 추천 섹션 title은 전체보기 action을 받지 않아 chevron이 표시되지 않는다. +- `SectionTitle`은 이미 action을 제공하면 chevron을 표시할 수 있으므로, 섹션별 전체보기 이동 callback을 연결해야 한다. +- 대부분의 추천 섹션 전체보기는 이미 구현된 하단 `콘텐츠` 탭 내부 `전체` 탭으로 대체할 수 있지만, `New&Hot`은 별도 API와 별도 화면이 필요하다. +- 신규 `GET /api/v2/contents` 응답은 기존 `AudioCardResponse` / `MainContentAudioResponse`와 필드명이 일부 달라 adapter 또는 전용 모델이 필요하다. + +## 3. Goals +- 추천 탭에서 전체보기가 필요한 섹션에 action을 추가해 title 우측 chevron을 표시한다. +- `새로 올라온 오디오` 전체보기는 하단 `콘텐츠` 탭의 상단 `전체` 탭에서 `오디오` 타입, `최신순` 정렬로 전환한다. +- `오직 보이스온에서만` 전체보기는 하단 `콘텐츠` 탭의 상단 `전체` 탭에서 `오리지널` 타입, `최신순` 정렬로 전환한다. +- `무료 오디오` 전체보기는 하단 `콘텐츠` 탭의 상단 `전체` 탭에서 `무료` 타입, `인기순` 정렬로 전환한다. +- `포인트 오디오` 전체보기는 하단 `콘텐츠` 탭의 상단 `전체` 탭에서 `포인트` 타입, `인기순` 정렬로 전환한다. +- `New&Hot` 전체보기는 `AppStep` route 기반 신규 콘텐츠 전체보기 화면으로 이동하고 `type = NEW_AND_HOT_AUDIO`를 요청한다. +- 신규 전체보기 화면은 `GET /api/v2/contents`를 호출하고 기본 query 값은 `page = 0`, `size = 20`, `type = NEW_AND_HOT_AUDIO`로 둔다. +- 신규 전체보기 화면은 `hasNext == true`일 때 다음 page를 append한다. +- 신규 전체보기 화면의 오디오 카드는 V2 공용 `AudioContentThumbnailCard` 재사용을 우선한다. + +## 4. Non-Goals +- 실제 구현과 Xcode 프로젝트 수정은 이번 PRD 작성 범위에 포함하지 않는다. +- 추천 탭의 기존 API `GET /api/v2/audio/recommendations` 응답 구조를 변경하지 않는다. +- `새로 올라온 오디오`, `오직 보이스온에서만`, `무료 오디오`, `포인트 오디오`용 별도 전체보기 화면을 새로 만들지 않는다. +- `처음부터 함께 성장!` 섹션과 `FIRST_AUDIO_CONTENT` 전체보기 진입은 이번 범위에 포함하지 않는다. +- `콘텐츠 탭 - 전체`의 기존 API `GET /api/v2/audio/contents` 명세를 변경하지 않는다. +- API 명세에 없는 신규 `type` 값을 임의로 추가하지 않는다. +- Figma localhost asset URL을 앱 코드에 직접 사용하지 않는다. +- 외부 라이브러리를 추가하지 않는다. +- `Pods/**`, `generated/**`, `build/**`는 수정하지 않는다. + +## 5. Target Users +- 추천 탭에서 관심 섹션의 콘텐츠를 더 많이 탐색하려는 사용자 +- 새롭거나 반응이 좋은 오디오를 연속해서 보고 싶은 사용자 +- 첫 콘텐츠, 무료, 포인트, 오리지널 등 조건별 콘텐츠를 빠르게 훑고 상세로 이동하려는 사용자 + +## 6. User Stories +- 사용자는 추천 탭 섹션 title 우측 chevron을 눌러 해당 섹션의 전체 콘텐츠를 보고 싶다. +- 사용자는 `새로 올라온 오디오` 전체보기에서 최신 오디오 목록을 바로 보고 싶다. +- 사용자는 `무료 오디오`와 `포인트 오디오` 전체보기에서 인기순 목록을 바로 보고 싶다. +- 사용자는 `New&Hot` 전체보기에서 Figma처럼 2열 카드 목록을 스크롤하며 더 많은 콘텐츠를 보고 싶다. +- 사용자는 전체보기 목록의 콘텐츠 카드를 누르면 기존 오디오 상세 화면으로 이동하기를 기대한다. + +## 7. Core Requirements + +### 7.1 추천 탭 섹션 action +- 전체보기 action이 필요한 섹션은 title 우측 chevron을 표시한다. +- `SectionTitle(title:action:)`의 기존 chevron 표시 동작을 우선 재사용한다. +- action이 없는 섹션은 기존처럼 chevron을 표시하지 않는다. +- 기존 추천 섹션의 empty 조건은 유지한다. 섹션 item이 비어 화면에 보이지 않는 경우 chevron도 표시하지 않는다. + +대상 섹션과 이동 규칙: +- `새로 올라온 오디오` + - 이동 대상: 하단 `콘텐츠` 탭 내부 상단 `전체` 탭 + - 초기 타입: `MainContentAllType.audio` + - 초기 정렬: `ContentSort.latest` +- `오직 보이스온에서만` + - 이동 대상: 하단 `콘텐츠` 탭 내부 상단 `전체` 탭 + - 초기 타입: `MainContentAllType.original` + - 초기 정렬: `ContentSort.latest` +- `무료 오디오` + - 이동 대상: 하단 `콘텐츠` 탭 내부 상단 `전체` 탭 + - 초기 타입: `MainContentAllType.free` + - 초기 정렬: `ContentSort.popular` +- `포인트 오디오` + - 이동 대상: 하단 `콘텐츠` 탭 내부 상단 `전체` 탭 + - 초기 타입: `MainContentAllType.point` + - 초기 정렬: `ContentSort.popular` +- `New&Hot` + - 이동 대상: `AppStep` route 기반 신규 콘텐츠 전체보기 화면 + - API type: `ContentOverviewType.newAndHotAudio` + +### 7.2 신규 콘텐츠 전체보기 API +- Method: `GET` +- Path: `/api/v2/contents` +- 인증: 기존 V2 API 인증 헤더 패턴을 따른다. +- 응답 래퍼: 기존 V2 관례대로 `ApiResponse` 디코딩을 우선한다. + +Query parameter: +- `page: Int` +- `size: Int` +- `type: ContentOverviewType` + +기본값: +- `page = 0` +- `size = 20` +- `type = NEW_AND_HOT_AUDIO` + +Swift enum: +```swift +enum ContentOverviewType: String, Decodable, Encodable, Hashable { + case newAndHotAudio = "NEW_AND_HOT_AUDIO" +} +``` + +Swift response model: +```swift +struct ContentOverviewPageResponse: Decodable { + let type: ContentOverviewType + let items: [ContentOverviewItemResponse] + let page: Int + let size: Int + let hasNext: Bool +} + +struct ContentOverviewItemResponse: Decodable, Identifiable { + let contentId: Int + let title: String + let coverImage: String? + let price: Int + let isPointAvailable: Bool + let creatorNickname: String + let isAdult: Bool + let isFirstContent: Bool + let isOriginalSeries: Bool + + var id: Int { contentId } +} +``` + +### 7.3 신규 콘텐츠 전체보기 화면 +- 화면은 `SodaLive/Sources/V2/**` 하위에 신규 `View`, `ViewModel`, `Repository`, `Models`를 작성한다. +- 신규 화면은 `AppStep`에 route를 추가하고 `ContentView`에서 해당 route를 화면에 매핑한다. +- 화면 title은 `New&Hot`으로 표시한다. +- Figma 기준 상단 title bar는 좌측 back chevron과 title을 가진다. +- 콘텐츠 목록은 2열 grid로 표시한다. +- 각 카드는 정사각형 cover image, title, creator nickname, 태그를 표시한다. +- 카드 태그는 응답 필드에 따라 기존 `AudioContentThumbnailCard`의 태그 표시 규칙을 따른다. + - `isOriginalSeries == true`: original tag + - `isFirstContent == true`: first tag + - `isPointAvailable == true`: point tag + - `price == 0`: free tag + - `isAdult == true`: adult tag +- 카드 tap 시 `contentId`로 기존 오디오 상세 화면에 진입한다. +- 첫 페이지 로딩, 빈 목록, 실패, pagination loading 상태는 기존 V2 콘텐츠 탭 패턴을 따른다. + +### 7.4 Paging +- `page = 0` 응답은 기존 목록을 교체한다. +- `page > 0` 응답은 기존 목록 뒤에 append한다. +- `hasNext == false`이면 추가 요청을 막는다. +- 이미 다음 페이지를 로딩 중이면 중복 요청을 막는다. +- 첫 페이지 실패 시 empty/error 상태를 표시한다. +- 다음 페이지 실패 시 기존 목록은 유지하고 toast 또는 error message를 표시한다. + +### 7.5 하단 콘텐츠 탭 내부 전체 탭 전환 +- `New&Hot`을 제외한 추천 섹션 전체보기는 별도 route를 추가하지 않고 하단 `콘텐츠` 탭 내부에서 처리한다. +- 추천 탭에서 하단 `콘텐츠` 탭 내부 상단 `전체` 탭으로 이동할 때 `MainContentView`의 내부 탭을 `.all`로 전환한다. +- 전환 직후 `MainContentAllViewModel`에 목표 타입과 정렬을 적용하고 첫 페이지를 다시 요청한다. +- 같은 타입/정렬 상태로 다시 진입하는 경우 불필요한 중복 요청은 피한다. +- 기존 `MainContentAllViewModel` 상태 유지 구조는 보존하되, 추천 섹션 전체보기 진입 시 명시적으로 선택 상태를 갱신할 수 있어야 한다. + +## 8. Reusable V2 Candidates +- `SodaLive/Sources/V2/Component/SectionTitle.swift` + - `action`이 있으면 `ic_chevron_right`를 표시하므로 추천 섹션 title chevron에 재사용 가능하다. +- `SodaLive/Sources/V2/Component/AudioContent/AudioContentThumbnailCard.swift` + - Figma 신규 전체보기의 2열 오디오 카드와 태그 표시 구조가 일치한다. + - `ContentOverviewItemResponse`를 `AudioContentThumbnailCardItem`으로 바꾸는 initializer 추가만으로 재사용 가능하다. +- `SodaLive/Sources/V2/Main/Content/All/MainContentAllView.swift` + - 하단 `콘텐츠` 탭 내부 상단 `전체` 탭 전환 대상 화면으로 재사용한다. + - 외부에서 초기 타입/정렬을 주입하거나 갱신하는 API가 필요하다. +- `SodaLive/Sources/V2/Main/Content/All/MainContentAllViewModel.swift` + - 타입/정렬 선택, 첫 페이지 reload, append paging 구조가 이미 있다. + - 추천 전체보기 진입용 `apply(type:sort:)` 같은 최소 API 추가 후보이다. +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioHorizontalCardSection.swift` + - `새로 올라온 오디오`, `무료 오디오`, `포인트 오디오` 섹션에 action parameter를 추가하는 후보이다. +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioListCarouselSection.swift` + - `New&Hot` 섹션에 action parameter를 추가하는 후보이다. +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentVoiceOnOnlySection.swift` + - `오직 보이스온에서만` 섹션에 action parameter를 추가하는 후보이다. +- `SodaLive/Sources/V2/Component/DefaultTitleBar.swift` / `SodaLive/Sources/V2/Component/TitleBar.swift` + - 신규 전체보기 화면 title bar 재사용 후보이다. Figma의 좌측 back chevron 구조와 정확히 맞지 않으면 기존 title bar 패턴 안에서 최소 확장한다. +- `SodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationApi.swift` + - V2 Moya `TargetType`, bearer token header 패턴 참고 대상이다. +- `SodaLive/Sources/V2/Main/Content/All/Repository/MainContentAllApi.swift` + - query parameter 기반 V2 API 구현 패턴 참고 대상이다. +- `SodaLive/Sources/V2/Main/Content/Recommendation/Components/MainContentAudioEmptyStateView.swift` + - 신규 전체보기의 빈 목록/실패 상태 재사용 후보이다. + +## 9. Technical Constraints +- 기능 변경은 `SodaLive/Sources/**`, 신규 V2 코드는 `SodaLive/Sources/V2/**` 하위에서 해결한다. +- 공용 컴포넌트 변경은 재사용성이 명확할 때만 `SodaLive/Sources/V2/Component/**`에서 최소 수정한다. +- 특정 화면 내부에서만 쓰는 컴포넌트는 해당 화면 폴더 하위 `Components`에 둔다. +- 기존 navigation은 `AppState.shared.setAppStep(step:)` 패턴을 따른다. +- `New&Hot` 신규 전체보기 화면은 탭 외부 전환이므로 `AppStep`과 `ContentView` mapping을 추가한다. +- `New&Hot`을 제외한 섹션 전체보기는 `AppStep` route를 추가하지 않고 `MainContentView` 내부 탭 상태 변경으로 처리한다. +- 인증 헤더는 기존 V2 API와 동일하게 `Authorization: Bearer ` 패턴을 따른다. +- `Long` id는 기존 V2 모델 관례에 맞춰 Swift `Int`를 우선 사용한다. +- 기존 `AudioContentThumbnailCard`는 `imageUrl` 필드명을 사용하므로 `coverImage` adapter가 필요하다. +- 구현 시 빌드/검증 명령은 `docs/agent-guides/build-test-verification.md`를 따른다. + +## 10. Success Criteria +- 전체보기 대상 추천 섹션 title 우측에 chevron이 표시된다. +- `새로 올라온 오디오` chevron tap 시 하단 `콘텐츠` 탭 내부 상단 `전체 > 오디오 > 최신순` 상태로 전환된다. +- `오직 보이스온에서만` chevron tap 시 하단 `콘텐츠` 탭 내부 상단 `전체 > 오리지널 > 최신순` 상태로 전환된다. +- `무료 오디오` chevron tap 시 하단 `콘텐츠` 탭 내부 상단 `전체 > 무료 > 인기순` 상태로 전환된다. +- `포인트 오디오` chevron tap 시 하단 `콘텐츠` 탭 내부 상단 `전체 > 포인트 > 인기순` 상태로 전환된다. +- `New&Hot` chevron tap 시 `AppStep` route로 신규 전체보기 화면에 진입하고 `GET /api/v2/contents?page=0&size=20&type=NEW_AND_HOT_AUDIO`를 요청한다. +- 신규 전체보기 화면은 Figma 기준 2열 오디오 card grid, title bar, back navigation을 제공한다. +- 신규 전체보기 화면은 `hasNext` 기반 다음 페이지 append를 제공한다. +- 신규 전체보기 화면의 card tap은 기존 오디오 상세 이동과 동일하게 동작한다. +- 기존 추천 탭 섹션의 item tap, banner tap, series tap 동작은 회귀하지 않는다. + +## 11. Open Questions +- 신규 API 실패 시 toast 문구와 empty state 문구의 정확한 I18n 문구는 확인이 필요하다.