From a53ff5cb82e711320a046602d8cd69dd07027b9b Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Thu, 13 Jun 2024 13:17:35 +0900 Subject: [PATCH 01/11] =?UTF-8?q?Feat:=20NetworkService=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/NetworkService.swift | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 HomeCafeRecipes/HomeCafeRecipes/Data/Network/NetworkService.swift diff --git a/HomeCafeRecipes/HomeCafeRecipes/Data/Network/NetworkService.swift b/HomeCafeRecipes/HomeCafeRecipes/Data/Network/NetworkService.swift new file mode 100644 index 0000000..cba7705 --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipes/Data/Network/NetworkService.swift @@ -0,0 +1,43 @@ +// +// NetworkService.swift +// HomeCafeRecipes +// +// Created by 김건호 on 6/12/24. +// + +import Foundation +import RxSwift + +protocol NetworkService { + func getRequest(url: URL, responseType: T.Type) -> Single +} + +class BaseNetworkService: NetworkService { + let baseURL = URL(string: "https://meog0.store/api")! + + func getRequest(url: URL, responseType: T.Type) -> Single { + var request = URLRequest(url: url) + request.httpMethod = "GET" + return Single.create { single in + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + single(.failure(error)) + } else if let data = data { + do { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let responseObject = try decoder.decode(T.self, from: data) + single(.success(responseObject)) + } catch let decodingError { + single(.failure(decodingError)) + } + } + } + task.resume() + + return Disposables.create { + task.cancel() + } + } + } +} From 9d8daeae30aaa14d576ab6a3efeee43b40502843 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Thu, 13 Jun 2024 13:18:01 +0900 Subject: [PATCH 02/11] =?UTF-8?q?Feat:=20RecipeFetchService=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/RecipeFetchService.swift | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift diff --git a/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift b/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift new file mode 100644 index 0000000..b150c59 --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift @@ -0,0 +1,50 @@ +// +// RecipeFetchService.swift +// HomeCafeRecipes +// +// Created by 김건호 on 6/10/24. +// + +import Foundation +import RxSwift + +protocol RecipeFetchService { + func fetchRecipes(pageNumber: Int) -> Single<[Recipe]> + func searchRecipes(title: String, pageNumber: Int) -> Single<[Recipe]> +} + +class DefaultRecipeFetchService: RecipeFetchService { + private let networkService: NetworkService + private let baseURL: URL + + init(networkService: NetworkService, baseURL: URL = URL(string: "https://meog0.store/api")!) { + self.networkService = networkService + self.baseURL = baseURL + } + + func fetchRecipes(pageNumber: Int) -> Single<[Recipe]> { + let url = baseURL.appendingPathComponent("recipes") + var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + urlComponents?.queryItems = [URLQueryItem(name: "pageNumber", value: String(pageNumber))] + guard let finalURL = urlComponents?.url else { + return Single.error(NSError(domain: "URLComponentsError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])) + } + return networkService.getRequest(url: finalURL, responseType: NetworkResponseDTO.self) + .map { responseDTO in + return responseDTO.data.recipes.map { $0.toDomain() } + } + } + + func searchRecipes(title: String, pageNumber: Int) -> Single<[Recipe]> { + let url = baseURL.appendingPathComponent("recipes") + var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) + urlComponents?.queryItems = [URLQueryItem(name: "keyword", value: title), URLQueryItem(name: "pageNumber", value: String(pageNumber))] + guard let finalURL = urlComponents?.url else { + return Single.error(NSError(domain: "URLComponentsError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])) + } + return networkService.getRequest(url: finalURL, responseType: NetworkResponseDTO.self) + .map { responseDTO in + return responseDTO.data.recipes.map { $0.toDomain() } + } + } +} From 257276e1b20b323c4494129bca6320a49865bfb4 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Thu, 13 Jun 2024 14:47:38 +0900 Subject: [PATCH 03/11] =?UTF-8?q?Feat:=20RecipeListView=EC=97=90=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=ED=98=95=ED=83=9C=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=ED=99=98=ED=95=98=EB=8A=94=20=EA=B0=9D=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/RecipeListItemViewModel.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 HomeCafeRecipes/HomeCafeRecipes/Presentation/FeedList/View/RecipeListItemViewModel.swift diff --git a/HomeCafeRecipes/HomeCafeRecipes/Presentation/FeedList/View/RecipeListItemViewModel.swift b/HomeCafeRecipes/HomeCafeRecipes/Presentation/FeedList/View/RecipeListItemViewModel.swift new file mode 100644 index 0000000..470924c --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipes/Presentation/FeedList/View/RecipeListItemViewModel.swift @@ -0,0 +1,20 @@ +// +// RecipeListItemViewModel.swift +// HomeCafeRecipes +// +// Created by 김건호 on 6/12/24. +// + +import Foundation + +struct RecipeListItemViewModel { + let id: Int + let name: String + let imageUrl: URL? + + init(recipe: Recipe) { + self.id = recipe.id + self.name = recipe.name + self.imageUrl = URL(string: recipe.imageUrls.first ?? "") + } +} From 69aa3a400b55f1413d36d738aa3157900acb3d26 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Thu, 13 Jun 2024 14:48:00 +0900 Subject: [PATCH 04/11] =?UTF-8?q?Feat:=20RecipeListViewModel=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/RecipeListViewModel.swift | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift diff --git a/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift b/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift new file mode 100644 index 0000000..80ddabd --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift @@ -0,0 +1,133 @@ +// +// RecipeListViewModel.swift +// HomeCafeRecipes +// +// Created by 김건호 on 6/10/24. +// + +import Foundation +import RxSwift + +protocol RecipeListViewModelDelegate: AnyObject { + func didFetchRecipes(_ recipes: [RecipeListItemViewModel]) + func didFailWithError(_ error: Error) +} + +protocol InputRecipeListViewModel { + func viewDidLoad() + func fetchNextPage() + func didSelectItem(id: Int) + func searchRecipes(with query: String) +} + +protocol OutputRecipeListViewModel { + var recipes: Observable<[RecipeListItemViewModel]> { get } + var error: Observable { get } +} + +class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { + + private let disposeBag = DisposeBag() + private let fetchFeedListUseCase: FetchFeedListUseCase + private let searchFeedListUseCase: SearchFeedListUseCase + private weak var delegate: RecipeListViewModelDelegate? + + private var currentPage: Int = 1 + private var isFetching = false + private var isSearching = false + private var currentSearchQuery: String? + + private let recipesSubject = BehaviorSubject<[RecipeListItemViewModel]>(value: []) + private let errorSubject = BehaviorSubject(value: nil) + + var recipes: Observable<[RecipeListItemViewModel]> { + return recipesSubject.asObservable() + } + + var error: Observable { + return errorSubject.asObservable() + } + + init(fetchFeedListUseCase: FetchFeedListUseCase, searchFeedListUseCase: SearchFeedListUseCase) { + self.fetchFeedListUseCase = fetchFeedListUseCase + self.searchFeedListUseCase = searchFeedListUseCase + } + + func setDelegate(_ delegate: RecipeListViewModelDelegate) { + self.delegate = delegate + bindOutputs() + } + + private func bindOutputs() { + recipes + .subscribe(onNext: { [weak self] recipes in + self?.delegate?.didFetchRecipes(recipes) + }) + .disposed(by: disposeBag) + + error + .subscribe(onNext: { [weak self] error in + if let error = error { + self?.delegate?.didFailWithError(error) + } + }) + .disposed(by: disposeBag) + } + + func viewDidLoad() { + fetchRecipes() + } + + func fetchNextPage() { + fetchRecipes() + } + + func didSelectItem(id: Int) { + print(id) + } + + func searchRecipes(with title: String) { + guard !isFetching else { return } + isFetching = true + currentSearchQuery = title + isSearching = true + currentPage = 1 + + searchFeedListUseCase.execute(title: title, pageNumber: currentPage) + .subscribe(onSuccess: handleSuccess, onFailure: handleError) + .disposed(by: disposeBag) + } + + private func fetchRecipes() { + guard !isFetching else { return } + isFetching = true + + fetchFeedListUseCase.execute(pageNumber: currentPage) + .subscribe(onSuccess: handleSuccess, onFailure: handleError) + .disposed(by: disposeBag) + } + + private func handleSuccess(result: Result<[Recipe], Error>) { + isFetching = false + switch result { + case .success(let recipes): + let recipeViewModels = recipes.map { RecipeListItemViewModel(recipe: $0) } + var currentRecipes = try! recipesSubject.value() + if isSearching { + currentRecipes = recipeViewModels + isSearching = false + } else { + currentRecipes.append(contentsOf: recipeViewModels) + } + recipesSubject.onNext(currentRecipes) + currentPage += 1 + case .failure(let error): + errorSubject.onNext(error) + } + } + + private func handleError(error: Error) { + isFetching = false + errorSubject.onNext(error) + } +} From 3a0873725f457034db9364b0a6bf6d1ef17e4ddb Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Thu, 13 Jun 2024 11:10:29 +0900 Subject: [PATCH 05/11] =?UTF-8?q?Feat:=20Kingfisher=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeCafeRecipes.xcodeproj/project.pbxproj | 10 ++++++++++ .../xcshareddata/swiftpm/Package.resolved | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj index 2773df9..1abeeb7 100644 --- a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj +++ b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ 1D2C16FD2BE532B800C04508 /* HomeCafeRecipesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16FC2BE532B800C04508 /* HomeCafeRecipesTests.swift */; }; 1D2C17072BE532B800C04508 /* HomeCafeRecipesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C17062BE532B800C04508 /* HomeCafeRecipesUITests.swift */; }; 1D2C17092BE532B800C04508 /* HomeCafeRecipesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C17082BE532B800C04508 /* HomeCafeRecipesUITestsLaunchTests.swift */; }; + 1D6C5ACF2C1A8C580052A36C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 1D6C5ACE2C1A8C580052A36C /* Kingfisher */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -342,6 +343,7 @@ mainGroup = 1D2C16D92BE532B700C04508; packageReferences = ( 1D740B3F2C15E1EC0001B704 /* XCRemoteSwiftPackageReference "RxSwift" */, + 1D6C5ACD2C1A8C580052A36C /* XCRemoteSwiftPackageReference "Kingfisher" */, ); productRefGroup = 1D2C16E32BE532B700C04508 /* Products */; projectDirPath = ""; @@ -739,6 +741,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 1D6C5ACD2C1A8C580052A36C /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/onevcat/Kingfisher.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.12.0; + }; + }; 1D740B3F2C15E1EC0001B704 /* XCRemoteSwiftPackageReference "RxSwift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ReactiveX/RxSwift"; diff --git a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 939be2d..de9a0ec 100644 --- a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "kingfisher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Kingfisher.git", + "state" : { + "revision" : "2ef543ee21d63734e1c004ad6c870255e8716c50", + "version" : "7.12.0" + } + }, { "identity" : "rxswift", "kind" : "remoteSourceControl", From aa20a4ba727f3e30dcf34e2d53703046c0aa0ff8 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Fri, 14 Jun 2024 15:37:06 +0900 Subject: [PATCH 06/11] =?UTF-8?q?Feat:=20RecipeMapper=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeCafeRecipes.xcodeproj/project.pbxproj | 199 ++++++++++++++---- .../Presentation/Mapper/RecipeMapper.swift | 18 ++ 2 files changed, 171 insertions(+), 46 deletions(-) create mode 100644 HomeCafeRecipes/HomeCafeRecipes/Presentation/Mapper/RecipeMapper.swift diff --git a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj index 1abeeb7..870e5ca 100644 --- a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj +++ b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj @@ -17,22 +17,32 @@ 1D1283B12C1697DB00C5A870 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 1D1283B02C1697DB00C5A870 /* RxSwift */; }; 1D1283B42C16983900C5A870 /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 1D1283B32C16983900C5A870 /* RxSwift */; }; 1D1283B62C16984E00C5A870 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 1D1283B52C16984E00C5A870 /* RxCocoa */; }; - 1D1283B82C169C0200C5A870 /* FeedListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283B72C169C0200C5A870 /* FeedListRepository.swift */; }; - 1D1283BA2C16A62800C5A870 /* SearchFeedListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283B92C16A62800C5A870 /* SearchFeedListRepository.swift */; }; - 1D1283C12C16B05800C5A870 /* RecipeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283C02C16B05800C5A870 /* RecipeDTO.swift */; }; - 1D1283C32C16B06F00C5A870 /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283C22C16B06F00C5A870 /* UserDTO.swift */; }; - 1D1283C52C16B07D00C5A870 /* CommentDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283C42C16B07D00C5A870 /* CommentDTO.swift */; }; 1D1283C82C16CE7C00C5A870 /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283C72C16CE7C00C5A870 /* DateFormatter+Extensions.swift */; }; 1D1283CA2C16D9C600C5A870 /* RecipeFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283C92C16D9C600C5A870 /* RecipeFetchService.swift */; }; 1D2C16E62BE532B700C04508 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16E52BE532B700C04508 /* AppDelegate.swift */; }; - 1D2C16E82BE532B700C04508 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16E72BE532B700C04508 /* SceneDelegate.swift */; }; 1D2C16EA2BE532B700C04508 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16E92BE532B700C04508 /* ViewController.swift */; }; - 1D2C16EF2BE532B800C04508 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1D2C16EE2BE532B800C04508 /* Assets.xcassets */; }; - 1D2C16F22BE532B800C04508 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1D2C16F02BE532B800C04508 /* LaunchScreen.storyboard */; }; 1D2C16FD2BE532B800C04508 /* HomeCafeRecipesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16FC2BE532B800C04508 /* HomeCafeRecipesTests.swift */; }; 1D2C17072BE532B800C04508 /* HomeCafeRecipesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C17062BE532B800C04508 /* HomeCafeRecipesUITests.swift */; }; 1D2C17092BE532B800C04508 /* HomeCafeRecipesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C17082BE532B800C04508 /* HomeCafeRecipesUITestsLaunchTests.swift */; }; - 1D6C5ACF2C1A8C580052A36C /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 1D6C5ACE2C1A8C580052A36C /* Kingfisher */; }; + 1D4741D12C1B4F8D009381CE /* RecipeImageDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741CC2C1B4F8D009381CE /* RecipeImageDTO.swift */; }; + 1D4741D22C1B4F8D009381CE /* RecipeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741CD2C1B4F8D009381CE /* RecipeDTO.swift */; }; + 1D4741D32C1B4F8D009381CE /* RecipePageDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741CE2C1B4F8D009381CE /* RecipePageDTO.swift */; }; + 1D4741D42C1B4F8D009381CE /* NetworkResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741CF2C1B4F8D009381CE /* NetworkResponseDTO.swift */; }; + 1D4741D52C1B4F8D009381CE /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741D02C1B4F8D009381CE /* UserDTO.swift */; }; + 1D4741D72C1B4FF4009381CE /* RecipeListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741D62C1B4FF4009381CE /* RecipeListViewModel.swift */; }; + 1DDFFD812C1C096A0083B077 /* RecipeMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDFFD802C1C096A0083B077 /* RecipeMapper.swift */; }; + 1DE19E9D2C1B3DC10031804A /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19E9C2C1B3DC10031804A /* SceneDelegate.swift */; }; + 1DE19EA72C1B420A0031804A /* FeedListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EA52C1B420A0031804A /* FeedListRepository.swift */; }; + 1DE19EA82C1B420A0031804A /* SearchFeedListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EA62C1B420A0031804A /* SearchFeedListRepository.swift */; }; + 1DE19EB12C1B42200031804A /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EB02C1B42200031804A /* NetworkService.swift */; }; + 1DE19EBF2C1B422F0031804A /* RecipeItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EB42C1B422F0031804A /* RecipeItemViewModel.swift */; }; + 1DE19EC02C1B422F0031804A /* RecipeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EB62C1B422F0031804A /* RecipeDetailView.swift */; }; + 1DE19EC22C1B422F0031804A /* RecipeListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EBA2C1B422F0031804A /* RecipeListItemViewModel.swift */; }; + 1DE19EC32C1B422F0031804A /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EBB2C1B422F0031804A /* SearchBar.swift */; }; + 1DE19EC42C1B422F0031804A /* RecipeListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EBC2C1B422F0031804A /* RecipeListViewController.swift */; }; + 1DE19EC52C1B422F0031804A /* RecipeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EBD2C1B422F0031804A /* RecipeListView.swift */; }; + 1DE19EC62C1B422F0031804A /* RecipeListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EBE2C1B422F0031804A /* RecipeListCell.swift */; }; + 1DE19EC82C1B4C2D0031804A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 1DE19EC72C1B4C2D0031804A /* Kingfisher */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -59,16 +69,10 @@ 1D1283A72C15EABB00C5A870 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; 1D1283A92C15EBCF00C5A870 /* SearchFeedUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedUseCase.swift; sourceTree = ""; }; 1D1283AB2C15EBE600C5A870 /* FetchFeedListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFeedListUseCase.swift; sourceTree = ""; }; - 1D1283B72C169C0200C5A870 /* FeedListRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListRepository.swift; sourceTree = ""; }; - 1D1283B92C16A62800C5A870 /* SearchFeedListRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedListRepository.swift; sourceTree = ""; }; - 1D1283C02C16B05800C5A870 /* RecipeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDTO.swift; sourceTree = ""; }; - 1D1283C22C16B06F00C5A870 /* UserDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDTO.swift; sourceTree = ""; }; - 1D1283C42C16B07D00C5A870 /* CommentDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentDTO.swift; sourceTree = ""; }; 1D1283C72C16CE7C00C5A870 /* DateFormatter+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extensions.swift"; sourceTree = ""; }; 1D1283C92C16D9C600C5A870 /* RecipeFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeFetchService.swift; sourceTree = ""; }; 1D2C16E22BE532B700C04508 /* HomeCafeRecipes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HomeCafeRecipes.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1D2C16E52BE532B700C04508 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 1D2C16E72BE532B700C04508 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 1D2C16E92BE532B700C04508 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 1D2C16EE2BE532B800C04508 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1D2C16F12BE532B800C04508 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -78,6 +82,24 @@ 1D2C17022BE532B800C04508 /* HomeCafeRecipesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HomeCafeRecipesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1D2C17062BE532B800C04508 /* HomeCafeRecipesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCafeRecipesUITests.swift; sourceTree = ""; }; 1D2C17082BE532B800C04508 /* HomeCafeRecipesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCafeRecipesUITestsLaunchTests.swift; sourceTree = ""; }; + 1D4741CC2C1B4F8D009381CE /* RecipeImageDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeImageDTO.swift; sourceTree = ""; }; + 1D4741CD2C1B4F8D009381CE /* RecipeDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeDTO.swift; sourceTree = ""; }; + 1D4741CE2C1B4F8D009381CE /* RecipePageDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipePageDTO.swift; sourceTree = ""; }; + 1D4741CF2C1B4F8D009381CE /* NetworkResponseDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkResponseDTO.swift; sourceTree = ""; }; + 1D4741D02C1B4F8D009381CE /* UserDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDTO.swift; sourceTree = ""; }; + 1D4741D62C1B4FF4009381CE /* RecipeListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeListViewModel.swift; sourceTree = ""; }; + 1DDFFD802C1C096A0083B077 /* RecipeMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeMapper.swift; sourceTree = ""; }; + 1DE19E9C2C1B3DC10031804A /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 1DE19EA52C1B420A0031804A /* FeedListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedListRepository.swift; sourceTree = ""; }; + 1DE19EA62C1B420A0031804A /* SearchFeedListRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchFeedListRepository.swift; sourceTree = ""; }; + 1DE19EB02C1B42200031804A /* NetworkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; + 1DE19EB42C1B422F0031804A /* RecipeItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeItemViewModel.swift; sourceTree = ""; }; + 1DE19EB62C1B422F0031804A /* RecipeDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeDetailView.swift; sourceTree = ""; }; + 1DE19EBA2C1B422F0031804A /* RecipeListItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeListItemViewModel.swift; sourceTree = ""; }; + 1DE19EBB2C1B422F0031804A /* SearchBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; + 1DE19EBC2C1B422F0031804A /* RecipeListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeListViewController.swift; sourceTree = ""; }; + 1DE19EBD2C1B422F0031804A /* RecipeListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeListView.swift; sourceTree = ""; }; + 1DE19EBE2C1B422F0031804A /* RecipeListCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipeListCell.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +107,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1DE19EC82C1B4C2D0031804A /* Kingfisher in Frameworks */, 1D1283B12C1697DB00C5A870 /* RxSwift in Frameworks */, 1D1283AF2C1697DB00C5A870 /* RxCocoa in Frameworks */, ); @@ -132,8 +155,8 @@ 1D1283AD2C16974B00C5A870 /* Data */ = { isa = PBXGroup; children = ( + 1DE19EA42C1B420A0031804A /* Repositories */, 1D1283BC2C16AA8100C5A870 /* Network */, - 1D1283BB2C16AA6400C5A870 /* Repositories */, ); path = Data; sourceTree = ""; @@ -145,35 +168,16 @@ name = Frameworks; sourceTree = ""; }; - 1D1283BB2C16AA6400C5A870 /* Repositories */ = { - isa = PBXGroup; - children = ( - 1D1283B72C169C0200C5A870 /* FeedListRepository.swift */, - 1D1283B92C16A62800C5A870 /* SearchFeedListRepository.swift */, - ); - name = Repositories; - path = "ㄲ데ㅐ냐새갿ㄴ"; - sourceTree = ""; - }; 1D1283BC2C16AA8100C5A870 /* Network */ = { isa = PBXGroup; children = ( - 1D1283BF2C16B02600C5A870 /* DTO */, + 1D4741CB2C1B4F8D009381CE /* DTO */, 1D1283C92C16D9C600C5A870 /* RecipeFetchService.swift */, + 1DE19EB02C1B42200031804A /* NetworkService.swift */, ); path = Network; sourceTree = ""; }; - 1D1283BF2C16B02600C5A870 /* DTO */ = { - isa = PBXGroup; - children = ( - 1D1283C02C16B05800C5A870 /* RecipeDTO.swift */, - 1D1283C22C16B06F00C5A870 /* UserDTO.swift */, - 1D1283C42C16B07D00C5A870 /* CommentDTO.swift */, - ); - path = DTO; - sourceTree = ""; - }; 1D1283C62C16CD9200C5A870 /* Utilities */ = { isa = PBXGroup; children = ( @@ -192,6 +196,7 @@ 1D1283B22C16983900C5A870 /* Frameworks */, ); sourceTree = ""; + wrapsLines = 1; }; 1D2C16E32BE532B700C04508 /* Products */ = { isa = PBXGroup; @@ -206,11 +211,12 @@ 1D2C16E42BE532B700C04508 /* HomeCafeRecipes */ = { isa = PBXGroup; children = ( + 1DE19EB22C1B422F0031804A /* Presentation */, 1D1283C62C16CD9200C5A870 /* Utilities */, 1D1283AD2C16974B00C5A870 /* Data */, 1D740B402C15E6680001B704 /* Domain */, 1D2C16E52BE532B700C04508 /* AppDelegate.swift */, - 1D2C16E72BE532B700C04508 /* SceneDelegate.swift */, + 1DE19E9C2C1B3DC10031804A /* SceneDelegate.swift */, 1D2C16E92BE532B700C04508 /* ViewController.swift */, 1D2C16EE2BE532B800C04508 /* Assets.xcassets */, 1D2C16F02BE532B800C04508 /* LaunchScreen.storyboard */, @@ -236,15 +242,100 @@ path = HomeCafeRecipesUITests; sourceTree = ""; }; + 1D4741CB2C1B4F8D009381CE /* DTO */ = { + isa = PBXGroup; + children = ( + 1D4741CC2C1B4F8D009381CE /* RecipeImageDTO.swift */, + 1D4741CD2C1B4F8D009381CE /* RecipeDTO.swift */, + 1D4741CE2C1B4F8D009381CE /* RecipePageDTO.swift */, + 1D4741CF2C1B4F8D009381CE /* NetworkResponseDTO.swift */, + 1D4741D02C1B4F8D009381CE /* UserDTO.swift */, + ); + path = DTO; + sourceTree = ""; + }; 1D740B402C15E6680001B704 /* Domain */ = { isa = PBXGroup; children = ( + 1DE19EA12C1B41FE0031804A /* ViewModel */, 1D1283A02C15E92C00C5A870 /* UseCases */, 1D12839F2C15E7A700C5A870 /* Entities */, ); path = Domain; sourceTree = ""; }; + 1DDFFD822C1C09AB0083B077 /* Mapper */ = { + isa = PBXGroup; + children = ( + 1DDFFD802C1C096A0083B077 /* RecipeMapper.swift */, + ); + path = Mapper; + sourceTree = ""; + }; + 1DE19EA12C1B41FE0031804A /* ViewModel */ = { + isa = PBXGroup; + children = ( + 1D4741D62C1B4FF4009381CE /* RecipeListViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 1DE19EA42C1B420A0031804A /* Repositories */ = { + isa = PBXGroup; + children = ( + 1DE19EA52C1B420A0031804A /* FeedListRepository.swift */, + 1DE19EA62C1B420A0031804A /* SearchFeedListRepository.swift */, + ); + path = Repositories; + sourceTree = ""; + }; + 1DE19EB22C1B422F0031804A /* Presentation */ = { + isa = PBXGroup; + children = ( + 1DDFFD822C1C09AB0083B077 /* Mapper */, + 1DE19EB32C1B422F0031804A /* Feed */, + 1DE19EB72C1B422F0031804A /* FeedList */, + ); + path = Presentation; + sourceTree = ""; + }; + 1DE19EB32C1B422F0031804A /* Feed */ = { + isa = PBXGroup; + children = ( + 1DE19EB42C1B422F0031804A /* RecipeItemViewModel.swift */, + 1DE19EB52C1B422F0031804A /* View */, + ); + path = Feed; + sourceTree = ""; + }; + 1DE19EB52C1B422F0031804A /* View */ = { + isa = PBXGroup; + children = ( + 1DE19EB62C1B422F0031804A /* RecipeDetailView.swift */, + ); + path = View; + sourceTree = ""; + }; + 1DE19EB72C1B422F0031804A /* FeedList */ = { + isa = PBXGroup; + children = ( + 1DE19EB92C1B422F0031804A /* View */, + ); + path = FeedList; + sourceTree = ""; + }; + 1DE19EB92C1B422F0031804A /* View */ = { + isa = PBXGroup; + children = ( + 1DE19EBA2C1B422F0031804A /* RecipeListItemViewModel.swift */, + 1DE19EBB2C1B422F0031804A /* SearchBar.swift */, + 1DE19EBC2C1B422F0031804A /* RecipeListViewController.swift */, + 1DE19EBD2C1B422F0031804A /* RecipeListView.swift */, + 1DE19EBE2C1B422F0031804A /* RecipeListCell.swift */, + ); + path = View; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -264,6 +355,7 @@ packageProductDependencies = ( 1D1283AE2C1697DB00C5A870 /* RxCocoa */, 1D1283B02C1697DB00C5A870 /* RxSwift */, + 1DE19EC72C1B4C2D0031804A /* Kingfisher */, ); productName = HomeCafeRecipes; productReference = 1D2C16E22BE532B700C04508 /* HomeCafeRecipes.app */; @@ -361,8 +453,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1D2C16F22BE532B800C04508 /* LaunchScreen.storyboard in Resources */, - 1D2C16EF2BE532B800C04508 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -388,21 +478,33 @@ buildActionMask = 2147483647; files = ( 1D2C16EA2BE532B700C04508 /* ViewController.swift in Sources */, + 1DE19EC52C1B422F0031804A /* RecipeListView.swift in Sources */, + 1D4741D32C1B4F8D009381CE /* RecipePageDTO.swift in Sources */, 1D2C16E62BE532B700C04508 /* AppDelegate.swift in Sources */, + 1DE19EB12C1B42200031804A /* NetworkService.swift in Sources */, 1D1283A62C15EAA600C5A870 /* User.swift in Sources */, - 1D1283C52C16B07D00C5A870 /* CommentDTO.swift in Sources */, 1D1283AC2C15EBE600C5A870 /* FetchFeedListUseCase.swift in Sources */, 1D1283A82C15EABB00C5A870 /* Comment.swift in Sources */, 1D1283C82C16CE7C00C5A870 /* DateFormatter+Extensions.swift in Sources */, - 1D1283B82C169C0200C5A870 /* FeedListRepository.swift in Sources */, 1D1283A42C15EA8100C5A870 /* RecipeType.swift in Sources */, + 1D4741D22C1B4F8D009381CE /* RecipeDTO.swift in Sources */, + 1DE19EC02C1B422F0031804A /* RecipeDetailView.swift in Sources */, 1D1283AA2C15EBCF00C5A870 /* SearchFeedUseCase.swift in Sources */, - 1D2C16E82BE532B700C04508 /* SceneDelegate.swift in Sources */, - 1D1283BA2C16A62800C5A870 /* SearchFeedListRepository.swift in Sources */, + 1DE19EA82C1B420A0031804A /* SearchFeedListRepository.swift in Sources */, + 1D4741D52C1B4F8D009381CE /* UserDTO.swift in Sources */, + 1DE19EC22C1B422F0031804A /* RecipeListItemViewModel.swift in Sources */, + 1DE19EC32C1B422F0031804A /* SearchBar.swift in Sources */, + 1D4741D72C1B4FF4009381CE /* RecipeListViewModel.swift in Sources */, + 1DE19E9D2C1B3DC10031804A /* SceneDelegate.swift in Sources */, + 1D4741D12C1B4F8D009381CE /* RecipeImageDTO.swift in Sources */, + 1DE19EA72C1B420A0031804A /* FeedListRepository.swift in Sources */, + 1DE19EC62C1B422F0031804A /* RecipeListCell.swift in Sources */, + 1DDFFD812C1C096A0083B077 /* RecipeMapper.swift in Sources */, + 1DE19EC42C1B422F0031804A /* RecipeListViewController.swift in Sources */, + 1DE19EBF2C1B422F0031804A /* RecipeItemViewModel.swift in Sources */, 1D1283A22C15E94300C5A870 /* Recipe.swift in Sources */, 1D1283CA2C16D9C600C5A870 /* RecipeFetchService.swift in Sources */, - 1D1283C32C16B06F00C5A870 /* UserDTO.swift in Sources */, - 1D1283C12C16B05800C5A870 /* RecipeDTO.swift in Sources */, + 1D4741D42C1B4F8D009381CE /* NetworkResponseDTO.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -780,6 +882,11 @@ package = 1D740B3F2C15E1EC0001B704 /* XCRemoteSwiftPackageReference "RxSwift" */; productName = RxCocoa; }; + 1DE19EC72C1B4C2D0031804A /* Kingfisher */ = { + isa = XCSwiftPackageProductDependency; + package = 1D6C5ACD2C1A8C580052A36C /* XCRemoteSwiftPackageReference "Kingfisher" */; + productName = Kingfisher; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 1D2C16DA2BE532B700C04508 /* Project object */; diff --git a/HomeCafeRecipes/HomeCafeRecipes/Presentation/Mapper/RecipeMapper.swift b/HomeCafeRecipes/HomeCafeRecipes/Presentation/Mapper/RecipeMapper.swift new file mode 100644 index 0000000..f282bf5 --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipes/Presentation/Mapper/RecipeMapper.swift @@ -0,0 +1,18 @@ +// +// RecipeMapper.swift +// HomeCafeRecipes +// +// Created by 김건호 on 6/14/24. +// + +import Foundation + +struct RecipeMapper { + static func mapToRecipeListItemViewModels(from recipes: [Recipe]) -> [RecipeListItemViewModel] { + return recipes.map { RecipeListItemViewModel(recipe: $0) } + } + + static func mapToRecipeItemViewModel(from recipe: Recipe) -> RecipeItemViewModel { + return RecipeItemViewModel(recipe: recipe) + } +} From 954200d19a79b69c5a2c324b85dbb913028a23b0 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Fri, 14 Jun 2024 16:59:59 +0900 Subject: [PATCH 07/11] =?UTF-8?q?Fix:=20=EA=B2=80=EC=83=89=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20Mapper=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/RecipeListViewModel.swift | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) mode change 100644 => 100755 HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift diff --git a/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift b/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift old mode 100644 new mode 100755 index 80ddabd..c792344 --- a/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift +++ b/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift @@ -16,8 +16,9 @@ protocol RecipeListViewModelDelegate: AnyObject { protocol InputRecipeListViewModel { func viewDidLoad() func fetchNextPage() - func didSelectItem(id: Int) + func didSelectItem(id: Int) -> RecipeItemViewModel? func searchRecipes(with query: String) + func resetSearch() } protocol OutputRecipeListViewModel { @@ -26,7 +27,7 @@ protocol OutputRecipeListViewModel { } class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { - + private let disposeBag = DisposeBag() private let fetchFeedListUseCase: FetchFeedListUseCase private let searchFeedListUseCase: SearchFeedListUseCase @@ -36,7 +37,8 @@ class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { private var isFetching = false private var isSearching = false private var currentSearchQuery: String? - + private var allRecipes: [Recipe] = [] + private let recipesSubject = BehaviorSubject<[RecipeListItemViewModel]>(value: []) private let errorSubject = BehaviorSubject(value: nil) @@ -82,8 +84,19 @@ class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { fetchRecipes() } - func didSelectItem(id: Int) { - print(id) + func didSelectItem(id: Int) -> RecipeItemViewModel? { + guard let recipe = allRecipes.first(where: { $0.id == id }) else { + return nil + } + return RecipeMapper.mapToRecipeItemViewModel(from: recipe) + } + + func resetSearch() { + isSearching = false + currentSearchQuery = nil + currentPage = 1 + recipesSubject.onNext([]) + fetchRecipes() } func searchRecipes(with title: String) { @@ -92,7 +105,6 @@ class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { currentSearchQuery = title isSearching = true currentPage = 1 - searchFeedListUseCase.execute(title: title, pageNumber: currentPage) .subscribe(onSuccess: handleSuccess, onFailure: handleError) .disposed(by: disposeBag) @@ -101,7 +113,6 @@ class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { private func fetchRecipes() { guard !isFetching else { return } isFetching = true - fetchFeedListUseCase.execute(pageNumber: currentPage) .subscribe(onSuccess: handleSuccess, onFailure: handleError) .disposed(by: disposeBag) @@ -111,7 +122,12 @@ class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { isFetching = false switch result { case .success(let recipes): - let recipeViewModels = recipes.map { RecipeListItemViewModel(recipe: $0) } + if currentPage == 1 { + allRecipes = recipes + } else { + allRecipes.append(contentsOf: recipes) + } + let recipeViewModels = RecipeMapper.mapToRecipeListItemViewModels(from: recipes) var currentRecipes = try! recipesSubject.value() if isSearching { currentRecipes = recipeViewModels From e96130867743682b5589145545499821935ad1e6 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Mon, 17 Jun 2024 11:12:10 +0900 Subject: [PATCH 08/11] =?UTF-8?q?Fix:=20didFail,=20URL=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Domain/ViewModel/RecipeListViewModel.swift | 4 ++-- .../Presentation/FeedList/View/RecipeListItemViewModel.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift b/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift index c792344..5c709b5 100755 --- a/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift +++ b/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift @@ -10,7 +10,7 @@ import RxSwift protocol RecipeListViewModelDelegate: AnyObject { func didFetchRecipes(_ recipes: [RecipeListItemViewModel]) - func didFailWithError(_ error: Error) + func didFail(with error: Error) } protocol InputRecipeListViewModel { @@ -70,7 +70,7 @@ class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { error .subscribe(onNext: { [weak self] error in if let error = error { - self?.delegate?.didFailWithError(error) + self?.delegate?.didFail(with: error) } }) .disposed(by: disposeBag) diff --git a/HomeCafeRecipes/HomeCafeRecipes/Presentation/FeedList/View/RecipeListItemViewModel.swift b/HomeCafeRecipes/HomeCafeRecipes/Presentation/FeedList/View/RecipeListItemViewModel.swift index 470924c..5614cca 100644 --- a/HomeCafeRecipes/HomeCafeRecipes/Presentation/FeedList/View/RecipeListItemViewModel.swift +++ b/HomeCafeRecipes/HomeCafeRecipes/Presentation/FeedList/View/RecipeListItemViewModel.swift @@ -10,11 +10,11 @@ import Foundation struct RecipeListItemViewModel { let id: Int let name: String - let imageUrl: URL? + let imageURL: URL? init(recipe: Recipe) { self.id = recipe.id self.name = recipe.name - self.imageUrl = URL(string: recipe.imageUrls.first ?? "") + self.imageURL = URL(string: recipe.imageUrls.first ?? "") } } From ded9099ba555dc361b5c1265e60737573c9ecfd3 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Mon, 17 Jun 2024 13:31:17 +0900 Subject: [PATCH 09/11] =?UTF-8?q?FIx:=20baseURL=20constant=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=EB=93=A4=EA=B3=A0=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD,=20url=EC=9D=84=20=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=94=B0=EB=A1=9C=20=EB=B9=BC=20buildURL=EC=9D=B4=EB=9D=BC?= =?UTF-8?q?=EB=8A=94=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/Network/RecipeFetchService.swift | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift b/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift index b150c59..e2c90ef 100644 --- a/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift +++ b/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift @@ -15,36 +15,36 @@ protocol RecipeFetchService { class DefaultRecipeFetchService: RecipeFetchService { private let networkService: NetworkService - private let baseURL: URL + private static let baseURL: URL = URL(string: "https://meog0.store/api")! - init(networkService: NetworkService, baseURL: URL = URL(string: "https://meog0.store/api")!) { + init(networkService: NetworkService) { self.networkService = networkService - self.baseURL = baseURL + } + + private func buildURL(endpoint: String, queryItems: [URLQueryItem]) -> URL? { + let URL = DefaultRecipeFetchService.baseURL.appendingPathComponent(endpoint) + var URLComponents = URLComponents(url: URL, resolvingAgainstBaseURL: false) + URLComponents?.queryItems = queryItems + return URLComponents?.url } func fetchRecipes(pageNumber: Int) -> Single<[Recipe]> { - let url = baseURL.appendingPathComponent("recipes") - var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) - urlComponents?.queryItems = [URLQueryItem(name: "pageNumber", value: String(pageNumber))] - guard let finalURL = urlComponents?.url else { + guard let URL = buildURL(endpoint: "recipes", queryItems: [URLQueryItem(name: "pageNumber", value: String(pageNumber))]) else { return Single.error(NSError(domain: "URLComponentsError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])) } - return networkService.getRequest(url: finalURL, responseType: NetworkResponseDTO.self) - .map { responseDTO in - return responseDTO.data.recipes.map { $0.toDomain() } - } + return networkService.getRequest(url: URL, responseType: NetworkResponseDTO.self) + .map { $0.data.recipes.map{ $0.toDomain() } } } + func searchRecipes(title: String, pageNumber: Int) -> Single<[Recipe]> { - let url = baseURL.appendingPathComponent("recipes") - var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) - urlComponents?.queryItems = [URLQueryItem(name: "keyword", value: title), URLQueryItem(name: "pageNumber", value: String(pageNumber))] - guard let finalURL = urlComponents?.url else { + guard let URL = buildURL(endpoint: "recipes", queryItems: [ + URLQueryItem(name: "keyword", value: title), + URLQueryItem(name: "pageNumber", value: String(pageNumber)) + ]) else { return Single.error(NSError(domain: "URLComponentsError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])) } - return networkService.getRequest(url: finalURL, responseType: NetworkResponseDTO.self) - .map { responseDTO in - return responseDTO.data.recipes.map { $0.toDomain() } - } + return networkService.getRequest(url: URL, responseType: NetworkResponseDTO.self) + .map { $0.data.recipes.map{ $0.toDomain() } } } } From dcc34a5689a00c4e8a848f0842c4229bac437ea1 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Mon, 17 Jun 2024 13:37:07 +0900 Subject: [PATCH 10/11] =?UTF-8?q?Fix:=20fetchNextRecipes=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=83=9D=EC=84=B1,=20recipes=EA=B0=80=20e?= =?UTF-8?q?mpty=EB=A9=B4=20return=EB=90=98=EA=B2=8C=20=ED=95=98=EC=97=AC?= =?UTF-8?q?=20=EB=8D=94=EC=9D=B4=EC=83=81=20=EC=83=88=EB=A1=9C=EC=9A=B4=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EA=B0=80=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Domain/ViewModel/RecipeListViewModel.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift b/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift index 5c709b5..d2d0ee0 100755 --- a/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift +++ b/HomeCafeRecipes/HomeCafeRecipes/Domain/ViewModel/RecipeListViewModel.swift @@ -79,9 +79,9 @@ class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { func viewDidLoad() { fetchRecipes() } - + func fetchNextPage() { - fetchRecipes() + fetchNextRecipes(nextPage: currentPage) } func didSelectItem(id: Int) -> RecipeItemViewModel? { @@ -117,11 +117,22 @@ class RecipeListViewModel: InputRecipeListViewModel, OutputRecipeListViewModel { .subscribe(onSuccess: handleSuccess, onFailure: handleError) .disposed(by: disposeBag) } + + private func fetchNextRecipes(nextPage: Int){ + guard !isFetching else { return } + isFetching = true + fetchFeedListUseCase.execute(pageNumber: nextPage) + .subscribe(onSuccess: handleSuccess, onFailure: handleError) + .disposed(by: disposeBag) + } private func handleSuccess(result: Result<[Recipe], Error>) { isFetching = false switch result { case .success(let recipes): + if recipes.isEmpty { + return + } if currentPage == 1 { allRecipes = recipes } else { From f971668d62a45c034b1cbd94ab64d3d9e1d1b9a8 Mon Sep 17 00:00:00 2001 From: GeonH0 Date: Tue, 18 Jun 2024 00:06:09 +0900 Subject: [PATCH 11/11] =?UTF-8?q?Fix:=20buildURL=EC=9D=84=20makeURL?= =?UTF-8?q?=EB=A1=9C=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HomeCafeRecipes/Data/Network/RecipeFetchService.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift b/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift index e2c90ef..135c541 100644 --- a/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift +++ b/HomeCafeRecipes/HomeCafeRecipes/Data/Network/RecipeFetchService.swift @@ -21,7 +21,7 @@ class DefaultRecipeFetchService: RecipeFetchService { self.networkService = networkService } - private func buildURL(endpoint: String, queryItems: [URLQueryItem]) -> URL? { + private func makeURL(endpoint: String, queryItems: [URLQueryItem]) -> URL? { let URL = DefaultRecipeFetchService.baseURL.appendingPathComponent(endpoint) var URLComponents = URLComponents(url: URL, resolvingAgainstBaseURL: false) URLComponents?.queryItems = queryItems @@ -29,7 +29,7 @@ class DefaultRecipeFetchService: RecipeFetchService { } func fetchRecipes(pageNumber: Int) -> Single<[Recipe]> { - guard let URL = buildURL(endpoint: "recipes", queryItems: [URLQueryItem(name: "pageNumber", value: String(pageNumber))]) else { + guard let URL = makeURL(endpoint: "recipes", queryItems: [URLQueryItem(name: "pageNumber", value: String(pageNumber))]) else { return Single.error(NSError(domain: "URLComponentsError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid URL"])) } return networkService.getRequest(url: URL, responseType: NetworkResponseDTO.self) @@ -38,7 +38,7 @@ class DefaultRecipeFetchService: RecipeFetchService { func searchRecipes(title: String, pageNumber: Int) -> Single<[Recipe]> { - guard let URL = buildURL(endpoint: "recipes", queryItems: [ + guard let URL = makeURL(endpoint: "recipes", queryItems: [ URLQueryItem(name: "keyword", value: title), URLQueryItem(name: "pageNumber", value: String(pageNumber)) ]) else {