feat(content): 전체 탭을 추가한다

This commit is contained in:
Yu Sung
2026-07-06 16:28:33 +09:00
parent b42ca61ce4
commit bae92eba4b
14 changed files with 1233 additions and 1 deletions

View File

@@ -0,0 +1,233 @@
import Foundation
import Combine
final class MainContentAllViewModel: ObservableObject {
private let repository = MainContentAllRepository()
private var subscription = Set<AnyCancellable>()
private let pageSize = 20
private var latestRequestId = 0
@Published var selectedType: MainContentAllType = .audio
@Published var selectedSort: ContentSort = .latest
@Published var selectedDayOfWeek: SeriesPublishedDaysOfWeek = .currentDeviceDay
@Published var response: MainContentAllTabResponse?
@Published var audios = [MainContentAudioResponse]()
@Published var series = [MainContentSeriesResponse]()
@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 allowedSorts: [ContentSort] {
selectedType == .free ? [.latest, .popular] : ContentSort.allCases
}
var shouldShowEmptyState: Bool {
guard hasLoaded, isLoading == false else { return false }
return selectedType.usesAudioItems ? audios.isEmpty : series.isEmpty
}
func fetchFirstPageIfNeeded() {
guard hasLoaded == false else { return }
fetchFirstPage()
}
func fetchFirstPage() {
fetchContents(page: 0, isNextPage: false)
}
func selectType(_ type: MainContentAllType) {
guard selectedType != type else { return }
selectedType = type
if allowedSorts.contains(selectedSort) == false {
selectedSort = .latest
}
clearItems()
fetchFirstPage()
}
func selectSort(_ sort: ContentSort) {
guard allowedSorts.contains(sort), selectedSort != sort else { return }
selectedSort = sort
clearItems()
fetchFirstPage()
}
func selectDayOfWeek(_ dayOfWeek: SeriesPublishedDaysOfWeek) {
guard selectedType == .series, selectedDayOfWeek != dayOfWeek else { return }
selectedDayOfWeek = dayOfWeek
clearItems()
fetchFirstPage()
}
func fetchNextPageIfNeeded(currentAudio: MainContentAudioResponse) {
guard audios.last?.audioContentId == currentAudio.audioContentId else { return }
fetchNextPageIfPossible()
}
func fetchNextPageIfNeeded(currentSeries: MainContentSeriesResponse) {
guard series.last?.seriesId == currentSeries.seriesId 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: selectedType,
sort: selectedSort,
page: requestPage,
size: pageSize,
dayOfWeek: selectedDayOfWeek
)
.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<MainContentAllTabResponse>.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: MainContentAllTabResponse, isNextPage: Bool) {
response = data
selectedType = data.type
selectedSort = data.sort
if let dayOfWeek = data.dayOfWeek {
selectedDayOfWeek = dayOfWeek
}
page = data.page
size = data.size
hasNext = data.hasNext
if data.type.usesAudioItems {
if isNextPage {
audios.append(contentsOf: data.audios)
} else {
audios = data.audios
series = []
}
} else {
if isNextPage {
series.append(contentsOf: data.series)
} else {
series = data.series
audios = []
}
}
}
private func applyFailureState() {
errorMessage = I18n.MainContentAll.loadFailedMessage
isShowPopup = true
hasLoaded = true
isLoading = false
isLoadingNextPage = false
}
private func clearItems() {
audios = []
series = []
response = nil
page = 0
hasNext = false
}
}
extension SeriesPublishedDaysOfWeek {
static var currentDeviceDay: SeriesPublishedDaysOfWeek {
switch Calendar.current.component(.weekday, from: Date()) {
case 1:
return .SUN
case 2:
return .MON
case 3:
return .TUE
case 4:
return .WED
case 5:
return .THU
case 6:
return .FRI
case 7:
return .SAT
default:
return .SUN
}
}
static var allFilterCases: [SeriesPublishedDaysOfWeek] {
[.MON, .TUE, .WED, .THU, .FRI, .SAT, .SUN, .RANDOM]
}
var shortTitle: String {
switch self {
case .SUN:
return I18n.MainContentAll.DayOfWeek.sun
case .MON:
return I18n.MainContentAll.DayOfWeek.mon
case .TUE:
return I18n.MainContentAll.DayOfWeek.tue
case .WED:
return I18n.MainContentAll.DayOfWeek.wed
case .THU:
return I18n.MainContentAll.DayOfWeek.thu
case .FRI:
return I18n.MainContentAll.DayOfWeek.fri
case .SAT:
return I18n.MainContentAll.DayOfWeek.sat
case .RANDOM:
return I18n.MainContentAll.DayOfWeek.random
}
}
}