From 658e35d9171d73f1a012306277042698db22377c Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 01:53:41 +0900 Subject: [PATCH 01/10] =?UTF-8?q?rename:=20API=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Popcorn-iOS/Source/Data/Network/APIConstant.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 { From 3d950d430c5034cfba6e3b7a745a7afbd224e025 Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 01:53:52 +0900 Subject: [PATCH 02/10] =?UTF-8?q?refactor:=20PopupInformationResponseDTO?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopupDetailScene/PopupInformationResponseDTO.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift b/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift index 9be999a1..a117b5af 100644 --- a/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift +++ b/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift @@ -14,6 +14,7 @@ struct PopupInformationResponseDTO: Decodable { let startDate: String let endDate: String let isPick: Bool + let hashtag: String let address: String let officialLink: String @@ -28,12 +29,13 @@ struct PopupInformationResponseDTO: Decodable { 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" } } @@ -50,7 +52,7 @@ extension PopupInformationResponseDTO { startDate: startDate, endDate: endDate, isPick: isPick, - hashTags: [], + hashTags: [hashtag], address: address, organizationUrl: officialLink, businesesHours: businesesHours, From 7d98c35652de8048156e690384561eea159838f3 Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 01:54:20 +0900 Subject: [PATCH 03/10] =?UTF-8?q?hotfix:=20MainCarouselView=EC=9D=98=20UI?= =?UTF-8?q?=20=EA=B0=B1=EC=8B=A0=20=EB=A1=9C=EC=A7=81=20=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=8A=A4=EB=A0=88=EB=93=9C=EC=97=90=EC=84=9C=20=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainScene/Common/View/MainCarouselView.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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() + } } } From fe54caf30a9681694b599852640335f563141308 Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 01:54:34 +0900 Subject: [PATCH 04/10] =?UTF-8?q?hotfix:=20PopupDetailDataSource=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B0=B0=EC=97=B4=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainScene/DataSource/PopupDetailDataSource.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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] } From 428abc6db5c5874ca349ed77a2ed5d1f96fa0811 Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 01:59:05 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=EC=83=81=EC=84=B8=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=A0=95=EB=B3=B4,=20=ED=8C=9D=EC=97=85=20?= =?UTF-8?q?=ED=8F=89=EC=A0=90=20=EB=B6=84=ED=8F=AC,=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20fetch=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 디버깅을 위해 각 요청 메서드 분리 --- .../MainScene/PopupDetailRepository.swift | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift index 7f62c80e..a8bd70cd 100644 --- a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift +++ b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift @@ -200,3 +200,52 @@ final class PopupDetailRepository: PopupDetailRepositoryProtocol { } } } + +extension PopupDetailRepository { + func fetchInformation(popupId: Int, token: String) async throws -> PopupInformation { + print(popupId) + let endpoint = Endpoint( + httpMethod: .get, + path: APIConstant.popupDetailPath(popupId: String(1)), + headers: ["Authorization": "Bearer \(token)"] + ) + + do { + return try await networkManager.request(endpoint: endpoint).toEntity() + } catch { + print(#function, error) + throw error + } + } + + func fetchRatingDistribution(popupId: Int, token: String) async throws -> PopupRatingDistribution { + let endpoint = Endpoint>( + httpMethod: .get, + path: APIConstant.popupRatingPath(popupId: String(popupId)) + ) + + do { + return try await networkManager.request(endpoint: endpoint).data.toEntity() + } catch { + print(#function, error) + throw error + } + } + + func fetchReviewList(popupId: Int, token: String) async throws -> PopupReviewList { + let endpoint = Endpoint>( + httpMethod: .get, + path: APIConstant.popupReviewPath(popupId: String(popupId)), + queryItems: [URLQueryItem(name: "page", value: "1")], + headers: ["Authorization": "Bearer \(token)"] + ) + + do { + let dto = try await networkManager.request(endpoint: endpoint).data + return PopupReviewList(reviews: dto.reviews.map { $0.toEntity() }) + } catch { + print(#function, error) + throw error + } + } +} From 82fb4828655ee2baef47a37b42bcc1cad1043939 Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 02:13:10 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor:=20Swift=20Concurrency=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20-=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=ED=99=94=EB=A9=B4=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20fetch=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainScene/PopupDetailRepository.swift | 122 ++---------------- .../PopupDetailRepositoryProtocol.swift | 5 +- .../Implementations/PopupDetailUseCase.swift | 19 +-- .../PopupDetailUseCaseProtocol.swift | 14 +- .../PopupDetailViewController.swift | 2 +- .../ViewModel/PopupDetailViewModel.swift | 30 +++-- 6 files changed, 37 insertions(+), 155 deletions(-) diff --git a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift index a8bd70cd..b2e6881c 100644 --- a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift +++ b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift @@ -17,126 +17,22 @@ 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() - - var popupInformationResponse: PopupInformationResponseDTO? - var popupRatingDistributionResponse: PopupRatingDistributionResponseDTO? - var popupReviewListResponse: PopupReviewListResponseDTO? - - let popupInformationEndpoint = Endpoint( - httpMethod: .get, - path: APIConstant.popupDetailPath(popupId: APIConstant.popupDetailPath(popupId: String(popupId))), - headers: ["Authorization": "Bearer \(token)"] - ) - - let popupRatingDistributionEndpoint = Endpoint( - httpMethod: .get, - path: APIConstant.popupRatingPath(popupId: String(popupId)) - ) + async let information = fetchInformation(popupId: popupId, token: token) + async let ratingDistribution = fetchRatingDistribution(popupId: popupId, token: token) + async let reviewList = fetchReviewList(popupId: popupId, token: token) - let popupReviewListEndpoint = Endpoint( - httpMethod: .get, - path: APIConstant.popupReviewPath(popupId: String(popupId)), - queryItems: [URLQueryItem(name: "page", value: "1")], - 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 - ))) - } + return try await (information, ratingDistribution, reviewList) } func fetchPopupReviews( @@ -206,7 +102,7 @@ extension PopupDetailRepository { print(popupId) let endpoint = Endpoint( httpMethod: .get, - path: APIConstant.popupDetailPath(popupId: String(1)), + path: APIConstant.popupDetailPath(popupId: String(1)), // TODO: - 서버 데이터 변경 후 1을 popupId로 변경 headers: ["Authorization": "Bearer \(token)"] ) diff --git a/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift b/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift index 27649f1f..0cb11f7a 100644 --- a/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift +++ b/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift @@ -9,9 +9,8 @@ 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) diff --git a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift index 69a5eded..ebb22c03 100644 --- a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift +++ b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift @@ -15,20 +15,11 @@ final class PopupDetailUseCase: PopupDetailUseCaseProtocol { } 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( diff --git a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift index 056faf48..f871c323 100644 --- a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift +++ b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift @@ -7,9 +7,8 @@ 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) @@ -17,12 +16,3 @@ protocol PopupDetailUseCaseProtocol { 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/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..9b6d1731 100644 --- a/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift +++ b/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift @@ -74,21 +74,27 @@ 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: + func fetchPopupDetail(for popupId: Int) { + Task { + do { + let (information, ratingDistribution, reviewList) = try await popupDetailUseCase.fetchPopupAllData(for: popupId) + popupDetailDataSource.updateInformationData(information) + popupDetailDataSource.updateRatingData(ratingDistribution) + popupDetailDataSource.updateReviewData(reviewList) + } catch { popupDetailDataSource.showPlaceholderData() + + if let error = error as? NetworkError { + print(#function, error.description) + } else { + print(#function, error) + } } + + carouselImagePublisher?() + popupInformationPublisher?() + popupReviewPublisher?() } - carouselImagePublisher?() - popupInformationPublisher?() - popupReviewPublisher?() } func fetchPopupReview() { From b1c4dea3a907ace2629c8f67bd44586ef12863ad Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 02:17:56 +0900 Subject: [PATCH 07/10] =?UTF-8?q?rename:=20=EC=9D=B4=EB=A6=84=20=EA=B0=84?= =?UTF-8?q?=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopupInformationResponseDTO.swift | 18 ++++----- .../ViewModel/PopupDetailViewModel.swift | 40 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift b/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift index a117b5af..11c34eb7 100644 --- a/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift +++ b/Popcorn-iOS/Source/Data/DTO/MainScene/PopupDetailScene/PopupInformationResponseDTO.swift @@ -8,9 +8,9 @@ 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 @@ -23,9 +23,9 @@ 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" @@ -46,9 +46,9 @@ 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, diff --git a/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift b/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift index 9b6d1731..9b44c4a9 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,31 +25,31 @@ 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 + useCase.togglePopupPick(popupId: popupId) { [weak self] result in guard let self else { return } switch result { case .success(let isPick): - self.popupDetailDataSource.updatePickStatus(isPick) + self.dataSource.updatePickStatus(isPick) popupPickPublisher?(isPick) case .failure(let error): // TODO: 에러 UI 처리 @@ -77,12 +77,12 @@ extension PopupDetailViewModel { func fetchPopupDetail(for popupId: Int) { Task { do { - let (information, ratingDistribution, reviewList) = try await popupDetailUseCase.fetchPopupAllData(for: popupId) - popupDetailDataSource.updateInformationData(information) - popupDetailDataSource.updateRatingData(ratingDistribution) - popupDetailDataSource.updateReviewData(reviewList) + let (information, ratingDistribution, reviewList) = try await useCase.fetchPopupAllData(for: popupId) + dataSource.updateInformationData(information) + dataSource.updateRatingData(ratingDistribution) + dataSource.updateReviewData(reviewList) } catch { - popupDetailDataSource.showPlaceholderData() + dataSource.showPlaceholderData() if let error = error as? NetworkError { print(#function, error.description) @@ -98,14 +98,14 @@ extension PopupDetailViewModel { } func fetchPopupReview() { - let popupId = popupDetailDataSource.getPopupId() - popupDetailUseCase.fetchPopupReviews(popupId: popupId, page: reviewPage) { [weak self] result in + let popupId = dataSource.getPopupId() + useCase.fetchPopupReviews(popupId: popupId, page: reviewPage) { [weak self] result in guard let self else { return } switch result { case .success(let popupReviewList): - self.popupDetailDataSource.updateReviewData(popupReviewList) + self.dataSource.updateReviewData(popupReviewList) case .failure: - popupDetailDataSource.showPlaceholderReviewData() + dataSource.showPlaceholderReviewData() } } popupReviewPublisher?() @@ -119,11 +119,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) } } From f13431715b1e220503acb0b4352a8c20b7eb164c Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 02:26:33 +0900 Subject: [PATCH 08/10] =?UTF-8?q?refactor:=20Swift=20Concurrency=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20-=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20fetch=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainScene/PopupDetailRepository.swift | 47 +++++-------------- .../PopupDetailRepositoryProtocol.swift | 2 +- .../Implementations/PopupDetailUseCase.swift | 8 ++-- .../PopupDetailUseCaseProtocol.swift | 2 +- .../ViewModel/PopupDetailViewModel.swift | 16 ++++--- 5 files changed, 26 insertions(+), 49 deletions(-) diff --git a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift index b2e6881c..7d182c8f 100644 --- a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift +++ b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift @@ -30,44 +30,11 @@ final class PopupDetailRepository: PopupDetailRepositoryProtocol { async let information = fetchInformation(popupId: popupId, token: token) async let ratingDistribution = fetchRatingDistribution(popupId: popupId, token: token) - async let reviewList = fetchReviewList(popupId: popupId, token: token) + async let reviewList = fetchReviewList(popupId: popupId, page: 1) return try await (information, ratingDistribution, reviewList) } - 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( - 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 reviewList = response.reviews.map { $0.toEntity() } - completion(.success(PopupReviewList(reviews: reviewList))) - case .failure(let error): - completion(.failure(error)) - } - } - } - func togglePopupPick(popupId: Int, completion: @escaping (Result) -> Void) { // TODO: TokenRepository에서 access token 만료 시 자동으로 reissue 하는 로직 구현 후 리팩토링 guard let token = tokenRepository.fetchAccessToken() else { @@ -128,11 +95,19 @@ extension PopupDetailRepository { } } - func fetchReviewList(popupId: Int, token: String) async throws -> PopupReviewList { + func fetchReviewList(popupId: Int, page: Int) async throws -> PopupReviewList { + guard let token = tokenRepository.fetchAccessToken() else { + throw NSError( + domain: "PopupDetailRepository", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "액세스 토큰 만료"] + ) + } + let endpoint = Endpoint>( httpMethod: .get, path: APIConstant.popupReviewPath(popupId: String(popupId)), - queryItems: [URLQueryItem(name: "page", value: "1")], + queryItems: [URLQueryItem(name: "page", value: String(page))], headers: ["Authorization": "Bearer \(token)"] ) diff --git a/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift b/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift index 0cb11f7a..e788673c 100644 --- a/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift +++ b/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift @@ -12,7 +12,7 @@ protocol PopupDetailRepositoryProtocol { 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) // 리뷰 좋아요 토글, 리뷰 작성 추가 diff --git a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift index ebb22c03..b6293ad4 100644 --- a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift +++ b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift @@ -24,11 +24,9 @@ final class PopupDetailUseCase: PopupDetailUseCaseProtocol { 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) { diff --git a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift index f871c323..e4445565 100644 --- a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift +++ b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift @@ -10,7 +10,7 @@ protocol PopupDetailUseCaseProtocol { 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) diff --git a/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift b/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift index 9b44c4a9..2bf3c7e5 100644 --- a/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift +++ b/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift @@ -99,13 +99,17 @@ extension PopupDetailViewModel { func fetchPopupReview() { let popupId = dataSource.getPopupId() - useCase.fetchPopupReviews(popupId: popupId, page: reviewPage) { [weak self] result in - guard let self else { return } - switch result { - case .success(let popupReviewList): - self.dataSource.updateReviewData(popupReviewList) - case .failure: + 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?() From 56f67063ed45468885e9bbfa4364d1eb848d3bf6 Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Mon, 16 Jun 2025 02:35:05 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refactor:=20Swift=20Concurrency=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20-=20?= =?UTF-8?q?=EC=B0=9C=20=ED=86=A0=EA=B8=80=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainScene/PopupDetailRepository.swift | 18 +++++------------- .../PopupDetailRepositoryProtocol.swift | 2 +- .../Implementations/PopupDetailUseCase.swift | 14 ++++++++------ .../PopupDetailUseCaseProtocol.swift | 2 +- .../ViewModel/PopupDetailViewModel.swift | 18 ++++++++++-------- 5 files changed, 25 insertions(+), 29 deletions(-) diff --git a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift index 7d182c8f..9d4ad656 100644 --- a/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift +++ b/Popcorn-iOS/Source/Data/Repositories/MainScene/PopupDetailRepository.swift @@ -35,15 +35,14 @@ final class PopupDetailRepository: PopupDetailRepositoryProtocol { return try await (information, ratingDistribution, reviewList) } - func togglePopupPick(popupId: Int, completion: @escaping (Result) -> Void) { + func togglePopupPick(popupId: Int) async throws -> Bool { // TODO: TokenRepository에서 access token 만료 시 자동으로 reissue 하는 로직 구현 후 리팩토링 guard let token = tokenRepository.fetchAccessToken() else { - completion(.failure(NSError( + throw NSError( domain: "PopupDetailRepository", code: -1, userInfo: [NSLocalizedDescriptionKey: "액세스 토큰 만료"] - ))) - return + ) } let endpoint = Endpoint>( @@ -52,15 +51,8 @@ final class PopupDetailRepository: PopupDetailRepositoryProtocol { 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)) - } - } + let isPick = try await networkManager.request(endpoint: endpoint).data + return isPick } } diff --git a/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift b/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift index e788673c..4b864961 100644 --- a/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift +++ b/Popcorn-iOS/Source/Domain/Interfaces/Repositories/MainScene/PopupDetailRepositoryProtocol.swift @@ -14,6 +14,6 @@ protocol PopupDetailRepositoryProtocol { 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 b6293ad4..e891df67 100644 --- a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift +++ b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Implementations/PopupDetailUseCase.swift @@ -9,11 +9,11 @@ import Foundation final class PopupDetailUseCase: PopupDetailUseCaseProtocol { private let repository: PopupDetailRepositoryProtocol - + init(repository: PopupDetailRepositoryProtocol) { self.repository = repository } - + func fetchPopupAllData( for popupId: Int ) async throws -> (PopupInformation, PopupRatingDistribution, PopupReviewList) { @@ -21,18 +21,20 @@ final class PopupDetailUseCase: PopupDetailUseCaseProtocol { information.hashTags = extractHashTag(from: information) return (information, ratingDistribution, reviewList) } - + func fetchPopupReviews( popupId: Int, 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 e4445565..9e3c5393 100644 --- a/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift +++ b/Popcorn-iOS/Source/Domain/UseCases/MainScene/Interfaces/PopupDetailUseCaseProtocol.swift @@ -12,7 +12,7 @@ protocol PopupDetailUseCaseProtocol { 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] } diff --git a/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift b/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift index 2bf3c7e5..4e7d5fd3 100644 --- a/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift +++ b/Popcorn-iOS/Source/Presentation/MainScene/ViewModel/PopupDetailViewModel.swift @@ -45,15 +45,17 @@ final class PopupDetailViewModel: MainCarouselViewModelProtocol { // MARK: - Input extension PopupDetailViewModel { func didTapPickButton(for popupId: Int) { - useCase.togglePopupPick(popupId: popupId) { [weak self] result in - guard let self else { return } - switch result { - case .success(let isPick): - self.dataSource.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) + } } } } From 0da8f47650aa484b572159a235e64182cf887b10 Mon Sep 17 00:00:00 2001 From: MinwooJe Date: Wed, 18 Jun 2025 12:57:05 +0900 Subject: [PATCH 10/10] =?UTF-8?q?test:=20DummyPopupDetailRepository=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=86=A0=EC=BD=9C=20=EC=A4=80=EC=88=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dummy/DummyPopupDetailRepository.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) 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 } + }