diff --git a/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift b/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift index 9be999a1..11c34eb7 100644 --- a/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift +++ b/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift @@ -8,12 +8,13 @@ import Foundation struct PopupInformationResponseDTO: Decodable { - let popupId: Int - let popupImagesUrl: [String] - let popupTitle: String + let id: Int + let imageUrls: [String] + let title: String let startDate: String let endDate: String let isPick: Bool + let hashtag: String let address: String let officialLink: String @@ -22,18 +23,19 @@ struct PopupInformationResponseDTO: Decodable { let reservationUrl: String enum CodingKeys: String, CodingKey { - case popupId - case popupImagesUrl = "popupImage" - case popupTitle = "title" + case id = "popupId" + case imageUrls = "popupImage" + case title case startDate = "startedAt" case endDate = "endedAt" case isPick = "isLiked" + case hashtag = "interest" case address = "location" case officialLink = "organizerUrl" - case businesesHours = "hours" - case introduce = "contents" - case reservationUrl + case businesesHours = "business_hours" + case introduce = "content" + case reservationUrl = "reservationUrl" } } @@ -44,13 +46,13 @@ extension PopupInformationResponseDTO { let endDate = DateFormatter.apiDateFormatter.date(from: endDate) ?? errorDate return PopupInformation( - id: popupId, - imageUrls: popupImagesUrl, - title: popupTitle, + id: id, + imageUrls: imageUrls, + title: title, startDate: startDate, endDate: endDate, isPick: isPick, - hashTags: [], + hashTags: [hashtag], address: address, organizationUrl: officialLink, businesesHours: businesesHours, diff --git a/Popcorn-iOS/Source/Data/Network/APIConstant.swift b/Popcorn-iOS/Source/Data/Network/APIConstant.swift index 7c373d1e..5235635f 100644 --- a/Popcorn-iOS/Source/Data/Network/APIConstant.swift +++ b/Popcorn-iOS/Source/Data/Network/APIConstant.swift @@ -47,11 +47,11 @@ struct APIConstant { } static func popupRatingPath(popupId: String) -> String { - return "/popups/reviewrating/\(popupId)" + return "/api/popups/reviewrating/\(popupId)" } static func popupReviewPath(popupId: String) -> String { - return "/popups/reviews/\(popupId)" + return "/api/reviews/popups/\(popupId)" } static func popupReviewToggleLike(popupId: String) -> String { diff --git a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift index 7f62c80e..9d4ad656 100644 --- a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift +++ b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift @@ -17,186 +17,98 @@ final class PopupDetailRepository: PopupDetailRepositoryProtocol { } func fetchPopupAllData( - popupId: Int, - completion: @escaping (Result<(PopupInformation, PopupRatingDistribution, PopupReviewList), any Error> - ) -> Void) { + for popupId: Int + ) async throws -> (PopupInformation, PopupRatingDistribution, PopupReviewList) { // TODO: TokenRepository에서 access token 만료 시 자동으로 reissue 하는 로직 구현 후 리팩토링 guard let token = tokenRepository.fetchAccessToken() else { - completion(.failure(NSError( + throw NSError( domain: "PopupDetailRepository", code: -1, userInfo: [NSLocalizedDescriptionKey: "액세스 토큰 만료"] - ))) - return + ) } - let dispatchGroup = DispatchGroup() - var capturedErrors = [NetworkError]() - let lock = NSLock() + async let information = fetchInformation(popupId: popupId, token: token) + async let ratingDistribution = fetchRatingDistribution(popupId: popupId, token: token) + async let reviewList = fetchReviewList(popupId: popupId, page: 1) - var popupInformationResponse: PopupInformationResponseDTO? - var popupRatingDistributionResponse: PopupRatingDistributionResponseDTO? - var popupReviewListResponse: PopupReviewListResponseDTO? + return try await (information, ratingDistribution, reviewList) + } - let popupInformationEndpoint = Endpoint( - httpMethod: .get, - path: APIConstant.popupDetailPath(popupId: APIConstant.popupDetailPath(popupId: String(popupId))), + func togglePopupPick(popupId: Int) async throws -> Bool { + // TODO: TokenRepository에서 access token 만료 시 자동으로 reissue 하는 로직 구현 후 리팩토링 + guard let token = tokenRepository.fetchAccessToken() else { + throw NSError( + domain: "PopupDetailRepository", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "액세스 토큰 만료"] + ) + } + + let endpoint = Endpoint>( + httpMethod: .post, + path: APIConstant.popupTogglePick(popupId: String(popupId)), headers: ["Authorization": "Bearer \(token)"] ) - let popupRatingDistributionEndpoint = Endpoint( - httpMethod: .get, - path: APIConstant.popupRatingPath(popupId: String(popupId)) - ) + let isPick = try await networkManager.request(endpoint: endpoint).data + return isPick + } +} - let popupReviewListEndpoint = Endpoint( +extension PopupDetailRepository { + func fetchInformation(popupId: Int, token: String) async throws -> PopupInformation { + print(popupId) + let endpoint = Endpoint( httpMethod: .get, - path: APIConstant.popupReviewPath(popupId: String(popupId)), - queryItems: [URLQueryItem(name: "page", value: "1")], + path: APIConstant.popupDetailPath(popupId: String(1)), // TODO: - 서버 데이터 변경 후 1을 popupId로 변경 headers: ["Authorization": "Bearer \(token)"] ) - dispatchGroup.enter() - networkManager.request(endpoint: popupInformationEndpoint) { result in - lock.lock() - defer { - lock.unlock() - dispatchGroup.leave() - } - - if !capturedErrors.isEmpty { return } - - switch result { - case .success(let response): - popupInformationResponse = response - case .failure(let error): - capturedErrors.append(error) - } - } - - dispatchGroup.enter() - networkManager.request(endpoint: popupRatingDistributionEndpoint) { result in - lock.lock() - defer { - lock.unlock() - dispatchGroup.leave() - } - - if !capturedErrors.isEmpty { return } - - switch result { - case .success(let response): - popupRatingDistributionResponse = response - case .failure(let error): - capturedErrors.append(error) - } - } - - dispatchGroup.enter() - networkManager.request(endpoint: popupReviewListEndpoint) { result in - lock.lock() - defer { - lock.unlock() - dispatchGroup.leave() - } - - if !capturedErrors.isEmpty { return } - - switch result { - case .success(let response): - popupReviewListResponse = response - case .failure(let error): - capturedErrors.append(error) - } - } - - dispatchGroup.notify(queue: .main) { - if !capturedErrors.isEmpty { - let combinedError = NSError( - domain: "PopupDetailRepsitory", - code: -2, - userInfo: [ - NSLocalizedDescriptionKey: "상세화면 데이터 요청 실패", - "error": capturedErrors - ] - ) - - completion(.failure(combinedError)) - return - } - - guard let popupInformationResponse, - let popupRatingDistributionResponse, - let popupReviewListResponse else { return } - - let popupReviewList = PopupReviewList(reviews: popupReviewListResponse.reviews.map { $0.toEntity() }) - - completion(.success(( - popupInformationResponse.toEntity(), - popupRatingDistributionResponse.toEntity(), - popupReviewList - ))) + do { + return try await networkManager.request(endpoint: endpoint).toEntity() + } catch { + print(#function, error) + throw error } } - func fetchPopupReviews( - popupId: Int, - page: Int, - completion: @escaping (Result - ) -> Void) { - // TODO: TokenRepository에서 access token 만료 시 자동으로 reissue 하는 로직 구현 후 리팩토링 - guard let token = tokenRepository.fetchAccessToken() else { - completion(.failure(NSError( - domain: "PopupDetailRepository", - code: -1, - userInfo: [NSLocalizedDescriptionKey: "액세스 토큰 만료"] - ))) - return - } - - let endpoint = Endpoint( + func fetchRatingDistribution(popupId: Int, token: String) async throws -> PopupRatingDistribution { + let endpoint = Endpoint>( httpMethod: .get, - path: APIConstant.popupReviewPath(popupId: String(popupId)), - queryItems: [URLQueryItem(name: "page", value: String(page))], - headers: ["Authorization": "Bearer \(token)"] + path: APIConstant.popupRatingPath(popupId: String(popupId)) ) - networkManager.request(endpoint: endpoint) { result in - switch result { - case .success(let response): - let reviewList = response.reviews.map { $0.toEntity() } - completion(.success(PopupReviewList(reviews: reviewList))) - case .failure(let error): - completion(.failure(error)) - } + do { + return try await networkManager.request(endpoint: endpoint).data.toEntity() + } catch { + print(#function, error) + throw error } } - func togglePopupPick(popupId: Int, completion: @escaping (Result) -> Void) { - // TODO: TokenRepository에서 access token 만료 시 자동으로 reissue 하는 로직 구현 후 리팩토링 + func fetchReviewList(popupId: Int, page: Int) async throws -> PopupReviewList { guard let token = tokenRepository.fetchAccessToken() else { - completion(.failure(NSError( + throw NSError( domain: "PopupDetailRepository", code: -1, userInfo: [NSLocalizedDescriptionKey: "액세스 토큰 만료"] - ))) - return + ) } - let endpoint = Endpoint>( - httpMethod: .post, - path: APIConstant.popupTogglePick(popupId: String(popupId)), + let endpoint = Endpoint>( + httpMethod: .get, + path: APIConstant.popupReviewPath(popupId: String(popupId)), + queryItems: [URLQueryItem(name: "page", value: String(page))], headers: ["Authorization": "Bearer \(token)"] ) - networkManager.request(endpoint: endpoint) { result in - switch result { - case .success(let response): - let isPick = response.data - completion(.success(isPick)) - case .failure(let error): - completion(.failure(error)) - } + do { + let dto = try await networkManager.request(endpoint: endpoint).data + return PopupReviewList(reviews: dto.reviews.map { $0.toEntity() }) + } catch { + print(#function, error) + throw error } } } diff --git a/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift b/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift index 27649f1f..4b864961 100644 --- a/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift +++ b/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift @@ -9,12 +9,11 @@ import Foundation protocol PopupDetailRepositoryProtocol { func fetchPopupAllData( - popupId: Int, - completion: @escaping (Result<(PopupInformation, PopupRatingDistribution, PopupReviewList), Error>) -> Void - ) + for popupId: Int + ) async throws -> (PopupInformation, PopupRatingDistribution, PopupReviewList) - func fetchPopupReviews(popupId: Int, page: Int, completion: @escaping (Result) -> Void) + func fetchReviewList(popupId: Int, page: Int) async throws -> PopupReviewList - func togglePopupPick(popupId: Int, completion: @escaping (Result) -> Void) + func togglePopupPick(popupId: Int) async throws -> Bool // 리뷰 좋아요 토글, 리뷰 작성 추가 } diff --git a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift index 69a5eded..e891df67 100644 --- a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift +++ b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift @@ -9,41 +9,32 @@ import Foundation final class PopupDetailUseCase: PopupDetailUseCaseProtocol { private let repository: PopupDetailRepositoryProtocol - + init(repository: PopupDetailRepositoryProtocol) { self.repository = repository } - + func fetchPopupAllData( - popupId: Int, - completion: @escaping (Result<(PopupInformation, PopupRatingDistribution, PopupReviewList), Error> - ) -> Void) { - repository.fetchPopupAllData(popupId: popupId) { [weak self] result in - guard let self else { return } - switch result { - case .success(let (popupInfo, popupRatingDistribution, popupReviewList)): - var popupInfo = popupInfo - popupInfo.hashTags = self.extractHashTag(from: popupInfo) - completion(.success((popupInfo, popupRatingDistribution, popupReviewList))) - case .failure(let error): - completion(.failure(error)) - } - } + for popupId: Int + ) async throws -> (PopupInformation, PopupRatingDistribution, PopupReviewList) { + var (information, ratingDistribution, reviewList) = try await repository.fetchPopupAllData(for: popupId) + information.hashTags = extractHashTag(from: information) + return (information, ratingDistribution, reviewList) } - + func fetchPopupReviews( popupId: Int, - page: Int, - completion: @escaping (Result - ) -> Void - ) { - repository.fetchPopupReviews(popupId: popupId, page: page, completion: completion) + page: Int + ) async throws -> PopupReviewList { + return try await repository.fetchReviewList(popupId: popupId, page: page) } - - func togglePopupPick(popupId: Int, completion: @escaping (Result) -> Void) { - repository.togglePopupPick(popupId: popupId, completion: completion) + + func togglePopupPick(popupId: Int) async throws -> Bool { + return try await repository.togglePopupPick(popupId: popupId) } +} +extension PopupDetailUseCase { func extractHashTag(from popupInformation: PopupInformation) -> [String] { let address = popupInformation.address let dDay = PopupDateFormatter.calculateDDay(from: popupInformation.endDate) diff --git a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift index 056faf48..9e3c5393 100644 --- a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift +++ b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift @@ -7,22 +7,12 @@ protocol PopupDetailUseCaseProtocol { func fetchPopupAllData( - popupId: Int, - completion: @escaping (Result<(PopupInformation, PopupRatingDistribution, PopupReviewList), Error>) -> Void - ) + for popupId: Int + ) async throws -> (PopupInformation, PopupRatingDistribution, PopupReviewList) - func fetchPopupReviews(popupId: Int, page: Int, completion: @escaping (Result) -> Void) + func fetchPopupReviews(popupId: Int, page: Int) async throws -> PopupReviewList - func togglePopupPick(popupId: Int, completion: @escaping (Result) -> Void) + func togglePopupPick(popupId: Int) async throws -> Bool func extractHashTag(from popupInformation: PopupInformation) -> [String] } - -extension PopupDetailUseCaseProtocol { - func fetchPopupAllData( - popupId: Int = 1, - completion: @escaping (Result<(PopupInformation, PopupRatingDistribution, PopupReviewList), Error>) -> Void - ) { - fetchPopupAllData(popupId: popupId, completion: completion) - } -} diff --git a/Popcorn-iOS/Source/Presentation/MainScene/Common/View/MainCarouselView.swift b/Popcorn-iOS/Source/Presentation/MainScene/Common/View/MainCarouselView.swift index cd03dfe9..315e4cdd 100644 --- a/Popcorn-iOS/Source/Presentation/MainScene/Common/View/MainCarouselView.swift +++ b/Popcorn-iOS/Source/Presentation/MainScene/Common/View/MainCarouselView.swift @@ -53,8 +53,10 @@ final class MainCarouselView: UIView { private func bind(to viewModel: MainCarouselViewModelProtocol) { viewModel.carouselImagePublisher = { [weak self] in guard let self else { return } - self.imagePageControl.numberOfPages = viewModel.numbersOfCarouselImage() - self.carouselCollectionView.reloadData() + DispatchQueue.main.async { + self.imagePageControl.numberOfPages = viewModel.numbersOfCarouselImage() + self.carouselCollectionView.reloadData() + } } } diff --git a/Popcorn-iOS/Source/Presentation/MainScene/DataSource/PopupDetailDataSource.swift b/Popcorn-iOS/Source/Presentation/MainScene/DataSource/PopupDetailDataSource.swift index 9e7556dd..5b0ed606 100644 --- a/Popcorn-iOS/Source/Presentation/MainScene/DataSource/PopupDetailDataSource.swift +++ b/Popcorn-iOS/Source/Presentation/MainScene/DataSource/PopupDetailDataSource.swift @@ -46,7 +46,10 @@ extension PopupDetailDataSource { } func popupImageItem(at indexPath: IndexPath) -> String { - guard let popupMainInformation else { return "" } + guard let popupMainInformation, + indexPath.row < popupMainInformation.popupImagesUrl.count + else { return "" } + return popupMainInformation.popupImagesUrl[indexPath.row] } diff --git a/Popcorn-iOS/Source/Presentation/MainScene/PopupDetailView/PopupDetailViewController.swift b/Popcorn-iOS/Source/Presentation/MainScene/PopupDetailView/PopupDetailViewController.swift index f5a25ac3..c6a7ab41 100644 --- a/Popcorn-iOS/Source/Presentation/MainScene/PopupDetailView/PopupDetailViewController.swift +++ b/Popcorn-iOS/Source/Presentation/MainScene/PopupDetailView/PopupDetailViewController.swift @@ -39,7 +39,7 @@ final class PopupDetailViewController: UIViewController { configureSubviews() configureLayout() bind(to: viewModel) - mockingData() + viewModel.fetchPopupDetail(for: popupId) } private func bind(to viewModel: PopupDetailViewModel) { diff --git a/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift b/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift index 1634e1d0..4e7d5fd3 100644 --- a/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift +++ b/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift @@ -9,8 +9,8 @@ import Foundation final class PopupDetailViewModel: MainCarouselViewModelProtocol { private let imageFetchUseCase: ImageFetchUseCaseProtocol - private let popupDetailUseCase: PopupDetailUseCaseProtocol - private let popupDetailDataSource: PopupDetailDataSource + private let useCase: PopupDetailUseCaseProtocol + private let dataSource: PopupDetailDataSource private var reviewPage = 1 // MARK: - Output @@ -25,35 +25,37 @@ final class PopupDetailViewModel: MainCarouselViewModelProtocol { popupDetailDataSource: PopupDetailDataSource = PopupDetailDataSource() ) { self.imageFetchUseCase = imageFetchUseCase - self.popupDetailUseCase = popupDetailUseCase - self.popupDetailDataSource = popupDetailDataSource + self.useCase = popupDetailUseCase + self.dataSource = popupDetailDataSource } func getDataSource() -> PopupDetailDataSource { - return popupDetailDataSource + return dataSource } func isFinished() -> Bool { - return popupDetailDataSource.detailInformationItem().isFinished + return dataSource.detailInformationItem().isFinished } func isWriteReviewEnabled() -> Bool { - return popupDetailDataSource.detailInformationItem().isWriteReviewEnabled + return dataSource.detailInformationItem().isWriteReviewEnabled } } // MARK: - Input extension PopupDetailViewModel { func didTapPickButton(for popupId: Int) { - popupDetailUseCase.togglePopupPick(popupId: popupId) { [weak self] result in - guard let self else { return } - switch result { - case .success(let isPick): - self.popupDetailDataSource.updatePickStatus(isPick) + Task { + do { + let isPick = try await useCase.togglePopupPick(popupId: popupId) + dataSource.updatePickStatus(isPick) popupPickPublisher?(isPick) - case .failure(let error): - // TODO: 에러 UI 처리 - print("찜하기 실패: \(error)") + } catch { + if let error = error as? NetworkError { + print(#function, error.description) + } else { + print(#function, error) + } } } } @@ -74,32 +76,42 @@ extension PopupDetailViewModel { imageFetchUseCase.fetchImage(url: url, completion: completion) } - func fetchPopupInformation() { - popupDetailUseCase.fetchPopupAllData { [weak self] result in - guard let self else { return } - switch result { - case .success(let (popupInformation, popupRatingDistribution, popupReviewList)): - self.popupDetailDataSource.updateInformationData(popupInformation) - self.popupDetailDataSource.updateRatingData(popupRatingDistribution) - self.popupDetailDataSource.updateReviewData(popupReviewList) - case .failure: - popupDetailDataSource.showPlaceholderData() + func fetchPopupDetail(for popupId: Int) { + Task { + do { + let (information, ratingDistribution, reviewList) = try await useCase.fetchPopupAllData(for: popupId) + dataSource.updateInformationData(information) + dataSource.updateRatingData(ratingDistribution) + dataSource.updateReviewData(reviewList) + } catch { + dataSource.showPlaceholderData() + + if let error = error as? NetworkError { + print(#function, error.description) + } else { + print(#function, error) + } } + + carouselImagePublisher?() + popupInformationPublisher?() + popupReviewPublisher?() } - carouselImagePublisher?() - popupInformationPublisher?() - popupReviewPublisher?() } func fetchPopupReview() { - let popupId = popupDetailDataSource.getPopupId() - popupDetailUseCase.fetchPopupReviews(popupId: popupId, page: reviewPage) { [weak self] result in - guard let self else { return } - switch result { - case .success(let popupReviewList): - self.popupDetailDataSource.updateReviewData(popupReviewList) - case .failure: - popupDetailDataSource.showPlaceholderReviewData() + let popupId = dataSource.getPopupId() + Task { + do { + let popupReviewList = try await useCase.fetchPopupReviews(popupId: popupId, page: reviewPage) + dataSource.updateReviewData(popupReviewList) + } catch { + dataSource.showPlaceholderReviewData() + if let error = error as? NetworkError { + print(#function, error.description) + } else { + print(#function, error) + } } } popupReviewPublisher?() @@ -113,11 +125,11 @@ extension PopupDetailViewModel { // MARK: - Implement MainCarouselViewModelProtocol extension PopupDetailViewModel { func numbersOfCarouselImage() -> Int { - return popupDetailDataSource.numberOfCarouseImage() + return dataSource.numberOfCarouseImage() } func provideCarouselImageUrl(at indexPath: IndexPath) -> String { - return popupDetailDataSource.popupImageItem(at: indexPath) + return dataSource.popupImageItem(at: indexPath) } } diff --git a/Popcorn-iOSTests/TestDouble/Dummy/DummyPopupDetailRepository.swift b/Popcorn-iOSTests/TestDouble/Dummy/DummyPopupDetailRepository.swift index 90ae893a..a9f76e11 100644 --- a/Popcorn-iOSTests/TestDouble/Dummy/DummyPopupDetailRepository.swift +++ b/Popcorn-iOSTests/TestDouble/Dummy/DummyPopupDetailRepository.swift @@ -8,21 +8,21 @@ @testable import Popcorn_iOS final class DummyPopupDetailRepository: PopupDetailRepositoryProtocol { - func togglePopupPick(popupId: Int, completion: @escaping (Result) -> Void) { + func fetchPopupAllData(for popupId: Int) async throws -> ( + PopupInformation, + PopupRatingDistribution, + PopupReviewList + ) { + throw NetworkError.emptyData } - func fetchPopupAllData(popupId: Int, completion: @escaping ( - Result<(Popcorn_iOS.PopupInformation, Popcorn_iOS.PopupRatingDistribution, - Popcorn_iOS.PopupReviewList), any Error>) -> Void - ) { - completion(.failure(NetworkError.emptyData)) + func fetchReviewList(popupId: Int, page: Int) async throws -> Popcorn_iOS.PopupReviewList { + throw NetworkError.emptyData + } - func fetchPopupReviews( - popupId: Int, - page: Int, - completion: @escaping (Result) -> Void - ) { - completion(.failure(NetworkError.emptyData)) + func togglePopupPick(popupId: Int) async throws -> Bool { + throw NetworkError.emptyData } + }