feat(content): 랭킹 유형 탭을 추가한다
This commit is contained in:
@@ -3054,6 +3054,30 @@ If you block this user, the following features will be restricted.
|
|||||||
pick(ko: "랭킹", en: "Ranking", ja: "ランキング")
|
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 {
|
static var emptyStateMessage: String {
|
||||||
pick(
|
pick(
|
||||||
ko: "랭킹 콘텐츠가 아직 없어요.",
|
ko: "랭킹 콘텐츠가 아직 없어요.",
|
||||||
|
|||||||
@@ -14,13 +14,23 @@ struct MainContentRankingView: View {
|
|||||||
Color.black
|
Color.black
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
if viewModel.isLoading && !viewModel.hasLoaded {
|
VStack(spacing: 0) {
|
||||||
ProgressView()
|
MainContentRankingTypeTabBar(
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
items: AudioRankingType.allCases,
|
||||||
} else if viewModel.shouldShowEmptyState {
|
selectedItem: selectedTypeBinding,
|
||||||
MainContentAudioRankingEmptyStateView(message: viewModel.emptyStateMessage)
|
title: { $0.title }
|
||||||
} else {
|
)
|
||||||
rankingContent
|
|
||||||
|
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 {
|
.onAppear {
|
||||||
@@ -93,6 +103,13 @@ struct MainContentRankingView: View {
|
|||||||
[GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
|
[GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var selectedTypeBinding: Binding<AudioRankingType> {
|
||||||
|
Binding(
|
||||||
|
get: { viewModel.selectedType },
|
||||||
|
set: { viewModel.selectType($0) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private var topItem: AudioRankingItemResponse? {
|
private var topItem: AudioRankingItemResponse? {
|
||||||
viewModel.items.first { $0.rank == 1 }
|
viewModel.items.first { $0.rank == 1 }
|
||||||
}
|
}
|
||||||
@@ -109,3 +126,54 @@ struct MainContentRankingView: View {
|
|||||||
viewModel.items.filter { 11...20 ~= $0.rank }
|
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 items = [AudioRankingItemResponse]()
|
||||||
@Published var showRankChange = false
|
@Published var showRankChange = false
|
||||||
@Published var hasLoaded = false
|
@Published var hasLoaded = false
|
||||||
|
@Published var selectedType: AudioRankingType = .weeklyPopular
|
||||||
|
|
||||||
var emptyStateMessage: String {
|
var emptyStateMessage: String {
|
||||||
if !errorMessage.isEmpty {
|
if !errorMessage.isEmpty {
|
||||||
@@ -29,7 +30,7 @@ final class MainContentRankingViewModel: ObservableObject {
|
|||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
repository.getAudioRankings()
|
repository.getAudioRankings(type: selectedType)
|
||||||
.sink { [weak self] result in
|
.sink { [weak self] result in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
@@ -77,4 +78,16 @@ final class MainContentRankingViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
.store(in: &subscription)
|
.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 salesCount = "SALES_COUNT"
|
||||||
case commentCount = "COMMENT_COUNT"
|
case commentCount = "COMMENT_COUNT"
|
||||||
case likeCount = "LIKE_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 {
|
struct AudioRankingItemResponse: Decodable, Identifiable {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Foundation
|
|||||||
import Moya
|
import Moya
|
||||||
|
|
||||||
enum MainContentRankingApi {
|
enum MainContentRankingApi {
|
||||||
case getAudioRankings
|
case getAudioRankings(type: AudioRankingType)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainContentRankingApi: TargetType {
|
extension MainContentRankingApi: TargetType {
|
||||||
@@ -27,8 +27,11 @@ extension MainContentRankingApi: TargetType {
|
|||||||
|
|
||||||
var task: Moya.Task {
|
var task: Moya.Task {
|
||||||
switch self {
|
switch self {
|
||||||
case .getAudioRankings:
|
case .getAudioRankings(let type):
|
||||||
return .requestPlain
|
return .requestParameters(
|
||||||
|
parameters: ["type": type.rawValue],
|
||||||
|
encoding: URLEncoding.queryString
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Moya
|
|||||||
final class MainContentRankingRepository {
|
final class MainContentRankingRepository {
|
||||||
private let api = MoyaProvider<MainContentRankingApi>()
|
private let api = MoyaProvider<MainContentRankingApi>()
|
||||||
|
|
||||||
func getAudioRankings() -> AnyPublisher<Response, MoyaError> {
|
func getAudioRankings(type: AudioRankingType) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.getAudioRankings)
|
return api.requestPublisher(.getAudioRankings(type: type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user