import Foundation import Combine struct CreatorChannelAudioThemeItem: Hashable { let themeId: Int? let themeName: String } final class CreatorChannelAudioViewModel: ObservableObject { private let repository = CreatorChannelAudioRepository() private var subscription = Set() private let pageSize = 20 private var latestRequestId = 0 @Published var isLoading = false @Published var isLoadingNextPage = false @Published var response: CreatorChannelAudioTabResponse? @Published var audioContents = [CreatorChannelAudioContentResponse]() @Published var selectedSort: ContentSort = .latest @Published var selectedTheme = CreatorChannelAudioThemeItem.all @Published var page = 0 @Published var size = 20 @Published var hasNext = false @Published var hasLoaded = false @Published var errorMessage = "" @Published var isShowPopup = false var displayThemes: [CreatorChannelAudioThemeItem] { let themes = response?.themes.map { CreatorChannelAudioThemeItem(themeId: $0.themeId, themeName: $0.themeName) } ?? [] return [CreatorChannelAudioThemeItem.all] + themes } var isAllThemeSelected: Bool { selectedTheme.themeId == nil } func fetchFirstPage(creatorId: Int) { fetchAudio(creatorId: creatorId, page: 0, isNextPage: false) } func fetchNextPageIfNeeded(creatorId: Int, currentItem: CreatorChannelAudioContentResponse) { guard audioContents.last?.audioContentId == currentItem.audioContentId else { return } guard hasNext, isLoadingNextPage == false, isLoading == false else { return } fetchAudio(creatorId: creatorId, page: page + 1, isNextPage: true) } func selectSort(_ sort: ContentSort, creatorId: Int) { guard selectedSort != sort else { return } selectedSort = sort fetchFirstPage(creatorId: creatorId) } func selectTheme(_ theme: CreatorChannelAudioThemeItem, creatorId: Int) { guard selectedTheme != theme else { return } selectedTheme = theme fetchFirstPage(creatorId: creatorId) } private func fetchAudio(creatorId: Int, page requestPage: Int, isNextPage: Bool) { latestRequestId += 1 let requestId = latestRequestId if isNextPage { isLoadingNextPage = true } else { isLoading = true } repository.getAudio( creatorId: creatorId, page: requestPage, size: pageSize, sort: selectedSort, themeId: selectedTheme.themeId ) .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 } var shouldFetchNextPage = false do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: response.data) if let data = decoded.data, decoded.success { shouldFetchNextPage = self.applySuccessState(data, isNextPage: isNextPage) } else { self.errorMessage = decoded.message ?? I18n.Common.commonError self.isShowPopup = true } } catch { ERROR_LOG(error.localizedDescription) self.errorMessage = I18n.Common.commonError self.isShowPopup = true } self.hasLoaded = true self.isLoading = false self.isLoadingNextPage = false if shouldFetchNextPage { self.fetchAudio(creatorId: creatorId, page: self.page + 1, isNextPage: true) } } .store(in: &subscription) } private func applySuccessState(_ data: CreatorChannelAudioTabResponse, isNextPage: Bool) -> Bool { response = data selectedSort = data.sort selectedTheme = selectedTheme(from: data) page = data.page size = data.size hasNext = data.hasNext let visibleAudioContents = data.audioContents.filter { guard let duration = $0.duration?.trimmingCharacters(in: .whitespacesAndNewlines) else { return false } return duration.isEmpty == false } if isNextPage { audioContents.append(contentsOf: visibleAudioContents) } else { audioContents = visibleAudioContents } return visibleAudioContents.isEmpty && data.hasNext } private func selectedTheme(from data: CreatorChannelAudioTabResponse) -> CreatorChannelAudioThemeItem { guard let themeId = data.themeId else { return .all } if let theme = data.themes.first(where: { $0.themeId == themeId }) { return CreatorChannelAudioThemeItem(themeId: theme.themeId, themeName: theme.themeName) } return CreatorChannelAudioThemeItem(themeId: themeId, themeName: selectedTheme.themeName) } private func applyFailureState() { errorMessage = I18n.Common.commonError isShowPopup = true hasLoaded = true isLoading = false isLoadingNextPage = false } } private extension CreatorChannelAudioThemeItem { static var all: CreatorChannelAudioThemeItem { CreatorChannelAudioThemeItem(themeId: nil, themeName: I18n.CreatorChannelAudio.totalLabel) } }