diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 0c1a2d6f..d25269e6 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -3054,6 +3054,30 @@ If you block this user, the following features will be restricted. pick(ko: "랭킹", en: "Ranking", ja: "ランキング") } + static var weeklyPopularTabTitle: String { + pick(ko: "주간 인기", en: "Weekly popular", ja: "週間人気") + } + + static var risingTabTitle: String { + pick(ko: "지금 뜨는 중", en: "Rising now", ja: "今話題") + } + + static var revenueTabTitle: String { + pick(ko: "매출", en: "Revenue", ja: "売上") + } + + static var salesCountTabTitle: String { + pick(ko: "판매량", en: "Sales", ja: "販売数") + } + + static var commentCountTabTitle: String { + pick(ko: "댓글수", en: "Comments", ja: "コメント数") + } + + static var likeCountTabTitle: String { + pick(ko: "좋아요", en: "Likes", ja: "いいね") + } + static var emptyStateMessage: String { pick( ko: "랭킹 콘텐츠가 아직 없어요.", diff --git a/SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift b/SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift index c962e41d..2427ccf7 100644 --- a/SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift +++ b/SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingView.swift @@ -14,13 +14,23 @@ struct MainContentRankingView: View { Color.black .ignoresSafeArea() - if viewModel.isLoading && !viewModel.hasLoaded { - ProgressView() - .progressViewStyle(CircularProgressViewStyle(tint: .white)) - } else if viewModel.shouldShowEmptyState { - MainContentAudioRankingEmptyStateView(message: viewModel.emptyStateMessage) - } else { - rankingContent + VStack(spacing: 0) { + MainContentRankingTypeTabBar( + items: AudioRankingType.allCases, + selectedItem: selectedTypeBinding, + title: { $0.title } + ) + + if viewModel.isLoading && !viewModel.hasLoaded { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if viewModel.shouldShowEmptyState { + MainContentAudioRankingEmptyStateView(message: viewModel.emptyStateMessage) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + rankingContent + } } } .onAppear { @@ -93,6 +103,13 @@ struct MainContentRankingView: View { [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())] } + private var selectedTypeBinding: Binding { + Binding( + get: { viewModel.selectedType }, + set: { viewModel.selectType($0) } + ) + } + private var topItem: AudioRankingItemResponse? { viewModel.items.first { $0.rank == 1 } } @@ -109,3 +126,54 @@ struct MainContentRankingView: View { viewModel.items.filter { 11...20 ~= $0.rank } } } + +private struct MainContentRankingTypeTabBar: View { + let items: [AudioRankingType] + @Binding var selectedItem: AudioRankingType + let title: (AudioRankingType) -> String + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(alignment: .center, spacing: SodaSpacing.s8) { + ForEach(items, id: \.self) { item in + MainContentRankingTypeTabBarItem( + title: title(item), + isSelected: selectedItem == item, + action: { selectedItem = item } + ) + } + } + .padding(.horizontal, SodaSpacing.s14) + } + .frame(maxWidth: .infinity) + .frame(height: 52, alignment: .center) + .background(Color.black) + } +} + +private struct MainContentRankingTypeTabBarItem: View { + let title: String + let isSelected: Bool + let action: () -> Void + + var body: some View { + Button { + action() + } label: { + Text(title) + .appFont(size: 14, weight: .medium) + .foregroundColor(isSelected ? Color.black : Color.white) + .lineLimit(1) + .padding(.horizontal, SodaSpacing.s12) + .padding(.vertical, SodaSpacing.s8) + .frame(height: 34) + .background(isSelected ? Color.white : Color.black) + .clipShape(Capsule()) + .overlay( + Capsule() + .stroke(isSelected ? Color.white : Color.gray700, lineWidth: 1) + ) + } + .buttonStyle(.plain) + } +} diff --git a/SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingViewModel.swift b/SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingViewModel.swift index 27988421..265c6f29 100644 --- a/SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingViewModel.swift +++ b/SodaLive/Sources/V2/Main/Content/Ranking/MainContentRankingViewModel.swift @@ -11,6 +11,7 @@ final class MainContentRankingViewModel: ObservableObject { @Published var items = [AudioRankingItemResponse]() @Published var showRankChange = false @Published var hasLoaded = false + @Published var selectedType: AudioRankingType = .weeklyPopular var emptyStateMessage: String { if !errorMessage.isEmpty { @@ -29,7 +30,7 @@ final class MainContentRankingViewModel: ObservableObject { isLoading = true - repository.getAudioRankings() + repository.getAudioRankings(type: selectedType) .sink { [weak self] result in guard let self else { return } @@ -77,4 +78,16 @@ final class MainContentRankingViewModel: ObservableObject { } .store(in: &subscription) } + + func selectType(_ type: AudioRankingType) { + guard selectedType != type else { return } + + subscription.forEach { $0.cancel() } + subscription.removeAll() + selectedType = type + items = [] + isLoading = false + hasLoaded = false + fetchRankings() + } } diff --git a/SodaLive/Sources/V2/Main/Content/Ranking/Models/AudioRankingResponse.swift b/SodaLive/Sources/V2/Main/Content/Ranking/Models/AudioRankingResponse.swift index f3832a33..773f00b4 100644 --- a/SodaLive/Sources/V2/Main/Content/Ranking/Models/AudioRankingResponse.swift +++ b/SodaLive/Sources/V2/Main/Content/Ranking/Models/AudioRankingResponse.swift @@ -13,6 +13,23 @@ enum AudioRankingType: String, Decodable, CaseIterable, Hashable { case salesCount = "SALES_COUNT" case commentCount = "COMMENT_COUNT" case likeCount = "LIKE_COUNT" + + var title: String { + switch self { + case .weeklyPopular: + return I18n.MainContentRanking.weeklyPopularTabTitle + case .rising: + return I18n.MainContentRanking.risingTabTitle + case .revenue: + return I18n.MainContentRanking.revenueTabTitle + case .salesCount: + return I18n.MainContentRanking.salesCountTabTitle + case .commentCount: + return I18n.MainContentRanking.commentCountTabTitle + case .likeCount: + return I18n.MainContentRanking.likeCountTabTitle + } + } } struct AudioRankingItemResponse: Decodable, Identifiable { diff --git a/SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingApi.swift b/SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingApi.swift index b1396d4d..95fffbbb 100644 --- a/SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingApi.swift +++ b/SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingApi.swift @@ -3,7 +3,7 @@ import Foundation import Moya enum MainContentRankingApi { - case getAudioRankings + case getAudioRankings(type: AudioRankingType) } extension MainContentRankingApi: TargetType { @@ -27,8 +27,11 @@ extension MainContentRankingApi: TargetType { var task: Moya.Task { switch self { - case .getAudioRankings: - return .requestPlain + case .getAudioRankings(let type): + return .requestParameters( + parameters: ["type": type.rawValue], + encoding: URLEncoding.queryString + ) } } diff --git a/SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingRepository.swift b/SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingRepository.swift index 0ea34d37..580f6bf1 100644 --- a/SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingRepository.swift +++ b/SodaLive/Sources/V2/Main/Content/Ranking/Repository/MainContentRankingRepository.swift @@ -7,7 +7,7 @@ import Moya final class MainContentRankingRepository { private let api = MoyaProvider() - func getAudioRankings() -> AnyPublisher { - return api.requestPublisher(.getAudioRankings) + func getAudioRankings(type: AudioRankingType) -> AnyPublisher { + return api.requestPublisher(.getAudioRankings(type: type)) } }