feat(content): 랭킹 유형 탭을 추가한다
This commit is contained in:
@@ -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<AudioRankingType> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import Moya
|
||||
final class MainContentRankingRepository {
|
||||
private let api = MoyaProvider<MainContentRankingApi>()
|
||||
|
||||
func getAudioRankings() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getAudioRankings)
|
||||
func getAudioRankings(type: AudioRankingType) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getAudioRankings(type: type))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user