feat(recommendation): 추천 서브 배너와 연속 순환을 추가한다

This commit is contained in:
Yu Sung
2026-10-30 12:19:00 +09:00
parent 88f923072c
commit 5dcd0f0104
18 changed files with 1045 additions and 2 deletions
@@ -32,6 +32,12 @@ struct MainContentRecommendationView: View {
onTapBanner: onTapBanner
)
if !recommendations.eligibleSubBanners.isEmpty {
SubBannerCarousel(items: recommendations.eligibleSubBanners) {
SubBannerLinkOpener.open($0)
}
}
MainContentVoiceOnOnlySection(
title: I18n.MainContentRecommendation.sodaLiveOnlySectionTitle,
items: recommendations.originalSeries,
@@ -43,6 +43,14 @@ final class MainContentRecommendationViewModel: ObservableObject {
} receiveValue: { [weak self] response in
guard let self else { return }
guard response.statusCode == 200 else {
self.errorMessage = I18n.MainContentRecommendation.loadFailedMessage
self.isShowPopup = true
self.isLoading = false
self.hasLoaded = true
return
}
let responseData = response.data
do {
@@ -2,6 +2,7 @@ import Foundation
struct AudioRecommendationsResponse: Decodable {
let banners: [AudioBannerResponse]
let subBanners: [SubBanner]
let originalSeries: [OriginalSeriesResponse]
let latestAudios: [AudioCardResponse]
let newAndHotAudios: [AudioCardResponse]
@@ -10,8 +11,13 @@ struct AudioRecommendationsResponse: Decodable {
let mostCommentedAudios: [CommentedAudioResponse]
let recommendedAudios: [AudioCardResponse]
var eligibleSubBanners: [SubBanner] {
subBanners.filter { $0.placement == .contentRecommendation && $0.isEligible }
}
var isEmpty: Bool {
banners.isEmpty &&
eligibleSubBanners.isEmpty &&
originalSeries.isEmpty &&
latestAudios.isEmpty &&
newAndHotAudios.isEmpty &&
@@ -32,6 +32,8 @@ extension MainContentRecommendationApi: TargetType {
}
var headers: [String: String]? {
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
let token = UserDefaults.string(forKey: .token)
guard !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil }
return ["Authorization": "Bearer \(token)"]
}
}
@@ -44,6 +44,10 @@ struct MainHomeRecommendationView: View {
onTapCommunityPost: onTapCommunityPost
)
if let items = viewModel.recommendations?.eligibleSubBanners, !items.isEmpty {
SubBannerCarousel(items: items) { SubBannerLinkOpener.open($0) }
}
MainHomeRecentDebutCreatorSection(
items: viewModel.recommendations?.recentDebutCreators ?? [],
onTapCreator: onTapCreator
@@ -33,6 +33,13 @@ final class MainHomeRecommendationViewModel: ObservableObject {
} receiveValue: { [weak self] response in
guard let self else { return }
guard response.statusCode == 200 else {
self.errorMessage = I18n.Common.commonError
self.isShowPopup = true
self.isLoading = false
return
}
let responseData = response.data
do {
@@ -3,12 +3,17 @@ import Foundation
struct HomeRecommendationResponse: Decodable {
let lives: [HomeLiveItem]
let banners: [RecommendationBannerResponse]
let subBanners: [SubBanner]
let recentlyActiveCreators: [HomeActiveCreatorItem]
let recentDebutCreators: [HomeCreatorItem]
let aiCharacters: [HomeAiCharacterItem]
let genreCreators: [HomeGenreCreatorGroupItem]
let cheerCreators: [HomeCreatorItem]
let popularCommunityPosts: [HomePopularCommunityPostItem]
var eligibleSubBanners: [SubBanner] {
subBanners.filter { $0.placement == .homeRecommendation && $0.isEligible }
}
}
struct HomeLiveItem: Decodable, Hashable {
@@ -42,6 +42,11 @@ extension MainHomeRecommendationApi: TargetType {
}
var headers: [String: String]? {
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
let token = UserDefaults.string(forKey: .token)
if case .getRecommendations = self,
token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return nil
}
return ["Authorization": "Bearer \(token)"]
}
}