// // AuditionRoleDetailViewModel.swift // SodaLive // // Created by klaus on 1/6/25. // import Foundation import Moya import Combine final class AuditionRoleDetailViewModel: ObservableObject { private let repository = AuditionRepository() private var subscription = Set() @Published var errorMessage = "" @Published var isShowPopup = false @Published var isLoading = false @Published var totalCount = 0 @Published var applicantList = [GetAuditionRoleApplicantItem]() @Published var name = I18n.Audition.defaultTitle @Published var auditionRoleDetail: GetAuditionRoleDetailResponse? = nil @Published private(set) var sortType = AuditionApplicantSortType.NEWEST { didSet { refreshApplicantList() } } @Published var fileName = "" @Published var soundData: Data? = nil @Published var phoneNumber = "" @Published var isShowNotifyVote = true @Published var isShowVoteCompleteView = false @Published var isShowNoticeReapply = false @Published var dialogTitle = "" @Published var dialogDesc = "" var page = 1 var isLast = false private var pageSize = 10 var auditionRoleId = -1 { didSet { if auditionRoleId > 0 { getAuditionRoleDetail() } else { onFailure() } } } var onFailure: () -> Void = {} func setSortType(sortType: AuditionApplicantSortType) { if self.sortType != sortType { self.sortType = sortType } } func getAuditionRoleDetail() { isLoading = true let auditionRoleDetail = repository.getAuditionRoleDetail(auditionRoleId: auditionRoleId) let auditionApplicantList = repository.getAuditionApplicantList(auditionRoleId: auditionRoleId, sortType: sortType, page: page, size: pageSize) Publishers .CombineLatest(auditionRoleDetail, auditionApplicantList) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] (roleDetailResponse, applicantListResponse) in let roleDetail = roleDetailResponse.data let applicantList = applicantListResponse.data let jsonDecoder = JSONDecoder() do { let roleDetailDecoded = try jsonDecoder.decode(ApiResponse.self, from: roleDetail) if let data = roleDetailDecoded.data, roleDetailDecoded.success { self.name = data.name self.isShowNoticeReapply = data.isAlreadyApplicant self.auditionRoleDetail = data } else { if let message = roleDetailDecoded.message { self.errorMessage = message } else { self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { self.errorMessage = I18n.Common.commonError self.isShowPopup = true } do { let applicantListDecoded = try jsonDecoder.decode(ApiResponse.self, from: applicantList) if let data = applicantListDecoded.data, applicantListDecoded.success { self.totalCount = data.totalCount self.applicantList.append(contentsOf: data.items) if data.items.isEmpty { isLast = true } else { page += 1 } } else { if let message = applicantListDecoded.message { self.errorMessage = message } else { self.errorMessage = I18n.Common.commonError } self.isShowPopup = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.onFailure() } } } catch { self.errorMessage = I18n.Common.commonError self.isShowPopup = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self.onFailure() } } self.isLoading = false } .store(in: &subscription) } func getAuditionApplicantList() { if !isLoading && !isLast { isLoading = true repository.getAuditionApplicantList(auditionRoleId: auditionRoleId, sortType: sortType, page: page, size: pageSize) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { [unowned self] response in let responseData = response.data self.isLoading = false do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) if let data = decoded.data, decoded.success { self.totalCount = data.totalCount self.applicantList.append(contentsOf: data.items) if data.items.isEmpty { isLast = true } else { page += 1 } } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = I18n.Common.commonError } self.isShowPopup = true } } catch { self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } .store(in: &subscription) } } func applyAudition(onSuccess: @escaping () -> Void) { if phoneNumber.count != 11 { errorMessage = I18n.Audition.Apply.invalidContact isShowPopup = true return } guard let soundData = soundData else { errorMessage = I18n.Audition.Apply.invalidRecordingFile isShowPopup = true return } isLoading = true let request = ApplyAuditionRoleRequest(roleId: auditionRoleId, phoneNumber: phoneNumber) var multipartData = [MultipartFormData]() let encoder = JSONEncoder() encoder.outputFormatting = .withoutEscapingSlashes let jsonData = try? encoder.encode(request) if let jsonData = jsonData { multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) multipartData.append( MultipartFormData( provider: .data(soundData), name: "contentFile", fileName: fileName, mimeType: "audio/*" ) ) repository.applyAudition(parameters: multipartData) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { self.deleteAllRecordingFilesWithNamePrefix("voiceon_now_voice") self.phoneNumber = "" self.fileName = "" self.soundData = nil self.refreshApplicantList() onSuccess() } else { if let message = decoded.message { self.errorMessage = message } else { self.errorMessage = I18n.Audition.Apply.applyFailed } self.isShowPopup = true } } catch { self.errorMessage = I18n.Audition.Apply.applyFailed self.isShowPopup = true } } .store(in: &subscription) } else { self.errorMessage = I18n.Audition.Apply.applyFailed self.isShowPopup = true self.isLoading = false } } func voteApplicant(applicantId: Int) { isLoading = true repository.voteApplicant(applicantId: applicantId) .sink { result in switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) } } receiveValue: { response in self.isLoading = false let responseData = response.data do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) if decoded.success { if self.isShowNotifyVote { self.dialogTitle = I18n.Audition.Vote.cheerTitle self.dialogDesc = I18n.Audition.Vote.cheerDescription self.isShowVoteCompleteView = true } if let index = self.applicantList.firstIndex(where: { $0.applicantId == applicantId }) { var applicant = self.applicantList[index] applicant.voteCount += 1 self.applicantList.remove(at: index) self.applicantList.insert(applicant, at: index) } } else { if let message = decoded.message { if message.contains("오늘 응원은 여기까지") { self.dialogTitle = I18n.Audition.Vote.limitTitle self.dialogDesc = I18n.Audition.Vote.limitDescription self.isShowVoteCompleteView = true } else { self.errorMessage = message self.isShowPopup = true } } else { self.errorMessage = I18n.Audition.Vote.unknownError self.isShowPopup = true } } } catch { self.errorMessage = I18n.Audition.Vote.unknownError self.isShowPopup = true } } .store(in: &subscription) } private func refreshApplicantList() { self.page = 1 self.isLast = false self.totalCount = 0 self.applicantList = [] self.getAuditionApplicantList() } func deleteAllRecordingFilesWithNamePrefix(_ prefix: String) { let fileManager = FileManager.default let documentsURL = getDocumentsDirectory() do { let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil, options: []) for fileURL in fileURLs { if fileURL.lastPathComponent.hasPrefix(prefix) { try fileManager.removeItem(at: fileURL) DEBUG_LOG("녹음 파일 삭제 성공: \(fileURL)") } } } catch { DEBUG_LOG("녹음 파일 삭제 실패: \(error.localizedDescription)") } } private func getDocumentsDirectory() -> URL { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) return paths[0] } }