diff --git a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj index 7ef56b6..939385c 100644 --- a/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj +++ b/HomeCafeRecipes/HomeCafeRecipes.xcodeproj/project.pbxproj @@ -18,16 +18,6 @@ 1D1283CA2C16D9C600C5A870 /* RecipeFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283C92C16D9C600C5A870 /* RecipeFetchService.swift */; }; 1D166D0D2C4AD54E00A50963 /* AddRecipeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166D0C2C4AD54D00A50963 /* AddRecipeViewModel.swift */; }; 1D166D0E2C4AD54E00A50963 /* AddRecipeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166D0C2C4AD54D00A50963 /* AddRecipeViewModel.swift */; }; - 1D166DE82C508C7F00A50963 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DE62C508C7E00A50963 /* Comment.swift */; }; - 1D166DE92C508C7F00A50963 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DE62C508C7E00A50963 /* Comment.swift */; }; - 1D166DEA2C508C7F00A50963 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DE72C508C7F00A50963 /* User.swift */; }; - 1D166DEB2C508C7F00A50963 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DE72C508C7F00A50963 /* User.swift */; }; - 1D166DF02C508CA500A50963 /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DEF2C508CA500A50963 /* UserDTO.swift */; }; - 1D166DF12C508CA500A50963 /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DEF2C508CA500A50963 /* UserDTO.swift */; }; - 1D166DF32C508CAB00A50963 /* ErrorResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DF22C508CAB00A50963 /* ErrorResponseDTO.swift */; }; - 1D166DF42C508CAB00A50963 /* ErrorResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DF22C508CAB00A50963 /* ErrorResponseDTO.swift */; }; - 1D166DF72C508CB700A50963 /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DF62C508CB700A50963 /* DateFormatter+Extensions.swift */; }; - 1D166DF82C508CB700A50963 /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D166DF62C508CB700A50963 /* DateFormatter+Extensions.swift */; }; 1D2C16E62BE532B700C04508 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16E52BE532B700C04508 /* AppDelegate.swift */; }; 1D2C16EA2BE532B700C04508 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16E92BE532B700C04508 /* ViewController.swift */; }; 1D2C16FD2BE532B800C04508 /* HomeCafeRecipesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D2C16FC2BE532B800C04508 /* HomeCafeRecipesTests.swift */; }; @@ -53,6 +43,30 @@ 1D5AEE532C592A9900BBD5F0 /* RecipeListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE522C592A9900BBD5F0 /* RecipeListItemViewModel.swift */; }; 1D5AEE552C592F2600BBD5F0 /* AddRecipeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE542C592F2600BBD5F0 /* AddRecipeUseCase.swift */; }; 1D5AEE562C592F2600BBD5F0 /* AddRecipeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE542C592F2600BBD5F0 /* AddRecipeUseCase.swift */; }; + 1D5AEE762C61095500BBD5F0 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE742C61095500BBD5F0 /* User.swift */; }; + 1D5AEE772C61095500BBD5F0 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE742C61095500BBD5F0 /* User.swift */; }; + 1D5AEE782C61095500BBD5F0 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE752C61095500BBD5F0 /* Comment.swift */; }; + 1D5AEE792C61095500BBD5F0 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE752C61095500BBD5F0 /* Comment.swift */; }; + 1D5AEE7B2C61096A00BBD5F0 /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE7A2C61096A00BBD5F0 /* UserDTO.swift */; }; + 1D5AEE7C2C61096A00BBD5F0 /* UserDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE7A2C61096A00BBD5F0 /* UserDTO.swift */; }; + 1D5AEE812C61099900BBD5F0 /* RecipeListInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE802C61099900BBD5F0 /* RecipeListInteractorTests.swift */; }; + 1D5AEF272C646E5B00BBD5F0 /* FetchFeedListUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283AB2C15EBE600C5A870 /* FetchFeedListUseCase.swift */; }; + 1D5AEF282C646FE600BBD5F0 /* RecipeListInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741D62C1B4FF4009381CE /* RecipeListInteractor.swift */; }; + 1D5AEF2A2C64730A00BBD5F0 /* FetchFeedListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEF292C64730A00BBD5F0 /* FetchFeedListUseCaseTests.swift */; }; + 1D5AEF2B2C64753900BBD5F0 /* FeedListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EA52C1B420A0031804A /* FeedListRepository.swift */; }; + 1D5AEF2E2C64786F00BBD5F0 /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEF2D2C64786F00BBD5F0 /* DateFormatter+Extensions.swift */; }; + 1D5AEF2F2C64786F00BBD5F0 /* DateFormatter+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEF2D2C64786F00BBD5F0 /* DateFormatter+Extensions.swift */; }; + 1D5AEF302C6478D800BBD5F0 /* RecipeFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283C92C16D9C600C5A870 /* RecipeFetchService.swift */; }; + 1D5AEF312C6478E100BBD5F0 /* AddRecipeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7368732C32CF09000EF904 /* AddRecipeRepository.swift */; }; + 1D5AEF322C6478FE00BBD5F0 /* RecipePostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7368772C32E7FE000EF904 /* RecipePostService.swift */; }; + 1D5AEF332C64790200BBD5F0 /* RecipeUploadDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7368792C32EB18000EF904 /* RecipeUploadDTO.swift */; }; + 1D5AEF342C64790700BBD5F0 /* String+Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF829B82C2A818D00C337FC /* String+Validation.swift */; }; + 1D5AEF352C64790C00BBD5F0 /* SearchFeedUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1283A92C15EBCF00C5A870 /* SearchFeedUseCase.swift */; }; + 1D5AEF362C64791300BBD5F0 /* RecipePageDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741CE2C1B4F8D009381CE /* RecipePageDTO.swift */; }; + 1D5AEF372C64791E00BBD5F0 /* RecipeUploadResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7368852C33D7BE000EF904 /* RecipeUploadResponseDTO.swift */; }; + 1D5AEF382C64794A00BBD5F0 /* CGSize+addButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D5AEE502C592A8000BBD5F0 /* CGSize+addButton.swift */; }; + 1D5AEF392C64795300BBD5F0 /* RecipeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4741CD2C1B4F8D009381CE /* RecipeDTO.swift */; }; + 1D5AEF3A2C64795900BBD5F0 /* SearchFeedListRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE19EA62C1B420A0031804A /* SearchFeedListRepository.swift */; }; 1D60CC3D2C3E4F1600D08FA3 /* APIConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D60CC3C2C3E4F1600D08FA3 /* APIConfig.swift */; }; 1D60CC402C3EB76600D08FA3 /* APIConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D60CC3C2C3E4F1600D08FA3 /* APIConfig.swift */; }; 1D6958D22C3D0553008604B3 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6958D12C3D0553008604B3 /* Router.swift */; }; @@ -74,6 +88,8 @@ 1D7368782C32E7FE000EF904 /* RecipePostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7368772C32E7FE000EF904 /* RecipePostService.swift */; }; 1D73687A2C32EB18000EF904 /* RecipeUploadDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7368792C32EB18000EF904 /* RecipeUploadDTO.swift */; }; 1D7368862C33D7BE000EF904 /* RecipeUploadResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D7368852C33D7BE000EF904 /* RecipeUploadResponseDTO.swift */; }; + 1D8474562C6C917900323001 /* SearchFeedListUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8474552C6C917900323001 /* SearchFeedListUseCaseTests.swift */; }; + 1D8474592C6CCF6900323001 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8474582C6CCF6900323001 /* TestUtils.swift */; }; 1D95A0A62C37C79500F09077 /* RecipeDetailError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D95A0A52C37C79500F09077 /* RecipeDetailError.swift */; }; 1DBC63662C47D23000DA00C2 /* AddRecipeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC63652C47D23000DA00C2 /* AddRecipeError.swift */; }; 1DBC63672C47D23000DA00C2 /* AddRecipeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC63652C47D23000DA00C2 /* AddRecipeError.swift */; }; @@ -120,11 +136,6 @@ 1D1283AB2C15EBE600C5A870 /* FetchFeedListUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFeedListUseCase.swift; sourceTree = ""; }; 1D1283C92C16D9C600C5A870 /* RecipeFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeFetchService.swift; sourceTree = ""; }; 1D166D0C2C4AD54D00A50963 /* AddRecipeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddRecipeViewModel.swift; sourceTree = ""; }; - 1D166DE62C508C7E00A50963 /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; - 1D166DE72C508C7F00A50963 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; - 1D166DEF2C508CA500A50963 /* UserDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDTO.swift; sourceTree = ""; }; - 1D166DF22C508CAB00A50963 /* ErrorResponseDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorResponseDTO.swift; sourceTree = ""; }; - 1D166DF62C508CB700A50963 /* DateFormatter+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extensions.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 = ""; }; 1D2C16E92BE532B700C04508 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -154,6 +165,12 @@ 1D5AEE502C592A8000BBD5F0 /* CGSize+addButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGSize+addButton.swift"; sourceTree = ""; }; 1D5AEE522C592A9900BBD5F0 /* RecipeListItemViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RecipeListItemViewModel.swift; path = View/RecipeListItemViewModel.swift; sourceTree = ""; }; 1D5AEE542C592F2600BBD5F0 /* AddRecipeUseCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddRecipeUseCase.swift; sourceTree = ""; }; + 1D5AEE742C61095500BBD5F0 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + 1D5AEE752C61095500BBD5F0 /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + 1D5AEE7A2C61096A00BBD5F0 /* UserDTO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDTO.swift; sourceTree = ""; }; + 1D5AEE802C61099900BBD5F0 /* RecipeListInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeListInteractorTests.swift; sourceTree = ""; }; + 1D5AEF292C64730A00BBD5F0 /* FetchFeedListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchFeedListUseCaseTests.swift; sourceTree = ""; }; + 1D5AEF2D2C64786F00BBD5F0 /* DateFormatter+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extensions.swift"; sourceTree = ""; }; 1D60CC3C2C3E4F1600D08FA3 /* APIConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIConfig.swift; sourceTree = ""; }; 1D6958D12C3D0553008604B3 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = ""; }; 1D6958D32C3D059E008604B3 /* RecipeListRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeListRouter.swift; sourceTree = ""; }; @@ -164,6 +181,8 @@ 1D7368772C32E7FE000EF904 /* RecipePostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipePostService.swift; sourceTree = ""; }; 1D7368792C32EB18000EF904 /* RecipeUploadDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeUploadDTO.swift; sourceTree = ""; }; 1D7368852C33D7BE000EF904 /* RecipeUploadResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeUploadResponseDTO.swift; sourceTree = ""; }; + 1D8474552C6C917900323001 /* SearchFeedListUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedListUseCaseTests.swift; sourceTree = ""; }; + 1D8474582C6CCF6900323001 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; 1D95A0A52C37C79500F09077 /* RecipeDetailError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailError.swift; sourceTree = ""; }; 1DBC63652C47D23000DA00C2 /* AddRecipeError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddRecipeError.swift; sourceTree = ""; }; 1DC7CC312C283C0200796889 /* RecipeUploadImgaeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeUploadImgaeCell.swift; sourceTree = ""; }; @@ -217,9 +236,9 @@ 1D12839F2C15E7A700C5A870 /* Entities */ = { isa = PBXGroup; children = ( + 1D5AEE752C61095500BBD5F0 /* Comment.swift */, + 1D5AEE742C61095500BBD5F0 /* User.swift */, 1D1283A12C15E94300C5A870 /* Recipe.swift */, - 1D166DE62C508C7E00A50963 /* Comment.swift */, - 1D166DE72C508C7F00A50963 /* User.swift */, 1D1283A32C15EA8100C5A870 /* RecipeType.swift */, 1D95A0A52C37C79500F09077 /* RecipeDetailError.swift */, 1DBC63652C47D23000DA00C2 /* AddRecipeError.swift */, @@ -267,14 +286,6 @@ path = Network; sourceTree = ""; }; - 1D166DF52C508CB700A50963 /* Utilities */ = { - isa = PBXGroup; - children = ( - 1D166DF62C508CB700A50963 /* DateFormatter+Extensions.swift */, - ); - path = Utilities; - sourceTree = ""; - }; 1D2C16D92BE532B700C04508 = { isa = PBXGroup; children = ( @@ -300,11 +311,11 @@ 1D2C16E42BE532B700C04508 /* HomeCafeRecipes */ = { isa = PBXGroup; children = ( + 1D5AEF2C2C64786F00BBD5F0 /* Utilities */, 1D439E972C2C5837008530A5 /* Router */, 1DF829B52C2A7C8600C337FC /* Extensions */, 1DF829B22C2A7A0B00C337FC /* Resources */, 1DE19EB22C1B422F0031804A /* Presentation */, - 1D166DF52C508CB700A50963 /* Utilities */, 1D1283AD2C16974B00C5A870 /* Data */, 1D740B402C15E6680001B704 /* Domain */, 1D2C16E52BE532B700C04508 /* AppDelegate.swift */, @@ -320,9 +331,13 @@ 1D2C16FB2BE532B800C04508 /* HomeCafeRecipesTests */ = { isa = PBXGroup; children = ( + 1D8474572C6CCDC000323001 /* TestUtilities */, 1D2C16FC2BE532B800C04508 /* HomeCafeRecipesTests.swift */, 1D6958D72C3D5A80008604B3 /* RecipeDeatilInteractorTests.swift */, 1D39729D2C46C57A00495014 /* FetchRecipeDetailUseCaseTests.swift */, + 1D5AEE802C61099900BBD5F0 /* RecipeListInteractorTests.swift */, + 1D5AEF292C64730A00BBD5F0 /* FetchFeedListUseCaseTests.swift */, + 1D8474552C6C917900323001 /* SearchFeedListUseCaseTests.swift */, ); path = HomeCafeRecipesTests; sourceTree = ""; @@ -368,19 +383,26 @@ 1D4741CB2C1B4F8D009381CE /* DTO */ = { isa = PBXGroup; children = ( + 1D5AEE7A2C61096A00BBD5F0 /* UserDTO.swift */, 1D4741CC2C1B4F8D009381CE /* RecipeImageDTO.swift */, 1D4741CD2C1B4F8D009381CE /* RecipeDTO.swift */, 1D4741CE2C1B4F8D009381CE /* RecipePageDTO.swift */, 1D4741CF2C1B4F8D009381CE /* NetworkResponseDTO.swift */, 1D73686D2C305757000EF904 /* RecipeDetailDTO.swift */, - 1D166DEF2C508CA500A50963 /* UserDTO.swift */, 1D7368792C32EB18000EF904 /* RecipeUploadDTO.swift */, 1D7368852C33D7BE000EF904 /* RecipeUploadResponseDTO.swift */, - 1D166DF22C508CAB00A50963 /* ErrorResponseDTO.swift */, ); path = DTO; sourceTree = ""; }; + 1D5AEF2C2C64786F00BBD5F0 /* Utilities */ = { + isa = PBXGroup; + children = ( + 1D5AEF2D2C64786F00BBD5F0 /* DateFormatter+Extensions.swift */, + ); + path = Utilities; + sourceTree = ""; + }; 1D740B402C15E6680001B704 /* Domain */ = { isa = PBXGroup; children = ( @@ -391,6 +413,14 @@ path = Domain; sourceTree = ""; }; + 1D8474572C6CCDC000323001 /* TestUtilities */ = { + isa = PBXGroup; + children = ( + 1D8474582C6CCF6900323001 /* TestUtils.swift */, + ); + path = TestUtilities; + sourceTree = ""; + }; 1DDFFD822C1C09AB0083B077 /* Mapper */ = { isa = PBXGroup; children = ( @@ -633,12 +663,13 @@ 1D439E9E2C2C598A008530A5 /* RecipeDetailRepository.swift in Sources */, 1D2C6F6C2C27051D004BB54E /* CustomNavigationBar.swift in Sources */, 1D3972682C44185B00495014 /* RecipeListMapper.swift in Sources */, + 1D5AEE7B2C61096A00BBD5F0 /* UserDTO.swift in Sources */, + 1D5AEE782C61095500BBD5F0 /* Comment.swift in Sources */, 1D2C16EA2BE532B700C04508 /* ViewController.swift in Sources */, 1DE19EC52C1B422F0031804A /* RecipeListView.swift in Sources */, 1D39726C2C458CE100495014 /* MultipartFormDataRequest.swift in Sources */, 1D6958D22C3D0553008604B3 /* Router.swift in Sources */, 1D4741D32C1B4F8D009381CE /* RecipePageDTO.swift in Sources */, - 1D166DEA2C508C7F00A50963 /* User.swift in Sources */, 1D2C6F652C2446D8004BB54E /* MainTabBarController.swift in Sources */, 1DDFFD842C1C324F0083B077 /* RecipeDetailViewController.swift in Sources */, 1D2C16E62BE532B700C04508 /* AppDelegate.swift in Sources */, @@ -647,10 +678,9 @@ 1DE19EB12C1B42200031804A /* NetworkService.swift in Sources */, 1DC7CC322C283C0200796889 /* RecipeUploadImgaeCell.swift in Sources */, 1D7368782C32E7FE000EF904 /* RecipePostService.swift in Sources */, - 1D166DF32C508CAB00A50963 /* ErrorResponseDTO.swift in Sources */, - 1D166DE82C508C7F00A50963 /* Comment.swift in Sources */, 1DBC63662C47D23000DA00C2 /* AddRecipeError.swift in Sources */, 1D95A0A62C37C79500F09077 /* RecipeDetailError.swift in Sources */, + 1D5AEF2E2C64786F00BBD5F0 /* DateFormatter+Extensions.swift in Sources */, 1D5AEE552C592F2600BBD5F0 /* AddRecipeUseCase.swift in Sources */, 1D1283AC2C15EBE600C5A870 /* FetchFeedListUseCase.swift in Sources */, 1D73687A2C32EB18000EF904 /* RecipeUploadDTO.swift in Sources */, @@ -663,7 +693,6 @@ 1DF829B42C2A7A7D00C337FC /* Fonts.swift in Sources */, 1D4741D22C1B4F8D009381CE /* RecipeDTO.swift in Sources */, 1DE19EC02C1B422F0031804A /* RecipeDetailView.swift in Sources */, - 1D166DF02C508CA500A50963 /* UserDTO.swift in Sources */, 1D1283AA2C15EBCF00C5A870 /* SearchFeedUseCase.swift in Sources */, 1DE19EA82C1B420A0031804A /* SearchFeedListRepository.swift in Sources */, 1DE19EC32C1B422F0031804A /* SearchBar.swift in Sources */, @@ -675,6 +704,7 @@ 1D4741D12C1B4F8D009381CE /* RecipeImageDTO.swift in Sources */, 1D7368742C32CF09000EF904 /* AddRecipeRepository.swift in Sources */, 1DE19EA72C1B420A0031804A /* FeedListRepository.swift in Sources */, + 1D5AEE762C61095500BBD5F0 /* User.swift in Sources */, 1DE19EC62C1B422F0031804A /* RecipeListCell.swift in Sources */, 1D166D0D2C4AD54E00A50963 /* AddRecipeViewModel.swift in Sources */, 1DF829B92C2A818D00C337FC /* String+Validation.swift in Sources */, @@ -686,7 +716,6 @@ 1D6958D42C3D059E008604B3 /* RecipeListRouter.swift in Sources */, 1D4741D42C1B4F8D009381CE /* NetworkResponseDTO.swift in Sources */, 1D439E9C2C2C58DD008530A5 /* FetchRecipeDetailUseCase.swift in Sources */, - 1D166DF72C508CB700A50963 /* DateFormatter+Extensions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -694,29 +723,46 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1D5AEF272C646E5B00BBD5F0 /* FetchFeedListUseCase.swift in Sources */, + 1D5AEF392C64795300BBD5F0 /* RecipeDTO.swift in Sources */, 1D6958DF2C3D5E35008604B3 /* NetworkService.swift in Sources */, - 1D166DF42C508CAB00A50963 /* ErrorResponseDTO.swift in Sources */, + 1D8474592C6CCF6900323001 /* TestUtils.swift in Sources */, + 1D5AEF2F2C64786F00BBD5F0 /* DateFormatter+Extensions.swift in Sources */, + 1D5AEF342C64790700BBD5F0 /* String+Validation.swift in Sources */, 1D6958DC2C3D5E20008604B3 /* RecipeDetailRepository.swift in Sources */, + 1D8474562C6C917900323001 /* SearchFeedListUseCaseTests.swift in Sources */, 1D6958E12C3D5E44008604B3 /* RecipeDetailDTO.swift in Sources */, + 1D5AEF382C64794A00BBD5F0 /* CGSize+addButton.swift in Sources */, 1D6958D82C3D5A80008604B3 /* RecipeDeatilInteractorTests.swift in Sources */, + 1D5AEE812C61099900BBD5F0 /* RecipeListInteractorTests.swift in Sources */, 1D60CC402C3EB76600D08FA3 /* APIConfig.swift in Sources */, + 1D5AEF3A2C64795900BBD5F0 /* SearchFeedListRepository.swift in Sources */, + 1D5AEF282C646FE600BBD5F0 /* RecipeListInteractor.swift in Sources */, + 1D5AEF2A2C64730A00BBD5F0 /* FetchFeedListUseCaseTests.swift in Sources */, + 1D5AEF352C64790C00BBD5F0 /* SearchFeedUseCase.swift in Sources */, 1D6958DE2C3D5E2C008604B3 /* RecipeType.swift in Sources */, + 1D5AEF2B2C64753900BBD5F0 /* FeedListRepository.swift in Sources */, + 1D5AEF312C6478E100BBD5F0 /* AddRecipeRepository.swift in Sources */, 1DBC63672C47D23000DA00C2 /* AddRecipeError.swift in Sources */, 1D5AEE562C592F2600BBD5F0 /* AddRecipeUseCase.swift in Sources */, + 1D5AEE792C61095500BBD5F0 /* Comment.swift in Sources */, + 1D5AEF372C64791E00BBD5F0 /* RecipeUploadResponseDTO.swift in Sources */, 1D6958D92C3D5AF7008604B3 /* RecipeDetailInteractor.swift in Sources */, 1D166D0E2C4AD54E00A50963 /* AddRecipeViewModel.swift in Sources */, - 1D166DEB2C508C7F00A50963 /* User.swift in Sources */, - 1D166DE92C508C7F00A50963 /* Comment.swift in Sources */, 1D39729C2C45905700495014 /* MultipartFormDataRequest.swift in Sources */, + 1D5AEF332C64790200BBD5F0 /* RecipeUploadDTO.swift in Sources */, + 1D5AEE7C2C61096A00BBD5F0 /* UserDTO.swift in Sources */, 1D2C16FD2BE532B800C04508 /* HomeCafeRecipesTests.swift in Sources */, 1D6958E42C3D5EA6008604B3 /* NetworkResponseDTO.swift in Sources */, + 1D5AEF302C6478D800BBD5F0 /* RecipeFetchService.swift in Sources */, + 1D5AEF362C64791300BBD5F0 /* RecipePageDTO.swift in Sources */, + 1D5AEF322C6478FE00BBD5F0 /* RecipePostService.swift in Sources */, 1D6958DB2C3D5C91008604B3 /* Recipe.swift in Sources */, 1D6958E02C3D5E3D008604B3 /* RecipeDetailError.swift in Sources */, - 1D166DF82C508CB700A50963 /* DateFormatter+Extensions.swift in Sources */, - 1D166DF12C508CA500A50963 /* UserDTO.swift in Sources */, 1D6958DA2C3D5BA4008604B3 /* FetchRecipeDetailUseCase.swift in Sources */, 1D39729E2C46C57A00495014 /* FetchRecipeDetailUseCaseTests.swift in Sources */, 1D6958E22C3D5E99008604B3 /* RecipeImageDTO.swift in Sources */, + 1D5AEE772C61095500BBD5F0 /* User.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/Recipe.swift b/HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/Recipe.swift index 5da605c..638a41f 100644 --- a/HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/Recipe.swift +++ b/HomeCafeRecipes/HomeCafeRecipes/Domain/Entities/Recipe.swift @@ -21,22 +21,27 @@ struct Recipe { extension Recipe { - static func dummyRecipe() -> Recipe { - .init( - id: 1, - type: .coffee, - name: "", - description: "", - writer: .init( - id: 1, - profileImage: "", - nickname: "", - createdAt: Date() - ), - imageUrls: [], - isLikedByCurrentUser: false, - likeCount: 0, - createdAt: Date() + static func dummyRecipe( + id: Int = 1, + type: RecipeType = .coffee, + name: String = "", + description: String = "", + writer: User = .init(id: 1, profileImage: "", nickname: "", createdAt: Date()), + imageUrls: [String] = [], + isLikedByCurrentUser: Bool = false, + likeCount: Int = 0, + createdAt: Date = Date() + ) -> Recipe { + return Recipe( + id: id, + type: type, + name: name, + description: description, + writer: writer, + imageUrls: imageUrls, + isLikedByCurrentUser: isLikedByCurrentUser, + likeCount: likeCount, + createdAt: createdAt ) } } diff --git a/HomeCafeRecipes/HomeCafeRecipesTests/FetchFeedListUseCaseTests.swift b/HomeCafeRecipes/HomeCafeRecipesTests/FetchFeedListUseCaseTests.swift new file mode 100644 index 0000000..51acf97 --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipesTests/FetchFeedListUseCaseTests.swift @@ -0,0 +1,115 @@ +// +// FetchFeedListUseCaseTests.swift +// HomeCafeRecipesTests +// +// Created by 김건호 on 8/8/24. +// + +import Foundation +import XCTest + +import RxSwift + +@testable +import HomeCafeRecipes + +final class FetchFeedListUseCaseTests: XCTestCase { + + var fetchRecipeListRepository: FeedListRepositoryMock! + var disposeBag: DisposeBag! + + final class FeedListRepositoryMock: FeedListRepository { + var fetchRecipeListCallCount: Int = 0 + var fetchRecipeListStub: Single<[Recipe]> = .just([Recipe.dummyRecipe()]) + + func fetchRecipes(pageNumber: Int) -> Single<[Recipe]> { + fetchRecipeListCallCount += 1 + return fetchRecipeListStub + } + } + + func createUseCase() -> FetchFeedListUseCase { + let usecase = FetchFeedListUseCaseImpl(repository: fetchRecipeListRepository) + return usecase + } + + override func setUpWithError() throws { + fetchRecipeListRepository = .init() + disposeBag = .init() + } +} + +extension FetchFeedListUseCaseTests { + func test_execute를_호출하면_FeedListRepository의_fetchRecipes을_호출합니다(){ + + // Given + + let usecase = createUseCase() + fetchRecipeListRepository.fetchRecipeListStub = .just([Recipe.dummyRecipe()]) + + // When + + usecase.execute(pageNumber: 0) + .subscribe() + .disposed(by: disposeBag) + + // Then + + XCTAssertEqual(fetchRecipeListRepository.fetchRecipeListCallCount, 1) + } + + func test_FeedListRepository의_성공응답이오면_Recipe배열를_반환합니다(){ + let usecase = createUseCase() + let recipe = [Recipe.dummyRecipe()] + fetchRecipeListRepository.fetchRecipeListStub = .just(recipe) + let expectation = self.expectation(description: "Fetch Recipes Success") + + // When + + usecase.execute(pageNumber: 0) + .subscribe(onSuccess:{ result in + if case .success(let fetchedRecipes) = result { + XCTAssertTrue(TestUtils.areRecipesEqual(fetchedRecipes, recipe)) + expectation.fulfill() + } else { + XCTFail("Expected success but got failure") + } + + }, onFailure: { error in + XCTFail("Expected success but got error: \(error)") + + }) + .disposed(by: disposeBag) + + wait(for: [expectation], timeout: 1.0) + XCTAssertEqual(fetchRecipeListRepository.fetchRecipeListCallCount, 1) + } + + func test_FeedListRepository의_실패응답이오면_Error를_반환합니다() { + // Given + let usecase = createUseCase() + let error = NSError(domain: "TestError", code: -1) + fetchRecipeListRepository.fetchRecipeListStub = .error(error) + let expectation = self.expectation(description: "Fetch Recipes Failure") + + // When + usecase.execute(pageNumber: 0) + .subscribe(onSuccess: { result in + if case .failure(let receivedError as NSError) = result { + // Then + + XCTAssertEqual(receivedError.domain, error.domain) + XCTAssertEqual(receivedError.code, error.code) + expectation.fulfill() + } else { + XCTFail("Expected failure but got success") + } + }, onFailure: { error in + XCTFail("Expected failure but got error: \(error)") + }) + .disposed(by: disposeBag) + + wait(for: [expectation], timeout: 1.0) + XCTAssertEqual(fetchRecipeListRepository.fetchRecipeListCallCount, 1) + } +} diff --git a/HomeCafeRecipes/HomeCafeRecipesTests/RecipeListInteractorTests.swift b/HomeCafeRecipes/HomeCafeRecipesTests/RecipeListInteractorTests.swift new file mode 100644 index 0000000..735d049 --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipesTests/RecipeListInteractorTests.swift @@ -0,0 +1,217 @@ +// +// RecipeListInteractorTests.swift +// HomeCafeRecipesTests +// +// Created by 김건호 on 8/5/24. +// + +import Foundation +import XCTest + +import RxSwift + +@testable +import HomeCafeRecipes + +final class RecipeListInteractorTests: XCTestCase { + + var fetchFeedListUseCase: FetchFeedListUseCaseMock! + var searchFeedListUseCase: SearchFeedListUseCaseMock! + var delegate: RecipeListInteractorDelegateMock! + var disposeBag: DisposeBag! + + final class FetchFeedListUseCaseMock: FetchFeedListUseCase { + var executeCallCount: Int = 0 + var executeStub: Single> = .just(.failure(NSError(domain: "Test", code: -1, userInfo: nil))) + func execute(pageNumber: Int) -> Single> { + executeCallCount += 1 + return executeStub + } + } + + final class SearchFeedListUseCaseMock: SearchFeedListUseCase { + var executeCallCount: Int = 0 + var executeStub: Single> = .just(.failure(NSError(domain: "Test", code: -1, userInfo: nil))) + + func execute(title: String, pageNumber: Int) -> Single> { + executeCallCount += 1 + return executeStub + } + } + + final class RecipeListInteractorDelegateMock: RecipeListInteractorDelegate { + var fetchedCallCount: Int = 0 + var showRecipeDetailCallCount: Int = 0 + var fetchedRecipeResult: Result<[Recipe], Error>? + var receivedShowRecipeDetailID: Int? + + func fetchedRecipes(result: Result<[Recipe], Error>) { + fetchedCallCount += 1 + fetchedRecipeResult = result + } + + func showRecipeDetail(ID: Int) { + showRecipeDetailCallCount += 1 + receivedShowRecipeDetailID = ID + } + } + + func createInteractor(pageNumber: Int = 0) -> RecipeListInteractor { + let interactor = RecipeListInteractorImpl( + fetchFeedListUseCase: fetchFeedListUseCase, + searchFeedListUseCase: searchFeedListUseCase + ) + interactor.delegate = delegate + return interactor + } + + override func setUpWithError() throws { + fetchFeedListUseCase = .init() + searchFeedListUseCase = .init() + delegate = .init() + disposeBag = .init() + } +} + +extension RecipeListInteractorTests { + + func test_화면이_로드될때_fetchedRecipes를_호출합니다(){ + + // Given + let interactor = createInteractor() + + // When + + interactor.viewDidLoad() + + // Then + XCTAssertEqual(self.fetchFeedListUseCase.executeCallCount,1) + } + + func test_FetchFeedListUseCase의_성공응답이오면_Delegate로_성공을_전달합니다() { + + // Given + let interactor = createInteractor() + let recipes = [Recipe.dummyRecipe()] + + // When + fetchFeedListUseCase.executeStub = .just(.success(recipes)) + + interactor.viewDidLoad() + + XCTAssertEqual(self.fetchFeedListUseCase.executeCallCount, 1) + XCTAssertEqual(self.delegate.fetchedCallCount, 1) + + if case .success(let fetchedRecipe) = delegate.fetchedRecipeResult { + XCTAssertTrue(TestUtils.areRecipesEqual(fetchedRecipe,recipes)) + } else { + XCTFail("Expected success but got failure or nil") + } + } + + func test_FetchFeedListUseCase의_실패응답이오면_Delegate로_실패를_전달합니다() { + + // Given + let interactor = createInteractor() + let error = NSError(domain: "TestError", code: -1) + fetchFeedListUseCase.executeStub = .just(.failure(error)) + + // When + interactor.viewDidLoad() + + // Then + XCTAssertEqual(self.fetchFeedListUseCase.executeCallCount, 1) + XCTAssertEqual(self.delegate.fetchedCallCount, 1) + + if case .failure(let receivedError as NSError) = delegate.fetchedRecipeResult { + XCTAssertEqual(receivedError.domain, error.domain) + XCTAssertEqual(receivedError.code, error.code) + } else { + XCTFail("Expected failure but got success or nil") + } + } + + func test_searchRecipes_호출시_검색된_결과가_로드됩니다() { + + // Given + let interactor = createInteractor() + let searchResultRecipes = [Recipe.dummyRecipe()] + + searchFeedListUseCase.executeStub = .just(.success(searchResultRecipes)) + + // When + interactor.searchRecipes(with: "Test Query") + + // Then + XCTAssertEqual(searchFeedListUseCase.executeCallCount, 1) + XCTAssertEqual(delegate.fetchedCallCount, 1) + if case .success(let fetchedRecipes) = delegate.fetchedRecipeResult { + XCTAssertTrue(TestUtils.areRecipesEqual(fetchedRecipes, searchResultRecipes)) + } else { + XCTFail("Expected success but got failure or nil") + } + } + + func test_fetchNextPage_호출시_다음_페이지_레시피가_로드됩니다() { + + // Given + let interactor = createInteractor() + let initialRecipes = [Recipe.dummyRecipe()] + let nextPageRecipes = [Recipe.dummyRecipe(id: 2)] + fetchFeedListUseCase.executeStub = .just(.success(initialRecipes)) + interactor.viewDidLoad() + fetchFeedListUseCase.executeStub = .just(.success(nextPageRecipes)) + + // When + interactor.fetchNextPage() + + // Then + XCTAssertEqual(fetchFeedListUseCase.executeCallCount, 2) + XCTAssertEqual(delegate.fetchedCallCount, 2) + if case .success(let fetchedRecipes) = delegate.fetchedRecipeResult { + XCTAssertEqual(fetchedRecipes.count, 2) + XCTAssertTrue(TestUtils.areRecipesEqual(fetchedRecipes, initialRecipes + nextPageRecipes)) + } else { + XCTFail("Expected success but got failure or nil") + } + } + + func test_didSelectItem_호출시_레시피_상세화면으로_이동합니다() { + + // Given + let interactor = createInteractor() + let selectedID = 1 + + // When + interactor.didSelectItem(ID: selectedID) + + // Then + XCTAssertEqual(delegate.showRecipeDetailCallCount, 1) + XCTAssertEqual(delegate.receivedShowRecipeDetailID, selectedID) + } + + func test_resetSearch_호출후_다시_searchRecipes_호출시_검색이_정상적으로_동작합니다() { + + // Given + let interactor = createInteractor() + let initialRecipes = [Recipe.dummyRecipe()] + searchFeedListUseCase.executeStub = .just(.success(initialRecipes)) + + // When + interactor.searchRecipes(with: "Test Query") + interactor.resetSearch() + + let newSearchRecipes = [Recipe.dummyRecipe(id: 2)] + searchFeedListUseCase.executeStub = .just(.success(newSearchRecipes)) + interactor.searchRecipes(with: "New Query") + + // Then + XCTAssertEqual(searchFeedListUseCase.executeCallCount, 2) + XCTAssertEqual(delegate.fetchedCallCount, 3) + if case .success(let fetchedRecipes) = delegate.fetchedRecipeResult { + XCTAssertTrue(TestUtils.areRecipesEqual(fetchedRecipes, newSearchRecipes)) + } else { + XCTFail("Expected success but got failure or nil") + } + } +} diff --git a/HomeCafeRecipes/HomeCafeRecipesTests/SearchFeedListUseCaseTests.swift b/HomeCafeRecipes/HomeCafeRecipesTests/SearchFeedListUseCaseTests.swift new file mode 100644 index 0000000..bf94bc7 --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipesTests/SearchFeedListUseCaseTests.swift @@ -0,0 +1,119 @@ +// +// SearchFeedListUseCaseTests.swift +// HomeCafeRecipesTests +// +// Created by 김건호 on 8/14/24. +// + +import XCTest + +import RxSwift + +@testable +import HomeCafeRecipes + +final class SearchFeedListUseCaseTests: XCTestCase { + + var searchRecipeRepository: SearchFeedListRepositoryMock! + var disposeBag: DisposeBag! + + final class SearchFeedListRepositoryMock: SearchFeedListRepository { + var searchRecipeListCallCount: Int = 0 + var searchRecipeListStub: Single<[Recipe]> = .just([Recipe.dummyRecipe()]) + func searchRecipes( + title: String, + pageNumber: Int + ) -> Single<[Recipe]> + { + searchRecipeListCallCount += 1 + return searchRecipeListStub + } + } + + func createUsecase() -> SearchFeedListUseCase { + let usecase = SearchFeedListUseCaseImpl(repository: searchRecipeRepository) + return usecase + } + + override func setUpWithError() throws { + searchRecipeRepository = .init() + disposeBag = .init() + } + + func test_execute를_호출하면_SearchRecipeRepository의_searchRecipes을_호출합니다(){ + + // Given + + let usecase = createUsecase() + + // When + + usecase.execute( + title: "", + pageNumber: 0 + ) + .subscribe() + .disposed(by: disposeBag) + + // Then + + XCTAssertEqual(searchRecipeRepository.searchRecipeListCallCount, 1) + } + + func test_SearchRecipeRepository의_성공응답이오면_Recipe배열을_반환합니다() { + // Given + let usecase = createUsecase() + let recipes = [Recipe.dummyRecipe()] + searchRecipeRepository.searchRecipeListStub = .just(recipes) + let expectation = self.expectation(description: "Search Recipes Success") + + // When + usecase.execute( + title: "Test Recipe", + pageNumber: 1 + ) + .subscribe(onSuccess: { result in + switch result { + case .success(let fetchedRecipes): + // Then + XCTAssertTrue(TestUtils.areRecipesEqual(fetchedRecipes, recipes)) + expectation.fulfill() + case .failure: + XCTFail("Expected success but got failure") + } + }, onFailure: { error in + XCTFail("Expected success but got error: \(error)") + }) + .disposed(by: disposeBag) + + wait(for: [expectation], timeout: 1.0) + } + + + func test_SearchRecipeRepository의_실패응답이오면_Error를_반환합니다() { + // Given + let usecase = createUsecase() + let error = NSError(domain: "TestError", code: -1, userInfo: nil) + searchRecipeRepository.searchRecipeListStub = .error(error) + let expectation = self.expectation(description: "Search Recipes Failure") + + // When + usecase.execute(title: "Test Recipe", pageNumber: 1) + .subscribe(onSuccess: { result in + switch result { + case .success: + XCTFail("Expected failure but got success") + case .failure(let receivedError as NSError): + XCTAssertEqual(receivedError.domain, error.domain) + XCTAssertEqual(receivedError.code, error.code) + expectation.fulfill() + } + }, onFailure: { error in + XCTFail("Expected failure but got error: \(error)") + }) + .disposed(by: disposeBag) + + wait(for: [expectation], timeout: 1.0) + } + +} diff --git a/HomeCafeRecipes/HomeCafeRecipesTests/TestUtilities/TestUtils.swift b/HomeCafeRecipes/HomeCafeRecipesTests/TestUtilities/TestUtils.swift new file mode 100644 index 0000000..6c21209 --- /dev/null +++ b/HomeCafeRecipes/HomeCafeRecipesTests/TestUtilities/TestUtils.swift @@ -0,0 +1,28 @@ +// +// TestUtils.swift +// HomeCafeRecipesTests +// +// Created by 김건호 on 8/14/24. +// + +import Foundation + +class TestUtils { + static func areRecipesEqual(_ lhs: [Recipe], _ rhs: [Recipe]) -> Bool { + guard lhs.count == rhs.count else { return false } + for (index, recipe) in lhs.enumerated() { + if recipe.id != rhs[index].id || + recipe.type != rhs[index].type || + recipe.name != rhs[index].name || + recipe.description != rhs[index].description || + recipe.writer != rhs[index].writer || + recipe.imageUrls != rhs[index].imageUrls || + recipe.isLikedByCurrentUser != rhs[index].isLikedByCurrentUser || + recipe.likeCount != rhs[index].likeCount || + recipe.createdAt != rhs[index].createdAt { + return false + } + } + return true + } +}