diff --git a/BoxOffice.xcodeproj/project.pbxproj b/BoxOffice.xcodeproj/project.pbxproj index b4505711..d7dfb512 100644 --- a/BoxOffice.xcodeproj/project.pbxproj +++ b/BoxOffice.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 27157BF72B8344F3000BAC45 /* MovieInfomationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27157BF62B8344F2000BAC45 /* MovieInfomationDetail.swift */; }; + 273615902BB04070004C3F15 /* HttpMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2736158F2BB04070004C3F15 /* HttpMethod.swift */; }; 2757936F2B7F1DB900C08906 /* MovieInfoResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757936E2B7F1DB900C08906 /* MovieInfoResult.swift */; }; 275793712B7F1DCC00C08906 /* MovieInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275793702B7F1DCC00C08906 /* MovieInfo.swift */; }; 275793732B7F1DE200C08906 /* Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275793722B7F1DE200C08906 /* Actor.swift */; }; @@ -19,10 +20,14 @@ 2757937F2B7F1E3F00C08906 /* ShowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757937E2B7F1E3F00C08906 /* ShowType.swift */; }; 275793812B7F1E4C00C08906 /* Staff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275793802B7F1E4C00C08906 /* Staff.swift */; }; 275793862B7F516800C08906 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275793852B7F516800C08906 /* MockURLSession.swift */; }; - 275793882B7F518900C08906 /* MockURLSessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275793872B7F518900C08906 /* MockURLSessionDataTask.swift */; }; 2757938A2B7F51BF00C08906 /* URLSession+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275793892B7F51BF00C08906 /* URLSession+Extension.swift */; }; 2757938C2B7F52B500C08906 /* JSONLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757938B2B7F52B500C08906 /* JSONLoader.swift */; }; 2757938D2B7F546300C08906 /* box_office.json in Resources */ = {isa = PBXBuildFile; fileRef = 275793822B7F402300C08906 /* box_office.json */; }; + 2757D9DB2B9B2BCB00B13BD7 /* MovieImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757D9DA2B9B2BCB00B13BD7 /* MovieImage.swift */; }; + 2757D9DD2B9B2C4200B13BD7 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757D9DC2B9B2C4200B13BD7 /* Document.swift */; }; + 2757D9DF2B9B2C5400B13BD7 /* Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2757D9DE2B9B2C5400B13BD7 /* Meta.swift */; }; + 27674EE82BA95ED400564726 /* APIType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27674EE72BA95ED400564726 /* APIType.swift */; }; + 27674EEE2BA95F3300564726 /* BoxOfficeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27674EED2BA95F3300564726 /* BoxOfficeType.swift */; }; 276ED7632B7B002600D37EBF /* BoxOfficeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276ED7622B7B002600D37EBF /* BoxOfficeTests.swift */; }; 276ED76A2B7B006400D37EBF /* BoxOffice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276ED7692B7B006400D37EBF /* BoxOffice.swift */; }; 276ED76C2B7B006D00D37EBF /* MovieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276ED76B2B7B006D00D37EBF /* MovieManager.swift */; }; @@ -31,6 +36,8 @@ 276ED7722B7B013E00D37EBF /* RankOldAndNew.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276ED7712B7B013E00D37EBF /* RankOldAndNew.swift */; }; 276ED7762B7B08E100D37EBF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276ED7752B7B08E100D37EBF /* NetworkError.swift */; }; 276ED7982B7B638C00D37EBF /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276ED7972B7B638C00D37EBF /* APIService.swift */; }; + 2782B6082BA1ACDD00D55005 /* MovieRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2782B6072BA1ACDD00D55005 /* MovieRepository.swift */; }; + 2782B60A2BA2B99E00D55005 /* LoadingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2782B6092BA2B99E00D55005 /* LoadingIndicatorView.swift */; }; 278FA99E2B88BEE100FFDB3F /* BoxOfficeListDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278FA99D2B88BEE100FFDB3F /* BoxOfficeListDataSource.swift */; }; 278FA9A32B8B0C7500FFDB3F /* BoxOfficeListViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278FA9A22B8B0C7500FFDB3F /* BoxOfficeListViewDelegate.swift */; }; 27AE2ED62B86D99C009B43E2 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27AE2ED52B86D99C009B43E2 /* Date+Extension.swift */; }; @@ -47,10 +54,12 @@ 63DF20FB2970E1A1005DF7D1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */; }; C14DF4B52B7DAE49009E3258 /* Bundle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14DF4B42B7DAE49009E3258 /* Bundle+Extension.swift */; }; C14DF4B72B7DAF3A009E3258 /* Private.plist in Resources */ = {isa = PBXBuildFile; fileRef = C14DF4B62B7DAF3A009E3258 /* Private.plist */; }; - C14DF4B92B7DAF97009E3258 /* MovieURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14DF4B82B7DAF97009E3258 /* MovieURL.swift */; }; + C14DF4B92B7DAF97009E3258 /* NetworkURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14DF4B82B7DAF97009E3258 /* NetworkURL.swift */; }; + C16A4D6E2B9ADF2100498BB3 /* BoxOfficeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16A4D6D2B9ADF2100498BB3 /* BoxOfficeDetailView.swift */; }; + C16A4D702B9AE4E600498BB3 /* BoxOfficeDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16A4D6F2B9AE4E600498BB3 /* BoxOfficeDetailViewController.swift */; }; + C16A4D722B9AE92C00498BB3 /* ReusedDetailStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16A4D712B9AE92C00498BB3 /* ReusedDetailStackView.swift */; }; C1FC46B02B84962000C6E49E /* JSONFileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FC46AF2B84962000C6E49E /* JSONFileName.swift */; }; C1FC46B22B8496B300C6E49E /* URLSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FC46B12B8496B300C6E49E /* URLSessionProtocol.swift */; }; - C1FC46B42B84AFFC00C6E49E /* URLSessionDataTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FC46B32B84AFFC00C6E49E /* URLSessionDataTaskProtocol.swift */; }; C1FC46BC2B85CD5A00C6E49E /* MovieCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FC46BB2B85CD5A00C6E49E /* MovieCell.swift */; }; C1FC46BE2B85D1A000C6E49E /* BoxOfficeListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FC46BD2B85D1A000C6E49E /* BoxOfficeListView.swift */; }; C1FC46C22B8772BF00C6E49E /* Int+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FC46C12B8772BF00C6E49E /* Int+Extension.swift */; }; @@ -68,6 +77,7 @@ /* Begin PBXFileReference section */ 27157BF62B8344F2000BAC45 /* MovieInfomationDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieInfomationDetail.swift; sourceTree = ""; }; + 2736158F2BB04070004C3F15 /* HttpMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HttpMethod.swift; sourceTree = ""; }; 2757936E2B7F1DB900C08906 /* MovieInfoResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieInfoResult.swift; sourceTree = ""; }; 275793702B7F1DCC00C08906 /* MovieInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieInfo.swift; sourceTree = ""; }; 275793722B7F1DE200C08906 /* Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actor.swift; sourceTree = ""; }; @@ -80,9 +90,13 @@ 275793802B7F1E4C00C08906 /* Staff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Staff.swift; sourceTree = ""; }; 275793822B7F402300C08906 /* box_office.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = box_office.json; sourceTree = ""; }; 275793852B7F516800C08906 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; - 275793872B7F518900C08906 /* MockURLSessionDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSessionDataTask.swift; sourceTree = ""; }; 275793892B7F51BF00C08906 /* URLSession+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+Extension.swift"; sourceTree = ""; }; 2757938B2B7F52B500C08906 /* JSONLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONLoader.swift; sourceTree = ""; }; + 2757D9DA2B9B2BCB00B13BD7 /* MovieImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieImage.swift; sourceTree = ""; }; + 2757D9DC2B9B2C4200B13BD7 /* Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; + 2757D9DE2B9B2C5400B13BD7 /* Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Meta.swift; sourceTree = ""; }; + 27674EE72BA95ED400564726 /* APIType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIType.swift; sourceTree = ""; }; + 27674EED2BA95F3300564726 /* BoxOfficeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeType.swift; sourceTree = ""; }; 276ED7602B7B002600D37EBF /* BoxOfficeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BoxOfficeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 276ED7622B7B002600D37EBF /* BoxOfficeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeTests.swift; sourceTree = ""; }; 276ED7692B7B006400D37EBF /* BoxOffice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOffice.swift; sourceTree = ""; }; @@ -92,6 +106,8 @@ 276ED7712B7B013E00D37EBF /* RankOldAndNew.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankOldAndNew.swift; sourceTree = ""; }; 276ED7752B7B08E100D37EBF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 276ED7972B7B638C00D37EBF /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; }; + 2782B6072BA1ACDD00D55005 /* MovieRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieRepository.swift; sourceTree = ""; }; + 2782B6092BA2B99E00D55005 /* LoadingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicatorView.swift; sourceTree = ""; }; 278FA99D2B88BEE100FFDB3F /* BoxOfficeListDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeListDataSource.swift; sourceTree = ""; }; 278FA9A22B8B0C7500FFDB3F /* BoxOfficeListViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeListViewDelegate.swift; sourceTree = ""; }; 27AE2ED52B86D99C009B43E2 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; @@ -110,10 +126,12 @@ 63DF20FC2970E1A1005DF7D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C14DF4B42B7DAE49009E3258 /* Bundle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extension.swift"; sourceTree = ""; }; C14DF4B62B7DAF3A009E3258 /* Private.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Private.plist; sourceTree = ""; }; - C14DF4B82B7DAF97009E3258 /* MovieURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieURL.swift; sourceTree = ""; }; + C14DF4B82B7DAF97009E3258 /* NetworkURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkURL.swift; sourceTree = ""; }; + C16A4D6D2B9ADF2100498BB3 /* BoxOfficeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeDetailView.swift; sourceTree = ""; }; + C16A4D6F2B9AE4E600498BB3 /* BoxOfficeDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeDetailViewController.swift; sourceTree = ""; }; + C16A4D712B9AE92C00498BB3 /* ReusedDetailStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusedDetailStackView.swift; sourceTree = ""; }; C1FC46AF2B84962000C6E49E /* JSONFileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONFileName.swift; sourceTree = ""; }; C1FC46B12B8496B300C6E49E /* URLSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionProtocol.swift; sourceTree = ""; }; - C1FC46B32B84AFFC00C6E49E /* URLSessionDataTaskProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionDataTaskProtocol.swift; sourceTree = ""; }; C1FC46BB2B85CD5A00C6E49E /* MovieCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieCell.swift; sourceTree = ""; }; C1FC46BD2B85D1A000C6E49E /* BoxOfficeListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeListView.swift; sourceTree = ""; }; C1FC46C12B8772BF00C6E49E /* Int+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Extension.swift"; sourceTree = ""; }; @@ -142,6 +160,7 @@ children = ( 276ED7692B7B006400D37EBF /* BoxOffice.swift */, 27157BF62B8344F2000BAC45 /* MovieInfomationDetail.swift */, + 2757D9D92B9B2B6F00B13BD7 /* Image */, 2757936D2B7F1D9C00C08906 /* DetailMovie */, 2757936C2B7F1D8900C08906 /* DailyBoxOffice */, ); @@ -180,11 +199,30 @@ children = ( 2757938B2B7F52B500C08906 /* JSONLoader.swift */, 275793852B7F516800C08906 /* MockURLSession.swift */, - 275793872B7F518900C08906 /* MockURLSessionDataTask.swift */, ); path = Mock; sourceTree = ""; }; + 2757D9D92B9B2B6F00B13BD7 /* Image */ = { + isa = PBXGroup; + children = ( + 2757D9DA2B9B2BCB00B13BD7 /* MovieImage.swift */, + 2757D9DC2B9B2C4200B13BD7 /* Document.swift */, + 2757D9DE2B9B2C5400B13BD7 /* Meta.swift */, + ); + path = Image; + sourceTree = ""; + }; + 27674EE62BA95E9800564726 /* URLRequest */ = { + isa = PBXGroup; + children = ( + 27674EE72BA95ED400564726 /* APIType.swift */, + 27674EED2BA95F3300564726 /* BoxOfficeType.swift */, + 2736158F2BB04070004C3F15 /* HttpMethod.swift */, + ); + path = URLRequest; + sourceTree = ""; + }; 276ED7612B7B002600D37EBF /* BoxOfficeTests */ = { isa = PBXGroup; children = ( @@ -198,9 +236,9 @@ isa = PBXGroup; children = ( 276ED76B2B7B006D00D37EBF /* MovieManager.swift */, + 2782B6072BA1ACDD00D55005 /* MovieRepository.swift */, 2757936B2B7F1D8100C08906 /* Movie */, 276ED7752B7B08E100D37EBF /* NetworkError.swift */, - C14DF4B82B7DAF97009E3258 /* MovieURL.swift */, C1FC46AE2B84961300C6E49E /* JSON */, ); path = Model; @@ -210,6 +248,7 @@ isa = PBXGroup; children = ( 63DF20F22970E1A0005DF7D1 /* BoxOfficeViewController.swift */, + C16A4D6F2B9AE4E600498BB3 /* BoxOfficeDetailViewController.swift */, 278FA99F2B8B0BF500FFDB3F /* Protocol */, ); path = Controller; @@ -239,7 +278,9 @@ 276ED79B2B7C605500D37EBF /* Network */ = { isa = PBXGroup; children = ( + 27674EE62BA95E9800564726 /* URLRequest */, 276ED7972B7B638C00D37EBF /* APIService.swift */, + C14DF4B82B7DAF97009E3258 /* NetworkURL.swift */, 27AE2ED42B86D91E009B43E2 /* Protocol */, 275793842B7F515500C08906 /* Mock */, ); @@ -268,7 +309,6 @@ isa = PBXGroup; children = ( C1FC46B12B8496B300C6E49E /* URLSessionProtocol.swift */, - C1FC46B32B84AFFC00C6E49E /* URLSessionDataTaskProtocol.swift */, ); path = Protocol; sourceTree = ""; @@ -333,6 +373,9 @@ 27AE2EDB2B86E950009B43E2 /* RankStackView.swift */, 27AE2ED92B86E930009B43E2 /* RankStateView.swift */, 27AE2EDD2B86E965009B43E2 /* MovieStackView.swift */, + C16A4D6D2B9ADF2100498BB3 /* BoxOfficeDetailView.swift */, + C16A4D712B9AE92C00498BB3 /* ReusedDetailStackView.swift */, + 2782B6092BA2B99E00D55005 /* LoadingIndicatorView.swift */, ); path = View; sourceTree = ""; @@ -452,6 +495,7 @@ 275793712B7F1DCC00C08906 /* MovieInfo.swift in Sources */, 63DF20F32970E1A0005DF7D1 /* BoxOfficeViewController.swift in Sources */, C1FC46B22B8496B300C6E49E /* URLSessionProtocol.swift in Sources */, + 27674EE82BA95ED400564726 /* APIType.swift in Sources */, 275793812B7F1E4C00C08906 /* Staff.swift in Sources */, 27157BF72B8344F3000BAC45 /* MovieInfomationDetail.swift in Sources */, 276ED7702B7B012900D37EBF /* DailyBoxOfficeList.swift in Sources */, @@ -460,35 +504,43 @@ 276ED76E2B7B010D00D37EBF /* BoxOfficeResult.swift in Sources */, 2757936F2B7F1DB900C08906 /* MovieInfoResult.swift in Sources */, 2757937F2B7F1E3F00C08906 /* ShowType.swift in Sources */, + 2757D9DD2B9B2C4200B13BD7 /* Document.swift in Sources */, 27AE2EE02B86E9AD009B43E2 /* UIFont+Extension.swift in Sources */, 276ED76C2B7B006D00D37EBF /* MovieManager.swift in Sources */, 2757937B2B7F1E2400C08906 /* Genre.swift in Sources */, 275793792B7F1E1400C08906 /* Director.swift in Sources */, 276ED76A2B7B006400D37EBF /* BoxOffice.swift in Sources */, + C16A4D702B9AE4E600498BB3 /* BoxOfficeDetailViewController.swift in Sources */, 278FA99E2B88BEE100FFDB3F /* BoxOfficeListDataSource.swift in Sources */, 276ED7982B7B638C00D37EBF /* APIService.swift in Sources */, + 2757D9DF2B9B2C5400B13BD7 /* Meta.swift in Sources */, + C16A4D6E2B9ADF2100498BB3 /* BoxOfficeDetailView.swift in Sources */, 275793732B7F1DE200C08906 /* Actor.swift in Sources */, + 27674EEE2BA95F3300564726 /* BoxOfficeType.swift in Sources */, + 2757D9DB2B9B2BCB00B13BD7 /* MovieImage.swift in Sources */, + 2782B60A2BA2B99E00D55005 /* LoadingIndicatorView.swift in Sources */, + C16A4D722B9AE92C00498BB3 /* ReusedDetailStackView.swift in Sources */, C1FC46C22B8772BF00C6E49E /* Int+Extension.swift in Sources */, 27C0258B2B8B30BD009C3F1F /* String+Extension.swift in Sources */, C1FC46BE2B85D1A000C6E49E /* BoxOfficeListView.swift in Sources */, 2757937D2B7F1E3200C08906 /* Nation.swift in Sources */, 276ED7762B7B08E100D37EBF /* NetworkError.swift in Sources */, 2757938C2B7F52B500C08906 /* JSONLoader.swift in Sources */, + 2782B6082BA1ACDD00D55005 /* MovieRepository.swift in Sources */, C1FC46B02B84962000C6E49E /* JSONFileName.swift in Sources */, - C1FC46B42B84AFFC00C6E49E /* URLSessionDataTaskProtocol.swift in Sources */, 27AE2ED62B86D99C009B43E2 /* Date+Extension.swift in Sources */, 278FA9A32B8B0C7500FFDB3F /* BoxOfficeListViewDelegate.swift in Sources */, 2757938A2B7F51BF00C08906 /* URLSession+Extension.swift in Sources */, C14DF4B52B7DAE49009E3258 /* Bundle+Extension.swift in Sources */, 275793772B7F1E0800C08906 /* Company.swift in Sources */, + 273615902BB04070004C3F15 /* HttpMethod.swift in Sources */, 276ED7722B7B013E00D37EBF /* RankOldAndNew.swift in Sources */, 27AE2EDA2B86E930009B43E2 /* RankStateView.swift in Sources */, 275793862B7F516800C08906 /* MockURLSession.swift in Sources */, 275793752B7F1DF900C08906 /* Audit.swift in Sources */, - 275793882B7F518900C08906 /* MockURLSessionDataTask.swift in Sources */, 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */, 27AE2EDC2B86E950009B43E2 /* RankStackView.swift in Sources */, - C14DF4B92B7DAF97009E3258 /* MovieURL.swift in Sources */, + C14DF4B92B7DAF97009E3258 /* NetworkURL.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -521,6 +573,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T92LV6HV88; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -544,6 +597,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = T92LV6HV88; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; diff --git a/BoxOffice/Controller/BoxOfficeDetailViewController.swift b/BoxOffice/Controller/BoxOfficeDetailViewController.swift new file mode 100644 index 00000000..47cd3a39 --- /dev/null +++ b/BoxOffice/Controller/BoxOfficeDetailViewController.swift @@ -0,0 +1,72 @@ +// +// BoxOfficeDetailViewController.swift +// BoxOffice +// +// Created by Matthew on 3/8/24. +// + +import UIKit + +final class BoxOfficeDetailViewController: UIViewController { + private let movieName: String + private let movieCode: String + private let movieManager: MovieManager + private let boxOfficeDetailView = BoxOfficeDetailView() + + init( + movieName: String, + movieCode: String, + movieManager: MovieManager + ) { + self.movieName = movieName + self.movieCode = movieCode + self.movieManager = movieManager + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + fetchBoxOfficeDetailData() + } +} + +private extension BoxOfficeDetailViewController { + func setupView() { + LoadingIndicatorView.showLoading(in: self.boxOfficeDetailView) + view = boxOfficeDetailView + view.backgroundColor = .white + self.title = movieName + } + + func fetchBoxOfficeDetailData() { + Task { + do { + try await fetchMoiveImageURL() + try await fetchMovieInfo() + try await setupMovieImage() + } catch { + print(error.localizedDescription) + } + LoadingIndicatorView.hideLoading(in: self.boxOfficeDetailView) + } + } + + func fetchMovieInfo() async throws { + let data = try await movieManager.fetchMovieInfoResultData(code: movieCode) + self.boxOfficeDetailView.setupDetailView(data: data) + } + + func fetchMoiveImageURL() async throws { + try await movieManager.fetchMoiveImageURL(movieName: movieName) + } + + func setupMovieImage() async throws { + guard let data = try await movieManager.fetchImageData() else { return } + self.boxOfficeDetailView.setupImage(image: UIImage(data: data)) + } +} diff --git a/BoxOffice/Controller/BoxOfficeViewController.swift b/BoxOffice/Controller/BoxOfficeViewController.swift index 3a045729..a962208b 100644 --- a/BoxOffice/Controller/BoxOfficeViewController.swift +++ b/BoxOffice/Controller/BoxOfficeViewController.swift @@ -17,20 +17,9 @@ final class BoxOfficeViewController: UIViewController { }() private lazy var dataSource = BoxOfficeListDataSource(self.boxOfficeListView) -// init() { -// super.init(nibName: nil, bundle: nil) -// } -// -// required init?(coder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } - override func viewDidLoad() { super.viewDidLoad() - boxOfficeListView.boxOfficeListDelegate = self - boxOfficeListView.delegate = self - boxOfficeListView.dataSource = dataSource - view = boxOfficeListView + setupBoxOfficeListView() setupBoxOfficeData() } } @@ -40,13 +29,22 @@ private extension BoxOfficeViewController { self.title = Date.titleDateFormatter.string(from: date) } + func setupBoxOfficeListView() { + LoadingIndicatorView.showLoading(in: self.boxOfficeListView) + boxOfficeListView.boxOfficeListDelegate = self + boxOfficeListView.delegate = self + boxOfficeListView.dataSource = dataSource + view = boxOfficeListView + } + func setupBoxOfficeData() { - movieManager.fetchBoxOfficeResultData(date: Date.movieDateToString) { result in - switch result { - case .success(let success): - self.reloadCollectionListData(result: success) - case .failure(let failure): - print("fetchBoxOfficeResultData 실패: \(failure)") + Task { + do { + let result = try await movieManager.fetchBoxOfficeResultData(date: Date.movieDateToString) + self.reloadCollectionListData(result: result) + LoadingIndicatorView.hideLoading(in: self.view) + } catch { + print(error.localizedDescription) } } } @@ -60,19 +58,19 @@ private extension BoxOfficeViewController { } func updateBoxOfficeList() { - movieManager.fetchBoxOfficeResultData(date: Date.movieDateToString) { [weak self] result in - switch result { - case .success(let result): - DispatchQueue.main.async { - self?.applyBoxOfficeList(result: result) - guard - let date = result.showRange.toDateFromRange() - else { - return - } - self?.configureNavigation(date: date) + Task { + do { + let result = try await self.movieManager.fetchBoxOfficeResultData( + date: Date.movieDateToString + ) + self.applyBoxOfficeList(result: result) + guard + let date = result.showRange.toDateFromRange() + else { + return } - case .failure(let error): + self.configureNavigation(date: date) + } catch { print(error.localizedDescription) } } @@ -80,7 +78,6 @@ private extension BoxOfficeViewController { func reloadCollectionListData(result: BoxOfficeResult) { DispatchQueue.main.async { - self.boxOfficeListView.indicatorView.stopAnimating() self.configureNavigation(date: Date.yesterday) self.applyBoxOfficeList(result: result) self.boxOfficeListView.isScrollEnabled = true @@ -90,7 +87,17 @@ private extension BoxOfficeViewController { extension BoxOfficeViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - self.navigationController?.popViewController(animated: true) + guard + let data = movieManager.setupBoxOfficeDetailData(for: indexPath.row) + else { + return + } + let detailViewController = BoxOfficeDetailViewController( + movieName: data.movieName, + movieCode: data.movieCode, + movieManager: movieManager + ) + self.navigationController?.pushViewController(detailViewController, animated: true) } } diff --git a/BoxOffice/Extension/Bundle+Extension.swift b/BoxOffice/Extension/Bundle+Extension.swift index 91970065..39ba2dc5 100644 --- a/BoxOffice/Extension/Bundle+Extension.swift +++ b/BoxOffice/Extension/Bundle+Extension.swift @@ -8,8 +8,8 @@ import Foundation extension Bundle { - var apiKey: String { - guard + var movieApiKey: String { + guard let file = self.path(forResource: "Private", ofType: "plist"), let resource = NSDictionary(contentsOfFile: file), let key = resource["API_KEY"] as? String @@ -18,4 +18,15 @@ extension Bundle { } return key } + + var kakaoApiKey: String { + guard + let file = self.path(forResource: "Private", ofType: "plist"), + let resource = NSDictionary(contentsOfFile: file), + let key = resource["Kakao_API_KEY"] as? String + else { + return "" + } + return key + } } diff --git a/BoxOffice/Extension/String+Extension.swift b/BoxOffice/Extension/String+Extension.swift index f1be98e4..caa2e55c 100644 --- a/BoxOffice/Extension/String+Extension.swift +++ b/BoxOffice/Extension/String+Extension.swift @@ -9,9 +9,20 @@ import Foundation extension String { func toDateFromRange() -> Date? { - guard let result = self.split(separator: "~").last else { return nil } - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyyMMdd" - return dateFormatter.date(from: Self(result)) + guard + let result = self.split(separator: "~").last + else { + return nil + } + return Date.movieDateFormatter.date(from: Self(result)) + } + + var makeDateFormat: String { + guard + let date = Date.movieDateFormatter.date(from: self) + else { + return "-" + } + return Date.titleDateFormatter.string(from: date) } } diff --git a/BoxOffice/Extension/URLSession+Extension.swift b/BoxOffice/Extension/URLSession+Extension.swift index 594c4ff7..f6400783 100644 --- a/BoxOffice/Extension/URLSession+Extension.swift +++ b/BoxOffice/Extension/URLSession+Extension.swift @@ -8,15 +8,7 @@ import Foundation extension URLSession: URLSessionProtocol { - func dataTask( - with url: URL, - completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void - ) -> URLSessionDataTaskProtocol { - return dataTask( - with: url, - completionHandler: completionHandler - ) as URLSessionDataTask + func requestData(with request: URLRequest) async throws -> (Data, URLResponse) { + return try await data(for: request) } } - -extension URLSessionDataTask: URLSessionDataTaskProtocol {} diff --git a/BoxOffice/Model/Movie/Image/Document.swift b/BoxOffice/Model/Movie/Image/Document.swift new file mode 100644 index 00000000..59d3c117 --- /dev/null +++ b/BoxOffice/Model/Movie/Image/Document.swift @@ -0,0 +1,28 @@ +// +// Document.swift +// BoxOffice +// +// Created by 강창현 on 3/8/24. +// + +struct Document: Codable { + let collection: String + let thumbnailURL: String + let imageURL: String + let width: Int + let height: Int + let displaySiteName: String + let docURL: String + let datetime: String + + enum CodingKeys: String, CodingKey { + case collection + case thumbnailURL = "thumbnail_url" + case imageURL = "image_url" + case width + case height + case displaySiteName = "display_sitename" + case docURL = "doc_url" + case datetime + } +} diff --git a/BoxOffice/Model/Movie/Image/Meta.swift b/BoxOffice/Model/Movie/Image/Meta.swift new file mode 100644 index 00000000..ff4219b7 --- /dev/null +++ b/BoxOffice/Model/Movie/Image/Meta.swift @@ -0,0 +1,18 @@ +// +// Meta.swift +// BoxOffice +// +// Created by 강창현 on 3/8/24. +// + +struct Meta: Codable { + let totalCount: Int + let pageableCount: Int + let isEnd: Bool + + enum CodingKeys: String, CodingKey { + case isEnd = "is_end" + case pageableCount = "pageable_count" + case totalCount = "total_count" + } +} diff --git a/BoxOffice/Model/Movie/Image/MovieImage.swift b/BoxOffice/Model/Movie/Image/MovieImage.swift new file mode 100644 index 00000000..894a8167 --- /dev/null +++ b/BoxOffice/Model/Movie/Image/MovieImage.swift @@ -0,0 +1,11 @@ +// +// MovieImage.swift +// BoxOffice +// +// Created by 강창현 on 3/8/24. +// + +struct MovieImage: Codable { + let meta: Meta + let documents: [Document] +} diff --git a/BoxOffice/Model/MovieManager.swift b/BoxOffice/Model/MovieManager.swift index 0a6b73c8..033baee5 100644 --- a/BoxOffice/Model/MovieManager.swift +++ b/BoxOffice/Model/MovieManager.swift @@ -5,48 +5,67 @@ // Created by 강창현 on 2/13/24. // -import Foundation +import UIKit final class MovieManager { - var movieDetailData: MovieInfomationDetail? var dailyBoxOfficeData: BoxOfficeResult? + private let movieRepository = MovieRepository() + private var imageURL: String? } extension MovieManager { - func fetchBoxOfficeResultData( - date: String, - completion: @escaping (Result) -> Void - ) { - let apiService = APIService() - let urlString = MovieURL.makeDailyBoxOfficeURL(date: date) - - apiService.fetchData(urlString: urlString) { [weak self] (result: Result) in - switch result { - case .success(let movies): - self?.dailyBoxOfficeData = movies.boxOfficeResult - guard let result = self?.dailyBoxOfficeData else { return } - completion(.success(result)) - case .failure(let error): - completion(.failure(error)) - } + func setupBoxOfficeDetailData(for indexPath: Int) -> (movieName: String, movieCode: String)? { + guard + let movieName = dailyBoxOfficeData?.dailyBoxOfficeList[indexPath].name, + let movieCode = dailyBoxOfficeData?.dailyBoxOfficeList[indexPath].code + else { + return nil } + return (movieName, movieCode) } - func fetchMovieInfoResultData( - code: String, - completion: @escaping (Result) -> Void - ) { - let apiService = APIService() - let urlString = MovieURL.makeMovieInfomationDetailURL(code: code) - - apiService.fetchData(urlString: urlString) { (result: Result) in - switch result { - case .success(let movies): - completion(.success(movies)) - self.movieDetailData = movies - case .failure(let error): - completion(.failure(error)) - } + func fetchBoxOfficeResultData(date: String) async throws -> BoxOfficeResult { + let result = try await movieRepository.fetchBoxOfficeResultData(date: date) + switch result { + case .success(let movies): + let result = movies.boxOfficeResult + self.dailyBoxOfficeData = result + return result + case .failure(let error): + throw error + } + } + + func fetchMovieInfoResultData(code: String) async throws -> MovieInfo { + let result = try await movieRepository.fetchMovieInfoResultData(code: code) + switch result { + case .success(let movies): + let result = movies.movieInfoResult.movieInfo + return result + case .failure(let error): + throw error + } + } + + func fetchMoiveImageURL(movieName: String) async throws { + let result = try await movieRepository.fetchMoiveImageURL(movieName: movieName) + switch result { + case .success(let image): + let movieImageData = image.documents[0] + self.imageURL = movieImageData.imageURL + case .failure(let error): + throw error + } + } + + func fetchImageData() async throws -> Data? { + guard let imageURL else { return nil } + let result = try await movieRepository.fetchMovieImage(urlString: imageURL) + switch result { + case .success(let data): + return data + case .failure(let error): + throw error } } } diff --git a/BoxOffice/Model/MovieRepository.swift b/BoxOffice/Model/MovieRepository.swift new file mode 100644 index 00000000..d1e8e4b3 --- /dev/null +++ b/BoxOffice/Model/MovieRepository.swift @@ -0,0 +1,94 @@ +// +// MovieRepository.swift +// BoxOffice +// +// Created by 강창현 on 3/13/24. +// + +import Foundation + +struct MovieRepository { + private let apiService = APIService.init() + + func fetchBoxOfficeResultData(date: String) async throws -> (Result) { + guard + let urlRequest = NetworkURL.makeURLRequest(type: .boxOffice(.dailyBoxOffice(date: date))) + else { + return .failure(.invalidURLRequestError) + } + + let result = try await apiService.fetchData(with: urlRequest) + switch result { + case .success(let success): + return handleDecodedData(data: success) + case .failure(let failure): + return .failure(failure) + } + } + + func fetchMovieInfoResultData(code: String) async throws -> (Result) { + guard + let urlRequest = NetworkURL.makeURLRequest(type: .boxOffice(.movieDetail(code: code))) + else { + return .failure(.invalidURLRequestError) + } + + let result = try await apiService.fetchData(with: urlRequest) + switch result { + case .success(let success): + return handleDecodedData(data: success) + case .failure(let failure): + return .failure(failure) + } + } + + func fetchMoiveImageURL(movieName: String) async throws -> (Result) { + guard + let urlRequest = NetworkURL.makeURLRequest(type: .kakao(movieName: movieName)) + else { + return .failure(.invalidURLRequestError) + } + + let result = try await apiService.fetchData(with: urlRequest) + switch result { + case .success(let success): + return handleDecodedData(data: success) + case .failure(let failure): + return .failure(failure) + } + } + + func fetchMovieImage(urlString: String) async throws -> (Result) { + guard + let url = URL(string: urlString) + else { + return .failure(.invalidURLError) + } + + guard + let urlRequest = NetworkURL.makeURLRequest(type: .image(url: url)) + else { + return .failure(.invalidURLRequestError) + } + + let result = try await apiService.fetchData(with: urlRequest) + return result + } +} + +private extension MovieRepository { + func handleDecodedData(data: Data?) -> (Result) { + guard + let data = data + else { + return .failure(.noDataError) + } + + do { + let decodedData = try JSONDecoder().decode(T.self, from: data) + return .success(decodedData) + } catch { + return .failure(.decodingError) + } + } +} diff --git a/BoxOffice/Model/MovieURL.swift b/BoxOffice/Model/MovieURL.swift deleted file mode 100644 index 4669bf62..00000000 --- a/BoxOffice/Model/MovieURL.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// URL.swift -// BoxOffice -// -// Created by Matthew on 2/15/24. -// - -import Foundation - -struct MovieURL { - static func makeDailyBoxOfficeURL(date: String) -> String { - let key = Bundle.main.apiKey - var url: String { - return "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=\(String(describing: key))&targetDt=\(date)" - } - return url - } - - static func makeMovieInfomationDetailURL(code: String) -> String { - let key = Bundle.main.apiKey - var url: String { - return "https://kobis.or.kr/kobisopenapi/webservice/rest/movie/searchMovieInfo.json?key=\(String(describing: key))&movieCd=\(code)" - } - return url - } -} diff --git a/BoxOffice/Model/NetworkError.swift b/BoxOffice/Model/NetworkError.swift index b878463b..c06c6d88 100644 --- a/BoxOffice/Model/NetworkError.swift +++ b/BoxOffice/Model/NetworkError.swift @@ -9,6 +9,7 @@ import Foundation enum NetworkError: LocalizedError { case invalidURLError + case invalidURLRequestError case noDataError case requestFailError case invalidResponseError @@ -21,6 +22,8 @@ enum NetworkError: LocalizedError { switch self { case .invalidURLError: return "잘못된 URL 주소입니다." + case .invalidURLRequestError: + return "잘못된 URL 요청입니다." case .noDataError: return "데이터가 존재하지 않습니다." case .requestFailError: diff --git a/BoxOffice/Network/APIService.swift b/BoxOffice/Network/APIService.swift index be24a0df..9726fe89 100644 --- a/BoxOffice/Network/APIService.swift +++ b/BoxOffice/Network/APIService.swift @@ -8,92 +8,61 @@ import Foundation struct APIService { + typealias APIResult = (Result) private let session: URLSessionProtocol init(session: URLSessionProtocol = URLSession.shared) { self.session = session } - func fetchData(urlString: String, completion: @escaping (Result) -> Void) { + func fetchData(with urlRequest: URLRequest) async throws -> APIResult { guard - let url = URL(string: urlString) + let cachedResponse = URLCache.shared.cachedResponse(for: urlRequest) else { - completion(.failure(.invalidURLError)) - return + let (data, response) = try await self.session.requestData(with: urlRequest) + let cachedURLResponse = CachedURLResponse(response: response, data: data) + URLCache.shared.storeCachedResponse(cachedURLResponse, for: urlRequest) + return handleDataTaskCompletion(data: data, response: response) } - - self.session.dataTask(with: url) { data, response, error in - DispatchQueue.global().async { - self.handleDataTaskCompletion( - data: data, - response: response, - error: error, - completion: completion - ) - } - }.resume() + return handleDataTaskCompletion(data: cachedResponse.data, response: cachedResponse.response) } - - private func handleDataTaskCompletion( +} + +private extension APIService { + func handleDataTaskCompletion( data: Data?, - response: URLResponse?, - error: Error?, - completion: @escaping (Result) -> Void - ) { - guard - error == nil - else { - completion(.failure(.requestFailError)) - return - } - + response: URLResponse? + ) -> APIResult { guard let httpResponse = response as? HTTPURLResponse else { - completion(.failure(.invalidResponseError)) - return + return .failure(.invalidResponseError) } - self.handleHTTPResponse( + return self.handleHTTPResponse( data: data, - httpResponse: httpResponse, - completion: completion + httpResponse: httpResponse ) } - private func handleHTTPResponse( + func handleHTTPResponse( data: Data?, - httpResponse: HTTPURLResponse, - completion: @escaping (Result) -> Void - ) { + httpResponse: HTTPURLResponse + ) -> APIResult { + guard + let data = data + else { + return .failure(.noDataError) + } switch httpResponse.statusCode { case 300..<400: - completion(.failure(.redirectionError)) + return .failure(.redirectionError) case 400..<500: - completion(.failure(.clientError)) + return .failure(.clientError) case 500..<600: - completion(.failure(.serverError)) + return .failure(.serverError) default: - self.handleDecodedData(data: data, completion: completion) - } - } - - private func handleDecodedData( - data: Data?, - completion: @escaping (Result) -> Void - ) { - guard - let data = data - else { - completion(.failure(.noDataError)) - return - } - - do { - let decodedData = try JSONDecoder().decode(T.self, from: data) - completion(.success(decodedData)) - } catch { - completion(.failure(.decodingError)) + return .success(data) } } } diff --git a/BoxOffice/Network/Mock/MockURLSession.swift b/BoxOffice/Network/Mock/MockURLSession.swift index 524e11ac..19a91b15 100644 --- a/BoxOffice/Network/Mock/MockURLSession.swift +++ b/BoxOffice/Network/Mock/MockURLSession.swift @@ -8,7 +8,7 @@ import Foundation final class MockURLSession: URLSessionProtocol { - typealias Response = (data: Data?, urlResponse: URLResponse?, error: Error?) + typealias Response = (Data, URLResponse) private let response: Response @@ -16,16 +16,14 @@ final class MockURLSession: URLSessionProtocol { self.response = response } - func dataTask( - with url: URL, - completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void - ) -> URLSessionDataTaskProtocol { - return MockURLSessionDataTask(completionHandler: { - completionHandler( - self.response.data, - self.response.urlResponse, - self.response.error - )} - ) + func data( + for request: URLRequest, + delegate: (any URLSessionTaskDelegate)? = nil + ) async throws -> (Data, URLResponse) { + return response + } + + func requestData(with request: URLRequest) async throws -> (Data, URLResponse) { + return try await data(for: request) } } diff --git a/BoxOffice/Network/Mock/MockURLSessionDataTask.swift b/BoxOffice/Network/Mock/MockURLSessionDataTask.swift deleted file mode 100644 index 57bc4a5c..00000000 --- a/BoxOffice/Network/Mock/MockURLSessionDataTask.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// MockURLSessionDataTask.swift -// BoxOffice -// -// Created by 강창현 on 2/16/24. -// - -import Foundation - -final class MockURLSessionDataTask: URLSessionDataTaskProtocol { - private let completionHandler: () -> Void - - init(completionHandler: @escaping () -> Void) { - self.completionHandler = completionHandler - } - - func resume() { - completionHandler() - } -} diff --git a/BoxOffice/Network/NetworkURL.swift b/BoxOffice/Network/NetworkURL.swift new file mode 100644 index 00000000..1eff432e --- /dev/null +++ b/BoxOffice/Network/NetworkURL.swift @@ -0,0 +1,38 @@ +// +// URL.swift +// BoxOffice +// +// Created by Matthew on 2/15/24. +// + +import Foundation + +enum NetworkURL { + static func makeURLRequest(type: APIType, httpMethod: HttpMethod = .get) -> URLRequest? { + let urlComponents = makeURLComponents(type: type) + guard + let url = urlComponents.url + else { + return nil + } + + var request = URLRequest(url: url) + request.httpMethod = httpMethod.type + guard + let header = type.header + else { + return request + } + request.allHTTPHeaderFields = header + return request + } + + private static func makeURLComponents(type: APIType) -> URLComponents { + var urlComponents = URLComponents() + urlComponents.scheme = "https" + urlComponents.host = type.host + urlComponents.path = type.path + urlComponents.queryItems = type.queries + return urlComponents + } +} diff --git a/BoxOffice/Network/Protocol/URLSessionDataTaskProtocol.swift b/BoxOffice/Network/Protocol/URLSessionDataTaskProtocol.swift deleted file mode 100644 index ec14af79..00000000 --- a/BoxOffice/Network/Protocol/URLSessionDataTaskProtocol.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// URLSessionDataTaskProtocol.swift -// BoxOffice -// -// Created by Matthew on 2/20/24. -// - -import Foundation - -protocol URLSessionDataTaskProtocol { - func resume() -} diff --git a/BoxOffice/Network/Protocol/URLSessionProtocol.swift b/BoxOffice/Network/Protocol/URLSessionProtocol.swift index 8d08c8ae..145d423a 100644 --- a/BoxOffice/Network/Protocol/URLSessionProtocol.swift +++ b/BoxOffice/Network/Protocol/URLSessionProtocol.swift @@ -8,8 +8,7 @@ import Foundation protocol URLSessionProtocol { - func dataTask( - with url: URL, - completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void - ) -> URLSessionDataTaskProtocol + func requestData( + with request: URLRequest + ) async throws -> (Data, URLResponse) } diff --git a/BoxOffice/Network/URLRequest/APIType.swift b/BoxOffice/Network/URLRequest/APIType.swift new file mode 100644 index 00000000..e76bc88f --- /dev/null +++ b/BoxOffice/Network/URLRequest/APIType.swift @@ -0,0 +1,69 @@ +// +// APIType.swift +// BoxOffice +// +// Created by 강창현 on 3/19/24. +// + +import Foundation + +enum APIType { + case kakao(movieName: String) + case boxOffice(BoxOfficeType) + case image(url: URL) + + var host: String? { + switch self { + case .boxOffice: + return "www.kobis.or.kr" + case .kakao: + return "dapi.kakao.com" + case .image(let url): + return url.host + } + } + + var header: [String:String]? { + switch self { + case .kakao: + return ["Authorization": "KakaoAK \(Bundle.main.kakaoApiKey)"] + case .boxOffice: + return nil + case .image: + return nil + } + } + + var queries: [URLQueryItem]? { + switch self { + case .kakao(let movieName): + return [ + URLQueryItem(name: "query", value: "\(movieName) 영화포스터"), + URLQueryItem(name: "sort", value: "accuracy") + ] + case .boxOffice(let type): + return type.queries + case .image(let url): + guard + let urlComponents = URLComponents( + url: url, + resolvingAgainstBaseURL: false + ) + else { + return nil + } + return urlComponents.queryItems + } + } + + var path: String { + switch self { + case .kakao: + return "/v2/search/image" + case .boxOffice(let type): + return type.path + case .image(url: let url): + return url.path + } + } +} diff --git a/BoxOffice/Network/URLRequest/BoxOfficeType.swift b/BoxOffice/Network/URLRequest/BoxOfficeType.swift new file mode 100644 index 00000000..f04b7ba8 --- /dev/null +++ b/BoxOffice/Network/URLRequest/BoxOfficeType.swift @@ -0,0 +1,37 @@ +// +// BoxOfficeType.swift +// BoxOffice +// +// Created by 강창현 on 3/19/24. +// + +import Foundation + +enum BoxOfficeType { + case dailyBoxOffice(date: String) + case movieDetail(code: String) + + var queries: [URLQueryItem] { + switch self { + case .dailyBoxOffice(let date): + [ + URLQueryItem(name: "key", value: Bundle.main.movieApiKey), + URLQueryItem(name: "targetDt", value: date) + ] + case .movieDetail(let code): + [ + URLQueryItem(name: "key", value: Bundle.main.movieApiKey), + URLQueryItem(name: "movieCd", value: code) + ] + } + } + + var path: String { + switch self { + case .dailyBoxOffice: + "/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json" + case .movieDetail: + "/kobisopenapi/webservice/rest/movie/searchMovieInfo.json" + } + } +} diff --git a/BoxOffice/Network/URLRequest/HttpMethod.swift b/BoxOffice/Network/URLRequest/HttpMethod.swift new file mode 100644 index 00000000..b49227c8 --- /dev/null +++ b/BoxOffice/Network/URLRequest/HttpMethod.swift @@ -0,0 +1,29 @@ +// +// HttpMethodType.swift +// BoxOffice +// +// Created by 강창현 on 3/24/24. +// + +enum HttpMethod { + case get + case post + case put + case patch + case delete + + var type: String { + switch self { + case .get: + return "GET" + case .post: + return "POST" + case .put: + return "PUT" + case .patch: + return "PATCH" + case .delete: + return "DELETE" + } + } +} diff --git a/BoxOffice/Resource/Assets.xcassets/prepare.imageset/Contents.json b/BoxOffice/Resource/Assets.xcassets/prepare.imageset/Contents.json new file mode 100644 index 00000000..bb97684a --- /dev/null +++ b/BoxOffice/Resource/Assets.xcassets/prepare.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "prepare.jpeg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/BoxOffice/Resource/Assets.xcassets/prepare.imageset/prepare.jpeg b/BoxOffice/Resource/Assets.xcassets/prepare.imageset/prepare.jpeg new file mode 100644 index 00000000..37c4daa3 Binary files /dev/null and b/BoxOffice/Resource/Assets.xcassets/prepare.imageset/prepare.jpeg differ diff --git a/BoxOffice/Resource/Private.plist b/BoxOffice/Resource/Private.plist index caf75b1c..45950a08 100644 --- a/BoxOffice/Resource/Private.plist +++ b/BoxOffice/Resource/Private.plist @@ -4,5 +4,7 @@ API_KEY f5eef3421c602c6cb7ea224104795888 + Kakao_API_KEY + 3ac5fdb6d1defd44daa87c6b277fc587 diff --git a/BoxOffice/View/BoxOfficeCollectionView/BoxOfficeListView.swift b/BoxOffice/View/BoxOfficeCollectionView/BoxOfficeListView.swift index 3438edf9..3c1dd023 100644 --- a/BoxOffice/View/BoxOfficeCollectionView/BoxOfficeListView.swift +++ b/BoxOffice/View/BoxOfficeCollectionView/BoxOfficeListView.swift @@ -9,7 +9,6 @@ import UIKit final class BoxOfficeListView: UICollectionView { weak var boxOfficeListDelegate: BoxOfficeListViewDelegate? - let indicatorView = UIActivityIndicatorView() private lazy var refresher: UIRefreshControl = { let refreshControl = UIRefreshControl() let action = UIAction { _ in @@ -24,7 +23,6 @@ final class BoxOfficeListView: UICollectionView { collectionViewLayout layout: UICollectionViewLayout ) { super.init(frame: frame, collectionViewLayout: layout) - setupIndicatorView() collectionViewRegister() } @@ -42,12 +40,6 @@ private extension BoxOfficeListView { self.refreshControl = refresher } - func setupIndicatorView() { - self.backgroundView = indicatorView - self.isScrollEnabled = false - indicatorView.startAnimating() - } - func refreshCollectionView() { DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { self.boxOfficeListDelegate?.applyBoxOfficeListView() diff --git a/BoxOffice/View/BoxOfficeDetailView.swift b/BoxOffice/View/BoxOfficeDetailView.swift new file mode 100644 index 00000000..5ca5770b --- /dev/null +++ b/BoxOffice/View/BoxOfficeDetailView.swift @@ -0,0 +1,107 @@ +// +// BoxOfficeDetailView.swift +// BoxOffice +// +// Created by Matthew on 3/8/24. +// + +import UIKit + +final class BoxOfficeDetailView: UIScrollView { + + private let directorView = ReusedDetailStackView() + private let productYearView = ReusedDetailStackView() + private let openDateView = ReusedDetailStackView() + private let showTimeView = ReusedDetailStackView() + private let watchGradeView = ReusedDetailStackView() + private let nationsView = ReusedDetailStackView() + private let genresView = ReusedDetailStackView() + private let actorsView = ReusedDetailStackView() + + private lazy var movieInfoStackView: UIStackView = { + let stack = UIStackView(arrangedSubviews: [ + directorView, + productYearView, + openDateView, + showTimeView, + watchGradeView, + nationsView, + genresView, + actorsView + ]) + stack.axis = .vertical + stack.alignment = .leading + stack.distribution = .equalSpacing + stack.spacing = 5 + return stack + }() + + private lazy var innerView: UIView = { + let view = UIView() + view.backgroundColor = .white + return view + }() + + private let movieImageView: UIImageView = { + let view = UIImageView() + view.contentMode = .scaleAspectFit + view.image = UIImage(named: "prepare") + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupDetailView(data: MovieInfo) { + directorView.setupStackView(title: "감독", contents: data.directors.map { $0.peopleName }) + productYearView.setupStackView(title: "제작년도", contents: ["\(data.productYear)년"]) + openDateView.setupStackView(title: "개봉일", contents: [data.openDate.makeDateFormat]) + showTimeView.setupStackView(title: "상영시간", contents: ["\(data.showTime)분"]) + watchGradeView.setupStackView(title: "관람등급", contents: data.audits.map { $0.watchGradeName }) + nationsView.setupStackView(title: "제작국가", contents: data.nations.map { $0.nationName }) + genresView.setupStackView(title: "장르", contents: data.genres.map { $0.genreName }) + actorsView.setupStackView(title: "배우", contents: data.actors.map { $0.peopleName }) + } + + func setupImage(image: UIImage?) { + self.movieImageView.image = image + } +} + +private extension BoxOfficeDetailView { + func setupView() { + self.addSubview(innerView) + innerView.addSubview(movieImageView) + innerView.addSubview(movieInfoStackView) + } + + func setupConstraints() { + innerView.translatesAutoresizingMaskIntoConstraints = false + innerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true + innerView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true + innerView.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true + innerView.bottomAnchor.constraint(equalTo: self.contentLayoutGuide.bottomAnchor).isActive = true + + innerView.widthAnchor.constraint(equalTo: self.frameLayoutGuide.widthAnchor).isActive = true + innerView.heightAnchor.constraint(equalTo: self.contentLayoutGuide.heightAnchor).isActive = true + + movieImageView.translatesAutoresizingMaskIntoConstraints = false + movieImageView.topAnchor.constraint(equalTo: innerView.topAnchor, constant: 10).isActive = true + movieImageView.centerXAnchor.constraint(equalTo: innerView.centerXAnchor).isActive = true + movieImageView.widthAnchor.constraint(equalTo: innerView.widthAnchor).isActive = true + movieImageView.heightAnchor.constraint(equalTo: innerView.widthAnchor).isActive = true + + movieInfoStackView.translatesAutoresizingMaskIntoConstraints = false + movieInfoStackView.topAnchor.constraint(equalTo: movieImageView.bottomAnchor, constant: 10).isActive = true + movieInfoStackView.leadingAnchor.constraint(equalTo: innerView.leadingAnchor, constant: 10).isActive = true + movieInfoStackView.trailingAnchor.constraint(equalTo: innerView.trailingAnchor, constant: -10).isActive = true + movieInfoStackView.bottomAnchor.constraint(equalTo: innerView.bottomAnchor, constant: -10).isActive = true + } +} diff --git a/BoxOffice/View/LoadingIndicatorView.swift b/BoxOffice/View/LoadingIndicatorView.swift new file mode 100644 index 00000000..ed440f62 --- /dev/null +++ b/BoxOffice/View/LoadingIndicatorView.swift @@ -0,0 +1,37 @@ +// +// LoadingIndicatorView.swift +// BoxOffice +// +// Created by 강창현 on 3/14/24. +// + +import UIKit + +enum LoadingIndicatorView { + static func showLoading(in view: UIView) { + DispatchQueue.main.async { + let loadingIndicatorView: UIActivityIndicatorView + guard + let existedView = view.subviews.first(where: { $0 is UIActivityIndicatorView } ) as? UIActivityIndicatorView + else { + loadingIndicatorView = UIActivityIndicatorView() + loadingIndicatorView.frame = view.frame + loadingIndicatorView.center = CGPoint( + x: view.frame.width / 2, + y: view.frame.height / 2 + view.bounds.origin.y + ) + view.addSubview(loadingIndicatorView) + loadingIndicatorView.startAnimating() + return + } + loadingIndicatorView = existedView + loadingIndicatorView.startAnimating() + } + } + + static func hideLoading(in view: UIView) { + DispatchQueue.main.async { + view.subviews.filter({ $0 is UIActivityIndicatorView }).forEach { $0.removeFromSuperview() } + } + } +} diff --git a/BoxOffice/View/RankStateView.swift b/BoxOffice/View/RankStateView.swift index 132219f0..e012e7d5 100644 --- a/BoxOffice/View/RankStateView.swift +++ b/BoxOffice/View/RankStateView.swift @@ -83,7 +83,7 @@ private extension RankStateView { extension RankStateView { func configureRankState(rankState: String, rankChanged: String) { - guard + guard rankState != "NEW" else { self.rankStateLabel.textColor = .systemRed @@ -103,4 +103,4 @@ extension RankStateView { self.rankStateImage.image = rankImageView } } -} +} \ No newline at end of file diff --git a/BoxOffice/View/ReusedDetailStackView.swift b/BoxOffice/View/ReusedDetailStackView.swift new file mode 100644 index 00000000..43e60777 --- /dev/null +++ b/BoxOffice/View/ReusedDetailStackView.swift @@ -0,0 +1,69 @@ +// +// ReusedDetailStackView.swift +// BoxOffice +// +// Created by Matthew on 3/8/24. +// + +import UIKit + +final class ReusedDetailStackView: UIStackView { + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(for: .body, weight: .bold) + label.textAlignment = .center + return label + }() + + private lazy var contentLabel: UILabel = { + let label = UILabel() + label.font = UIFont.preferredFont(forTextStyle: .body) + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + setupConstraint() + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupStackView(title: String, contents: [String]) { + titleLabel.text = title + guard + !contents.isEmpty + else { + contentLabel.text = "-" + return + } + var textList: String = "" + for (index, text) in contents.enumerated() { + textList.append(text) + if index != contents.endIndex - 1 { + textList.append(", ") + } + } + contentLabel.text = textList + } +} + +private extension ReusedDetailStackView { + private func configure() { + self.addArrangedSubview(titleLabel) + self.addArrangedSubview(contentLabel) + self.axis = .horizontal + self.alignment = .leading + self.distribution = .fill + self.spacing = 5 + } + + private func setupConstraint() { + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 6).isActive = true + } +} diff --git a/BoxOfficeTests/BoxOfficeMockTests.swift b/BoxOfficeTests/BoxOfficeMockTests.swift index 9aae747e..941a512a 100644 --- a/BoxOfficeTests/BoxOfficeMockTests.swift +++ b/BoxOfficeTests/BoxOfficeMockTests.swift @@ -8,52 +8,43 @@ import XCTest @testable import BoxOffice -final class BoxOfficeMockTests: XCTestCase { - func test_MockURLSession의_응답코드가_400이면_clientError가_발생한다() { +final class BoxOfficeMockTests: XCTestCase { + func test_MockURLSession의_응답코드가_400이면_clientError가_발생한다() async throws { // given - let urlString = MovieURL.makeDailyBoxOfficeURL(date: Date.movieDateToString) + let urlString = "www.naver.com" let mockURLSession = makeMockURLSession(fileName: JSONFileName.boxOffice, url: urlString, statusCode: 400) let sut = setSUT(session: mockURLSession) + let request = URLRequest(url: URL(string: urlString)!) // when - let expectation = XCTestExpectation(description: "데이터 패치 중...") - - sut.fetchData(urlString: urlString) { (result: Result) in - switch result { - case .success(_): - // then - XCTFail() - case .failure(let error): - // then - XCTAssertEqual(error, NetworkError.clientError) - } - expectation.fulfill() + let result = try await sut.fetchData(with: request) + switch result { + case .success(_): + // then + XCTFail() + case .failure(let error): + // then + XCTAssertEqual(error, NetworkError.clientError) } - wait(for: [expectation], timeout: 5.0) } - func test_MockURLSession의_응답코드가_200이면_boxOfficeResult는_nil이_아니다() { + func test_MockURLSession의_응답코드가_200이면_boxOfficeResult는_nil이_아니다() async throws { // given - let date = "20240210" - let urlString = MovieURL.makeDailyBoxOfficeURL(date: date) + let urlString = "www.naver.com" let mockURLSession = makeMockURLSession(fileName: JSONFileName.boxOffice, url: urlString, statusCode: 200) let sut = setSUT(session: mockURLSession) + let request = URLRequest(url: URL(string: urlString)!) // when - let expectation = XCTestExpectation(description: "데이터 패치 중...") - - sut.fetchData(urlString: urlString) { (result: Result) in - switch result { - case .success(let movies): - // then - XCTAssertNotNil(movies) - case .failure(let error): - // then - XCTFail("데이터 파싱 에러 발생: \(error))") - } - expectation.fulfill() + let result = try await sut.fetchData(with: request) + switch result { + case .success(let data): + // then + XCTAssertNotNil(data) + case .failure(let error): + // then + XCTFail("데이터 파싱 에러 발생: \(error))") } - wait(for: [expectation], timeout: 5.0) } } @@ -64,14 +55,14 @@ private extension BoxOfficeMockTests { func makeMockURLSession(fileName: String, url: String, statusCode: Int) -> MockURLSession { var response: MockURLSession.Response { - let data: Data? = JSONLoader.load(fileName: fileName) + let data: Data = JSONLoader.load(fileName: fileName)! let response = HTTPURLResponse( url: URL(string: url)!, statusCode: statusCode, httpVersion: nil, headerFields: nil - ) - return (data, response, nil) + )! + return (data, response) } return MockURLSession(response: response) } diff --git a/BoxOfficeTests/BoxOfficeTests.swift b/BoxOfficeTests/BoxOfficeTests.swift index 404c8f69..936d07d8 100644 --- a/BoxOfficeTests/BoxOfficeTests.swift +++ b/BoxOfficeTests/BoxOfficeTests.swift @@ -22,110 +22,76 @@ final class BoxOfficeTests: XCTestCase { self.sut = nil } - func test_date가_20230101이고_데이터_파싱이_올바르게_됐을_때_fetchData는_nil이_아니다() { + func test_date가_20230101이고_데이터_파싱이_올바르게_됐을_때_fetchData는_nil이_아니다() async throws { // given let date = "20170319" - let urlString = MovieURL.makeDailyBoxOfficeURL(date: date) - - // when - let expectation = XCTestExpectation(description: "데이터 패치 중...") - - sut.fetchData(urlString: urlString) { (result: Result) in - switch result { - case .success(let movies): - // then - XCTAssertNotNil(movies) - case .failure(let error): - // then - XCTFail("데이터 파싱 에러 발생: \(error))") - } - expectation.fulfill() + guard + let urlString = NetworkURL.makeURLRequest( + type: .boxOffice, + path: .dailyBoxOffice, + queries: .boxOffice(.dailyBoxOffice(date: date)) + ) + else { + return } - wait(for: [expectation], timeout: 5.0) - } - - func test_date가_잘못된_타입으로_데이터_파싱_됐을_때_fetchMovie에서_decodingError발생() { - // given - let wrongDate = "iWantToGoHome" - let urlString = MovieURL.makeDailyBoxOfficeURL(date: wrongDate) // when - let expectation = XCTestExpectation(description: "데이터 패치 중...") - - sut.fetchData(urlString: urlString) { (result: Result) in - switch result { - case .success(let movies): - // then - XCTFail("데이터 파싱 성공: \(movies))") - case .failure(let error): - // then - XCTAssertEqual(error, NetworkError.decodingError) - } - expectation.fulfill() + let result = try await sut.fetchData(with: urlString) + switch result { + case .success(let movies): + // then + XCTAssertNotNil(movies) + case .failure(let error): + // then + XCTFail("데이터 파싱 에러 발생: \(error))") } - wait(for: [expectation], timeout: 5.0) } - func test_url이_잘못된_주소로_데이터_파싱_됐을_때_fetchMovie에서_invalidURLError_감지발생() { + func test_어제날짜_영화_데이터_파싱이_올바르게_됐을_때_fetchData에_boxOfficeResult는_nil이_아니다() async throws { // given - let url = "qqqq://안녕하세요.닷컴 " - - // when - let expectation = XCTestExpectation(description: "데이터 패치 중...") - - sut.fetchData(urlString: url) { (result: Result) in - switch result { - case .success(let movies): - // then - XCTFail("데이터 파싱 성공: \(movies))") - case .failure(let error): - // then - XCTAssertEqual(error, NetworkError.invalidURLError) - } - expectation.fulfill() + guard + let urlString = NetworkURL.makeURLRequest( + type: .boxOffice, + path: .dailyBoxOffice, + queries: .boxOffice(.dailyBoxOffice(date: Date.movieDateToString)) + ) + else { + return } - wait(for: [expectation], timeout: 5.0) - } - - func test_어제날짜_영화_데이터_파싱이_올바르게_됐을_때_fetchData에_boxOfficeResult는_nil이_아니다() { - // given - let urlString = MovieURL.makeDailyBoxOfficeURL(date: Date.movieDateToString) // when - let expectation = XCTestExpectation(description: "데이터 패치 중...") - - sut.fetchData(urlString: urlString) { (result: Result) in - switch result { - case .success(let movies): - // then - XCTAssertNotNil(movies) - case .failure(let error): - // then - XCTFail("데이터 파싱 에러 발생: \(error))") - } - expectation.fulfill() + let result = try await sut.fetchData(with: urlString) + switch result { + case .success(let movies): + // then + XCTAssertNotNil(movies) + case .failure(let error): + // then + XCTFail("데이터 파싱 에러 발생: \(error))") } - wait(for: [expectation], timeout: 5.0) } - func test_특정_영화_코드로_데이터_파싱이_올바르게_됐을_때_fetchData에_movieInfoResult는_nil이_아니다() { + func test_특정_영화_코드로_데이터_파싱이_올바르게_됐을_때_fetchData에_movieInfoResult는_nil이_아니다() async throws { // given - let urlString = MovieURL.makeMovieInfomationDetailURL(code: "20124079") + guard + let urlString = NetworkURL.makeURLRequest( + type: .boxOffice, + path: .movieDetail, + queries: .boxOffice(.movieDetail(code: "20230102")) + ) + else { + return + } // when - let expectation = XCTestExpectation(description: "데이터 패치 중...") - - sut.fetchData(urlString: urlString) { (result: Result) in - switch result { - case .success(let movies): - // then - XCTAssertNotNil(movies) - case .failure(let error): - // then - XCTFail("데이터 파싱 에러 발생: \(error))") - } - expectation.fulfill() + let result = try await sut.fetchData(with: urlString) + switch result { + case .success(let movies): + // then + XCTAssertNotNil(movies) + case .failure(let error): + // then + XCTFail("데이터 파싱 에러 발생: \(error))") } - wait(for: [expectation], timeout: 5.0) } }