diff --git a/BoxOffice.xcodeproj/project.pbxproj b/BoxOffice.xcodeproj/project.pbxproj index ab928a0b..950495ec 100644 --- a/BoxOffice.xcodeproj/project.pbxproj +++ b/BoxOffice.xcodeproj/project.pbxproj @@ -10,24 +10,27 @@ 63DF20EF2970E1A0005DF7D1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20EE2970E1A0005DF7D1 /* AppDelegate.swift */; }; 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20F02970E1A0005DF7D1 /* SceneDelegate.swift */; }; 63DF20F32970E1A0005DF7D1 /* MovieListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20F22970E1A0005DF7D1 /* MovieListViewController.swift */; }; - 63DF20F62970E1A0005DF7D1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F42970E1A0005DF7D1 /* Main.storyboard */; }; 63DF20F82970E1A1005DF7D1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */; }; - 63DF20FB2970E1A1005DF7D1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */; }; + 8494905D2BA9837B00628DBB /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494905C2BA9837B00628DBB /* String+.swift */; }; + 8494905F2BA9A8B600628DBB /* Date+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8494905E2BA9A8B600628DBB /* Date+.swift */; }; + 84B8A6F52B9AFC0800F33659 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8A6F42B9AFC0800F33659 /* HTTPMethod.swift */; }; + 84B8A6F72B9B26E200F33659 /* HTTPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B8A6F62B9B26E200F33659 /* HTTPError.swift */; }; 84CCF7E82B8F6BE400CA2EDF /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7E72B8F6BE400CA2EDF /* NetworkManager.swift */; }; 84CCF7EA2B8F6BF800CA2EDF /* JSONParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7E92B8F6BF800CA2EDF /* JSONParser.swift */; }; - 84CCF7EC2B8F6C0000CA2EDF /* NetworkAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7EB2B8F6C0000CA2EDF /* NetworkAPI.swift */; }; - 84CCF7EE2B8F6C0900CA2EDF /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7ED2B8F6C0900CA2EDF /* Errors.swift */; }; - 84CCF7F02B8F6C1300CA2EDF /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7EF2B8F6C1300CA2EDF /* Query.swift */; }; + 84CCF7EC2B8F6C0000CA2EDF /* APIURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7EB2B8F6C0000CA2EDF /* APIURLBuilder.swift */; }; + 84CCF7EE2B8F6C0900CA2EDF /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7ED2B8F6C0900CA2EDF /* NetworkError.swift */; }; + 84CCF7F02B8F6C1300CA2EDF /* APIURLCompnents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7EF2B8F6C1300CA2EDF /* APIURLCompnents.swift */; }; 84CCF7F42B8F6C3800CA2EDF /* DailyBoxOfficeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7F32B8F6C3800CA2EDF /* DailyBoxOfficeDTO.swift */; }; 84CCF7F62B8F6C4400CA2EDF /* MovieDetailDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7F52B8F6C4400CA2EDF /* MovieDetailDTO.swift */; }; - 84CCF7F92B8F6C5F00CA2EDF /* Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7F82B8F6C5F00CA2EDF /* Actor.swift */; }; - 84CCF7FB2B8F6C6900CA2EDF /* Audit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7FA2B8F6C6900CA2EDF /* Audit.swift */; }; - 84CCF7FD2B8F6C7700CA2EDF /* Company.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7FC2B8F6C7700CA2EDF /* Company.swift */; }; - 84CCF7FF2B8F6C8900CA2EDF /* Director.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7FE2B8F6C8900CA2EDF /* Director.swift */; }; - 84CCF8012B8F6C9700CA2EDF /* Genre.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF8002B8F6C9700CA2EDF /* Genre.swift */; }; - 84CCF8032B8F6CA000CA2EDF /* Nation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF8022B8F6CA000CA2EDF /* Nation.swift */; }; - 84CCF8052B8F6CAE00CA2EDF /* showType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF8042B8F6CAE00CA2EDF /* showType.swift */; }; - 84CCF8072B8F6CBD00CA2EDF /* Staff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF8062B8F6CBD00CA2EDF /* Staff.swift */; }; + 84CCF7F92B8F6C5F00CA2EDF /* ActorDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7F82B8F6C5F00CA2EDF /* ActorDTO.swift */; }; + 84CCF7FB2B8F6C6900CA2EDF /* AuditDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7FA2B8F6C6900CA2EDF /* AuditDTO.swift */; }; + 84CCF7FD2B8F6C7700CA2EDF /* CompanyDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7FC2B8F6C7700CA2EDF /* CompanyDTO.swift */; }; + 84CCF7FF2B8F6C8900CA2EDF /* DirectorDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF7FE2B8F6C8900CA2EDF /* DirectorDTO.swift */; }; + 84CCF8012B8F6C9700CA2EDF /* GenreDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF8002B8F6C9700CA2EDF /* GenreDTO.swift */; }; + 84CCF8032B8F6CA000CA2EDF /* NationDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF8022B8F6CA000CA2EDF /* NationDTO.swift */; }; + 84CCF8052B8F6CAE00CA2EDF /* ShowTypeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF8042B8F6CAE00CA2EDF /* ShowTypeDTO.swift */; }; + 84CCF8072B8F6CBD00CA2EDF /* StaffDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CCF8062B8F6CBD00CA2EDF /* StaffDTO.swift */; }; + 84DFC37D2BA29AEC00E1F04D /* MovieListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DFC37C2BA29AEC00E1F04D /* MovieListCollectionViewCell.swift */; }; E4C414782B7DEE2100BB8860 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = E4C414772B7DEE2100BB8860 /* .swiftlint.yml */; }; /* End PBXBuildFile section */ @@ -45,27 +48,30 @@ 63DF20EE2970E1A0005DF7D1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 63DF20F02970E1A0005DF7D1 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 63DF20F22970E1A0005DF7D1 /* MovieListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListViewController.swift; sourceTree = ""; }; - 63DF20F52970E1A0005DF7D1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 63DF20FA2970E1A1005DF7D1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 63DF20FC2970E1A1005DF7D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 84CCF7E42B8F6B5F00CA2EDF /* BoxOffice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; name = BoxOffice.app; path = "/Users/admin/Desktop/sesac/프로젝트/ios-box-office/build/Debug-iphoneos/BoxOffice.app"; sourceTree = ""; }; - 84CCF7E52B8F6B5F00CA2EDF /* BoxOfficeUnitTest.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = BoxOfficeUnitTest.xctest; path = "/Users/admin/Desktop/sesac/프로젝트/ios-box-office/build/Debug/BoxOfficeUnitTest.xctest"; sourceTree = ""; }; + 8494905C2BA9837B00628DBB /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; + 8494905E2BA9A8B600628DBB /* Date+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+.swift"; sourceTree = ""; }; + 84B8A6DD2B99BA2200F33659 /* BoxOffice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BoxOffice.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 84B8A6DE2B99BA2200F33659 /* BoxOfficeUnitTest.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BoxOfficeUnitTest.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 84B8A6F42B9AFC0800F33659 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; + 84B8A6F62B9B26E200F33659 /* HTTPError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPError.swift; sourceTree = ""; }; 84CCF7E72B8F6BE400CA2EDF /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = SOURCE_ROOT; }; 84CCF7E92B8F6BF800CA2EDF /* JSONParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONParser.swift; sourceTree = ""; }; - 84CCF7EB2B8F6C0000CA2EDF /* NetworkAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAPI.swift; sourceTree = ""; }; - 84CCF7ED2B8F6C0900CA2EDF /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; - 84CCF7EF2B8F6C1300CA2EDF /* Query.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Query.swift; sourceTree = ""; }; + 84CCF7EB2B8F6C0000CA2EDF /* APIURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIURLBuilder.swift; sourceTree = ""; }; + 84CCF7ED2B8F6C0900CA2EDF /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + 84CCF7EF2B8F6C1300CA2EDF /* APIURLCompnents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIURLCompnents.swift; sourceTree = ""; }; 84CCF7F32B8F6C3800CA2EDF /* DailyBoxOfficeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyBoxOfficeDTO.swift; sourceTree = ""; }; 84CCF7F52B8F6C4400CA2EDF /* MovieDetailDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailDTO.swift; sourceTree = ""; }; - 84CCF7F82B8F6C5F00CA2EDF /* Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actor.swift; sourceTree = ""; }; - 84CCF7FA2B8F6C6900CA2EDF /* Audit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Audit.swift; sourceTree = ""; }; - 84CCF7FC2B8F6C7700CA2EDF /* Company.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Company.swift; sourceTree = ""; }; - 84CCF7FE2B8F6C8900CA2EDF /* Director.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Director.swift; sourceTree = ""; }; - 84CCF8002B8F6C9700CA2EDF /* Genre.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Genre.swift; sourceTree = ""; }; - 84CCF8022B8F6CA000CA2EDF /* Nation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nation.swift; sourceTree = ""; }; - 84CCF8042B8F6CAE00CA2EDF /* showType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = showType.swift; sourceTree = ""; }; - 84CCF8062B8F6CBD00CA2EDF /* Staff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Staff.swift; sourceTree = ""; }; + 84CCF7F82B8F6C5F00CA2EDF /* ActorDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActorDTO.swift; sourceTree = ""; }; + 84CCF7FA2B8F6C6900CA2EDF /* AuditDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuditDTO.swift; sourceTree = ""; }; + 84CCF7FC2B8F6C7700CA2EDF /* CompanyDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanyDTO.swift; sourceTree = ""; }; + 84CCF7FE2B8F6C8900CA2EDF /* DirectorDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectorDTO.swift; sourceTree = ""; }; + 84CCF8002B8F6C9700CA2EDF /* GenreDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenreDTO.swift; sourceTree = ""; }; + 84CCF8022B8F6CA000CA2EDF /* NationDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NationDTO.swift; sourceTree = ""; }; + 84CCF8042B8F6CAE00CA2EDF /* ShowTypeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowTypeDTO.swift; sourceTree = ""; }; + 84CCF8062B8F6CBD00CA2EDF /* StaffDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaffDTO.swift; sourceTree = ""; }; + 84DFC37C2BA29AEC00E1F04D /* MovieListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieListCollectionViewCell.swift; sourceTree = ""; }; E4C414772B7DEE2100BB8860 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; /* End PBXFileReference section */ @@ -92,6 +98,8 @@ children = ( E4C414772B7DEE2100BB8860 /* .swiftlint.yml */, 63DF20ED2970E1A0005DF7D1 /* BoxOffice */, + 84B8A6DD2B99BA2200F33659 /* BoxOffice.app */, + 84B8A6DE2B99BA2200F33659 /* BoxOfficeUnitTest.xctest */, ); sourceTree = ""; }; @@ -100,24 +108,51 @@ children = ( 84CCF8082B8F6CE000CA2EDF /* App */, 63DF20F22970E1A0005DF7D1 /* MovieListViewController.swift */, + 84DFC37C2BA29AEC00E1F04D /* MovieListCollectionViewCell.swift */, + 849490602BA9FFD500628DBB /* Extension */, 84CCF7F12B8F6C2700CA2EDF /* Model */, 84CCF7E62B8F6BDA00CA2EDF /* Network */, - 63DF20F42970E1A0005DF7D1 /* Main.storyboard */, 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */, - 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */, 63DF20FC2970E1A1005DF7D1 /* Info.plist */, ); path = BoxOffice; sourceTree = ""; }; + 849490602BA9FFD500628DBB /* Extension */ = { + isa = PBXGroup; + children = ( + 8494905C2BA9837B00628DBB /* String+.swift */, + 8494905E2BA9A8B600628DBB /* Date+.swift */, + ); + path = Extension; + sourceTree = ""; + }; + 84B8A6F82B9B392000F33659 /* Errors */ = { + isa = PBXGroup; + children = ( + 84B8A6F62B9B26E200F33659 /* HTTPError.swift */, + 84CCF7ED2B8F6C0900CA2EDF /* NetworkError.swift */, + ); + path = Errors; + sourceTree = ""; + }; + 84B8A6F92B9B3CE200F33659 /* APIManager */ = { + isa = PBXGroup; + children = ( + 84CCF7EB2B8F6C0000CA2EDF /* APIURLBuilder.swift */, + 84CCF7EF2B8F6C1300CA2EDF /* APIURLCompnents.swift */, + ); + path = APIManager; + sourceTree = ""; + }; 84CCF7E62B8F6BDA00CA2EDF /* Network */ = { isa = PBXGroup; children = ( 84CCF7E72B8F6BE400CA2EDF /* NetworkManager.swift */, + 84B8A6F92B9B3CE200F33659 /* APIManager */, 84CCF7E92B8F6BF800CA2EDF /* JSONParser.swift */, - 84CCF7EB2B8F6C0000CA2EDF /* NetworkAPI.swift */, - 84CCF7ED2B8F6C0900CA2EDF /* Errors.swift */, - 84CCF7EF2B8F6C1300CA2EDF /* Query.swift */, + 84B8A6F42B9AFC0800F33659 /* HTTPMethod.swift */, + 84B8A6F82B9B392000F33659 /* Errors */, ); path = Network; sourceTree = ""; @@ -143,14 +178,14 @@ 84CCF7F72B8F6C5100CA2EDF /* MovieInfoSubDetail */ = { isa = PBXGroup; children = ( - 84CCF7F82B8F6C5F00CA2EDF /* Actor.swift */, - 84CCF7FA2B8F6C6900CA2EDF /* Audit.swift */, - 84CCF7FC2B8F6C7700CA2EDF /* Company.swift */, - 84CCF7FE2B8F6C8900CA2EDF /* Director.swift */, - 84CCF8042B8F6CAE00CA2EDF /* showType.swift */, - 84CCF8062B8F6CBD00CA2EDF /* Staff.swift */, - 84CCF8002B8F6C9700CA2EDF /* Genre.swift */, - 84CCF8022B8F6CA000CA2EDF /* Nation.swift */, + 84CCF7F82B8F6C5F00CA2EDF /* ActorDTO.swift */, + 84CCF7FA2B8F6C6900CA2EDF /* AuditDTO.swift */, + 84CCF7FC2B8F6C7700CA2EDF /* CompanyDTO.swift */, + 84CCF7FE2B8F6C8900CA2EDF /* DirectorDTO.swift */, + 84CCF8042B8F6CAE00CA2EDF /* ShowTypeDTO.swift */, + 84CCF8062B8F6CBD00CA2EDF /* StaffDTO.swift */, + 84CCF8002B8F6C9700CA2EDF /* GenreDTO.swift */, + 84CCF8022B8F6CA000CA2EDF /* NationDTO.swift */, ); path = MovieInfoSubDetail; sourceTree = ""; @@ -182,7 +217,7 @@ ); name = BoxOffice; productName = BoxOffice; - productReference = 84CCF7E42B8F6B5F00CA2EDF /* BoxOffice.app */; + productReference = 84B8A6DD2B99BA2200F33659 /* BoxOffice.app */; productType = "com.apple.product-type.application"; }; E4C414812B7E021400BB8860 /* BoxOfficeUnitTest */ = { @@ -200,7 +235,7 @@ ); name = BoxOfficeUnitTest; productName = BoxOfficeUnitTest; - productReference = 84CCF7E52B8F6B5F00CA2EDF /* BoxOfficeUnitTest.xctest */; + productReference = 84B8A6DE2B99BA2200F33659 /* BoxOfficeUnitTest.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ @@ -247,9 +282,7 @@ buildActionMask = 2147483647; files = ( E4C414782B7DEE2100BB8860 /* .swiftlint.yml in Resources */, - 63DF20FB2970E1A1005DF7D1 /* LaunchScreen.storyboard in Resources */, 63DF20F82970E1A1005DF7D1 /* Assets.xcassets in Resources */, - 63DF20F62970E1A0005DF7D1 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -289,22 +322,27 @@ buildActionMask = 2147483647; files = ( 63DF20F32970E1A0005DF7D1 /* MovieListViewController.swift in Sources */, - 84CCF7F02B8F6C1300CA2EDF /* Query.swift in Sources */, + 84CCF7F02B8F6C1300CA2EDF /* APIURLCompnents.swift in Sources */, 63DF20EF2970E1A0005DF7D1 /* AppDelegate.swift in Sources */, - 84CCF7F92B8F6C5F00CA2EDF /* Actor.swift in Sources */, + 84CCF7F92B8F6C5F00CA2EDF /* ActorDTO.swift in Sources */, 84CCF7F62B8F6C4400CA2EDF /* MovieDetailDTO.swift in Sources */, 84CCF7EA2B8F6BF800CA2EDF /* JSONParser.swift in Sources */, - 84CCF8032B8F6CA000CA2EDF /* Nation.swift in Sources */, - 84CCF7EC2B8F6C0000CA2EDF /* NetworkAPI.swift in Sources */, - 84CCF8012B8F6C9700CA2EDF /* Genre.swift in Sources */, - 84CCF7FF2B8F6C8900CA2EDF /* Director.swift in Sources */, + 84CCF8032B8F6CA000CA2EDF /* NationDTO.swift in Sources */, + 84DFC37D2BA29AEC00E1F04D /* MovieListCollectionViewCell.swift in Sources */, + 84B8A6F52B9AFC0800F33659 /* HTTPMethod.swift in Sources */, + 8494905D2BA9837B00628DBB /* String+.swift in Sources */, + 84CCF7EC2B8F6C0000CA2EDF /* APIURLBuilder.swift in Sources */, + 84CCF8012B8F6C9700CA2EDF /* GenreDTO.swift in Sources */, + 84CCF7FF2B8F6C8900CA2EDF /* DirectorDTO.swift in Sources */, 84CCF7F42B8F6C3800CA2EDF /* DailyBoxOfficeDTO.swift in Sources */, - 84CCF7FB2B8F6C6900CA2EDF /* Audit.swift in Sources */, - 84CCF7EE2B8F6C0900CA2EDF /* Errors.swift in Sources */, - 84CCF8072B8F6CBD00CA2EDF /* Staff.swift in Sources */, + 8494905F2BA9A8B600628DBB /* Date+.swift in Sources */, + 84CCF7FB2B8F6C6900CA2EDF /* AuditDTO.swift in Sources */, + 84CCF7EE2B8F6C0900CA2EDF /* NetworkError.swift in Sources */, + 84CCF8072B8F6CBD00CA2EDF /* StaffDTO.swift in Sources */, + 84B8A6F72B9B26E200F33659 /* HTTPError.swift in Sources */, 84CCF7E82B8F6BE400CA2EDF /* NetworkManager.swift in Sources */, - 84CCF8052B8F6CAE00CA2EDF /* showType.swift in Sources */, - 84CCF7FD2B8F6C7700CA2EDF /* Company.swift in Sources */, + 84CCF8052B8F6CAE00CA2EDF /* ShowTypeDTO.swift in Sources */, + 84CCF7FD2B8F6C7700CA2EDF /* CompanyDTO.swift in Sources */, 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -326,25 +364,6 @@ }; /* End PBXTargetDependency section */ -/* Begin PBXVariantGroup section */ - 63DF20F42970E1A0005DF7D1 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 63DF20F52970E1A0005DF7D1 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 63DF20FA2970E1A1005DF7D1 /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ 63DF20FD2970E1A1005DF7D1 /* Debug */ = { isa = XCBuildConfiguration; @@ -471,7 +490,6 @@ INFOPLIST_FILE = BoxOffice/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -499,7 +517,6 @@ INFOPLIST_FILE = BoxOffice/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; - INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; diff --git a/BoxOffice/App/AppDelegate.swift b/BoxOffice/App/AppDelegate.swift index f321c71b..382d84c5 100644 --- a/BoxOffice/App/AppDelegate.swift +++ b/BoxOffice/App/AppDelegate.swift @@ -10,15 +10,15 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + let appearance = UINavigationBarAppearance() + appearance.configureWithOpaqueBackground() + UINavigationBar.appearance().standardAppearance = appearance return true } - + // MARK: UISceneSession Lifecycle - + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. diff --git a/BoxOffice/App/SceneDelegate.swift b/BoxOffice/App/SceneDelegate.swift index 1ac597ee..f287aa56 100644 --- a/BoxOffice/App/SceneDelegate.swift +++ b/BoxOffice/App/SceneDelegate.swift @@ -8,43 +8,34 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - + var window: UIWindow? - - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). - guard let _ = (scene as? UIWindowScene) else { return } + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + window = UIWindow(windowScene: windowScene) + let mainViewController = MovieListViewController() + let navigationController = UINavigationController(rootViewController: mainViewController) + window?.rootViewController = navigationController + navigationController.view.backgroundColor = .white + window?.makeKeyAndVisible() } - + func sceneDidDisconnect(_ scene: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). } - + func sceneDidBecomeActive(_ scene: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. } func sceneWillResignActive(_ scene: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). } func sceneWillEnterForeground(_ scene: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. } func sceneDidEnterBackground(_ scene: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. } diff --git a/BoxOffice/Base.lproj/LaunchScreen.storyboard b/BoxOffice/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e9329..00000000 --- a/BoxOffice/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BoxOffice/Base.lproj/Main.storyboard b/BoxOffice/Base.lproj/Main.storyboard deleted file mode 100644 index 87ec70b5..00000000 --- a/BoxOffice/Base.lproj/Main.storyboard +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BoxOffice/Extension/Date+.swift b/BoxOffice/Extension/Date+.swift new file mode 100644 index 00000000..b7eec1d8 --- /dev/null +++ b/BoxOffice/Extension/Date+.swift @@ -0,0 +1,29 @@ +// +// Date+.swift +// BoxOffice +// +// Created by dopamint on 3/19/24. +// + +import Foundation + +extension Date { + static let yyyyMMdd = "yyyyMMdd" + static let yyyyMMddHyphen = "yyyy-MM-dd" + + func yesterday(format: String) -> String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = format + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + + let today = Date() + let timezone = TimeZone.autoupdatingCurrent + let secondsFromGMT = timezone.secondsFromGMT(for: today) + let localizedDate = today.addingTimeInterval(TimeInterval(secondsFromGMT)) + let yesterday = Calendar(identifier: .gregorian).date(byAdding: .day, value: -1, to: localizedDate) + guard let dateString = dateFormatter.string(for: yesterday) else { + return "알 수 없는 날짜" + } + return dateString + } +} diff --git a/BoxOffice/Extension/String+.swift b/BoxOffice/Extension/String+.swift new file mode 100644 index 00000000..2c8c56e0 --- /dev/null +++ b/BoxOffice/Extension/String+.swift @@ -0,0 +1,21 @@ +// +// String+.swift +// BoxOffice +// +// Created by dopamint on 3/19/24. +// + +import Foundation + +extension String { + func formatNumberString() -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + guard let number = Double(self), + let result = formatter.string(from: NSNumber(value: number)) + else { + return "" + } + return result + } +} diff --git a/BoxOffice/Info.plist b/BoxOffice/Info.plist index dd3c9afd..0eb786dc 100644 --- a/BoxOffice/Info.plist +++ b/BoxOffice/Info.plist @@ -15,8 +15,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/BoxOffice/Model/DTO/DailyBoxOfficeDTO.swift b/BoxOffice/Model/DTO/DailyBoxOfficeDTO.swift index b9e87e55..4ad7bbf8 100644 --- a/BoxOffice/Model/DTO/DailyBoxOfficeDTO.swift +++ b/BoxOffice/Model/DTO/DailyBoxOfficeDTO.swift @@ -7,20 +7,19 @@ import Foundation -struct DailyBoxOfficeResultDTO: Decodable { +struct DailyBoxOfficeDTO: Decodable { let boxOfficeResult: BoxOfficeDTO } -extension DailyBoxOfficeResultDTO { +extension DailyBoxOfficeDTO { struct BoxOfficeDTO: Decodable { let boxOfficeType: String let dateRange: String let dailyBoxOfficeList: [MovieInfo]? } - } -extension DailyBoxOfficeResultDTO.BoxOfficeDTO { +extension DailyBoxOfficeDTO.BoxOfficeDTO { enum CodingKeys: String, CodingKey { case dateRange = "showRange" case boxOfficeType = "boxofficeType" @@ -28,7 +27,7 @@ extension DailyBoxOfficeResultDTO.BoxOfficeDTO { } } -extension DailyBoxOfficeResultDTO.BoxOfficeDTO { +extension DailyBoxOfficeDTO.BoxOfficeDTO { struct MovieInfo: Decodable { let rankNumber: String let rank: String @@ -51,7 +50,7 @@ extension DailyBoxOfficeResultDTO.BoxOfficeDTO { } } -extension DailyBoxOfficeResultDTO.BoxOfficeDTO.MovieInfo { +extension DailyBoxOfficeDTO.BoxOfficeDTO.MovieInfo { enum CodingKeys: String, CodingKey { case rankNumber = "rnum" case rank diff --git a/BoxOffice/Model/DTO/MovieDetailDTO.swift b/BoxOffice/Model/DTO/MovieDetailDTO.swift index c9f28201..d03a1677 100644 --- a/BoxOffice/Model/DTO/MovieDetailDTO.swift +++ b/BoxOffice/Model/DTO/MovieDetailDTO.swift @@ -29,14 +29,14 @@ extension MovieDetailDTO.MovieInfoDTO { let openDate: String let productionStatusName: String let typeName: String - let nations: [Nation] - let genres: [Genre] - let directors: [Director] - let actors: [Actor] - let showTypes: [ShowType] - let companys: [Company] - let audits: [Audit] - let staffs: [Staff] + let nations: [NationDTO] + let genres: [GenreDTO] + let directors: [DirectorDTO] + let actors: [ActorDTO] + let showTypes: [ShowTypeDTO] + let companys: [CompanyDTO] + let audits: [AuditDTO] + let staffs: [StaffDTO] } } diff --git a/BoxOffice/Model/DTO/MovieInfoSubDetail/Actor.swift b/BoxOffice/Model/DTO/MovieInfoSubDetail/ActorDTO.swift similarity index 92% rename from BoxOffice/Model/DTO/MovieInfoSubDetail/Actor.swift rename to BoxOffice/Model/DTO/MovieInfoSubDetail/ActorDTO.swift index 00f4cf31..67c1a9a7 100644 --- a/BoxOffice/Model/DTO/MovieInfoSubDetail/Actor.swift +++ b/BoxOffice/Model/DTO/MovieInfoSubDetail/ActorDTO.swift @@ -5,7 +5,7 @@ // Created by dopamint on 2/18/24. // -struct Actor: Decodable { +struct ActorDTO: Decodable { let peopleName: String let peopleNameInEnglish: String let cast: String diff --git a/BoxOffice/Model/DTO/MovieInfoSubDetail/Audit.swift b/BoxOffice/Model/DTO/MovieInfoSubDetail/AuditDTO.swift similarity index 90% rename from BoxOffice/Model/DTO/MovieInfoSubDetail/Audit.swift rename to BoxOffice/Model/DTO/MovieInfoSubDetail/AuditDTO.swift index 3d892f26..e186f1fe 100644 --- a/BoxOffice/Model/DTO/MovieInfoSubDetail/Audit.swift +++ b/BoxOffice/Model/DTO/MovieInfoSubDetail/AuditDTO.swift @@ -5,7 +5,7 @@ // Created by dopamint on 2/18/24. // -struct Audit: Decodable { +struct AuditDTO: Decodable { let auditNumber: String let watchGradeName: String diff --git a/BoxOffice/Model/DTO/MovieInfoSubDetail/Company.swift b/BoxOffice/Model/DTO/MovieInfoSubDetail/CompanyDTO.swift similarity index 93% rename from BoxOffice/Model/DTO/MovieInfoSubDetail/Company.swift rename to BoxOffice/Model/DTO/MovieInfoSubDetail/CompanyDTO.swift index 8ebee974..8c23eab9 100644 --- a/BoxOffice/Model/DTO/MovieInfoSubDetail/Company.swift +++ b/BoxOffice/Model/DTO/MovieInfoSubDetail/CompanyDTO.swift @@ -5,7 +5,7 @@ // Created by dopamint on 2/18/24. // -struct Company: Decodable { +struct CompanyDTO: Decodable { let companyCode: String let companyName: String let companyNameInEnglish: String diff --git a/BoxOffice/Model/DTO/MovieInfoSubDetail/Director.swift b/BoxOffice/Model/DTO/MovieInfoSubDetail/DirectorDTO.swift similarity index 89% rename from BoxOffice/Model/DTO/MovieInfoSubDetail/Director.swift rename to BoxOffice/Model/DTO/MovieInfoSubDetail/DirectorDTO.swift index 3df8f904..17718fbd 100644 --- a/BoxOffice/Model/DTO/MovieInfoSubDetail/Director.swift +++ b/BoxOffice/Model/DTO/MovieInfoSubDetail/DirectorDTO.swift @@ -5,7 +5,7 @@ // Created by dopamint on 2/18/24. // -struct Director: Decodable { +struct DirectorDTO: Decodable { let peopleName: String let peopleNameInEnglish: String diff --git a/BoxOffice/Model/DTO/MovieInfoSubDetail/Genre.swift b/BoxOffice/Model/DTO/MovieInfoSubDetail/GenreDTO.swift similarity index 86% rename from BoxOffice/Model/DTO/MovieInfoSubDetail/Genre.swift rename to BoxOffice/Model/DTO/MovieInfoSubDetail/GenreDTO.swift index 35ec7f4a..e3d99b58 100644 --- a/BoxOffice/Model/DTO/MovieInfoSubDetail/Genre.swift +++ b/BoxOffice/Model/DTO/MovieInfoSubDetail/GenreDTO.swift @@ -5,7 +5,7 @@ // Created by dopamint on 2/18/24. // -struct Genre: Decodable { +struct GenreDTO: Decodable { let genreName: String enum CodingKeys: String, CodingKey { diff --git a/BoxOffice/Model/DTO/MovieInfoSubDetail/Nation.swift b/BoxOffice/Model/DTO/MovieInfoSubDetail/NationDTO.swift similarity index 86% rename from BoxOffice/Model/DTO/MovieInfoSubDetail/Nation.swift rename to BoxOffice/Model/DTO/MovieInfoSubDetail/NationDTO.swift index c24e55d0..57ddda5e 100644 --- a/BoxOffice/Model/DTO/MovieInfoSubDetail/Nation.swift +++ b/BoxOffice/Model/DTO/MovieInfoSubDetail/NationDTO.swift @@ -5,7 +5,7 @@ // Created by dopamint on 2/18/24. // -struct Nation: Decodable { +struct NationDTO: Decodable { let nationName: String enum CodingKeys: String, CodingKey { diff --git a/BoxOffice/Model/DTO/MovieInfoSubDetail/showType.swift b/BoxOffice/Model/DTO/MovieInfoSubDetail/ShowTypeDTO.swift similarity index 90% rename from BoxOffice/Model/DTO/MovieInfoSubDetail/showType.swift rename to BoxOffice/Model/DTO/MovieInfoSubDetail/ShowTypeDTO.swift index def9a0de..5429d365 100644 --- a/BoxOffice/Model/DTO/MovieInfoSubDetail/showType.swift +++ b/BoxOffice/Model/DTO/MovieInfoSubDetail/ShowTypeDTO.swift @@ -5,7 +5,7 @@ // Created by dopamint on 2/18/24. // -struct ShowType: Decodable { +struct ShowTypeDTO: Decodable { let showTypeGroupName: String let showTypeName: String diff --git a/BoxOffice/Model/DTO/MovieInfoSubDetail/Staff.swift b/BoxOffice/Model/DTO/MovieInfoSubDetail/StaffDTO.swift similarity index 92% rename from BoxOffice/Model/DTO/MovieInfoSubDetail/Staff.swift rename to BoxOffice/Model/DTO/MovieInfoSubDetail/StaffDTO.swift index aa407cb7..5adf9dd0 100644 --- a/BoxOffice/Model/DTO/MovieInfoSubDetail/Staff.swift +++ b/BoxOffice/Model/DTO/MovieInfoSubDetail/StaffDTO.swift @@ -5,7 +5,7 @@ // Created by dopamint on 2/18/24. // -struct Staff: Decodable { +struct StaffDTO: Decodable { let peopleName: String let peopleNameInEnglish: String let staffRoleName: String diff --git a/BoxOffice/MovieListCollectionViewCell.swift b/BoxOffice/MovieListCollectionViewCell.swift new file mode 100644 index 00000000..05779f23 --- /dev/null +++ b/BoxOffice/MovieListCollectionViewCell.swift @@ -0,0 +1,153 @@ +// +// MovieListCollectionViewCell.swift +// BoxOffice +// +// Created by dopamint on 3/14/24. +// + +import UIKit + +class MovieListCollectionViewCell: UICollectionViewCell { + + static let identifier = "MovieListCollectionViewCell" + + private let rankLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 40, weight: .semibold) + return label + }() + + private let rankChangeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + let attributedString = NSMutableAttributedString(string: "순위변경") + label.font = UIFont.systemFont(ofSize: 14) + label.baselineAdjustment = .alignCenters + return label + }() + + private let movieTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 21, weight: .semibold) + return label + }() + + private let audienceLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 16) + label.numberOfLines = 0 + return label + }() + + private lazy var rankStackView: UIStackView = { + let stackView = UIStackView() + stackView.addSubview(rankLabel) + stackView.addSubview(rankChangeLabel) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .center + stackView.distribution = .fill + + return stackView + }() + + private lazy var movieTitleStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [movieTitleLabel, audienceLabel]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 5 + return stackView + }() + + private lazy var accessoryButton: UIButton = { + let button = UIButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(UIImage(systemName: "chevron.right"), for: .normal) + button.tintColor = .gray + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configureBorder() + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } +} + +extension MovieListCollectionViewCell { + + private func configureBorder() { + layer.borderWidth = 0.2 + layer.borderColor = UIColor.lightGray.cgColor + } + + private func setupUI() { + contentView.addSubview(rankStackView) + contentView.addSubview(movieTitleStackView) + contentView.addSubview(accessoryButton) + + setupRankStackViewConstraint() + setupMovieTitleStackViewConstraint() + setupCellAccessoryButtonConstraint() + } + + private func setupRankStackViewConstraint() { + NSLayoutConstraint.activate([ + rankStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 1), + rankStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: 1), + rankStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5), + rankStackView.widthAnchor.constraint(equalToConstant: 60), + rankLabel.centerXAnchor.constraint(equalTo: rankStackView.centerXAnchor), + rankChangeLabel.centerXAnchor.constraint(equalTo: rankStackView.centerXAnchor), + rankLabel.topAnchor.constraint(equalTo: rankStackView.topAnchor, constant: 10), + rankChangeLabel.topAnchor.constraint(equalTo: rankLabel.bottomAnchor) + ]) + } + + private func setupMovieTitleStackViewConstraint() { + NSLayoutConstraint.activate([ + movieTitleStackView.leadingAnchor.constraint(equalTo: rankStackView.trailingAnchor, constant: 25), + movieTitleStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16) + ]) + } + + private func setupCellAccessoryButtonConstraint() { + NSLayoutConstraint.activate([ + accessoryButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + accessoryButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 40), + accessoryButton.widthAnchor.constraint(equalToConstant: 20), + ]) + } + + func configureCell(with movieInfo: DailyBoxOfficeDTO.BoxOfficeDTO.MovieInfo) { + rankLabel.text = movieInfo.rank + rankChangeLabel.text = movieInfo.rankChanges + movieTitleLabel.text = movieInfo.movieName + audienceLabel.text = "오늘 : \(movieInfo.audienceCount.formatNumberString())명 / 총 : \( movieInfo.audienceAccumulated.formatNumberString())명" + + if movieInfo.rankOldAndNew == "NEW" { + rankChangeLabel.text = "신작" + return + } + + if let rankChangeValue = Int(movieInfo.rankChanges) { + if rankChangeValue > 0 { + rankChangeLabel.text = "▲ \(movieInfo.rankChanges)" + rankChangeLabel.textColor = .red + } else if rankChangeValue < 0 { + rankChangeLabel.text = "▼ \(movieInfo.rankChanges)" + rankChangeLabel.textColor = .blue + } else { + rankChangeLabel.text = "-" + rankChangeLabel.textColor = .black + } + } + } +} diff --git a/BoxOffice/MovieListViewController.swift b/BoxOffice/MovieListViewController.swift index 6132d740..45ea18f6 100644 --- a/BoxOffice/MovieListViewController.swift +++ b/BoxOffice/MovieListViewController.swift @@ -4,51 +4,102 @@ // // Created by kjs on 13/01/23. // - +import OSLog import UIKit final class MovieListViewController: UIViewController { - private let networking = NetworkManager.shared - private let networkAPI = NetworkAPI() + private let networkManager = NetworkManager.shared + private var dailyBoxOfficeList:[DailyBoxOfficeDTO.BoxOfficeDTO.MovieInfo] = [] + private let apiBuilder = APIURLBuilder() + + private let collectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + let CV = UICollectionView(frame: .zero, collectionViewLayout: layout) + return CV + }() + + lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView() + activityIndicator.frame = CGRect(x: 0, y: 0, width: 50, height: 50) + activityIndicator.center = self.view.center + activityIndicator.hidesWhenStopped = false + activityIndicator.style = .large + activityIndicator.startAnimating() + return activityIndicator + }() + + private lazy var refreshControl: UIRefreshControl = { + let refreshControl = UIRefreshControl() + refreshControl.addTarget(self, action: #selector(loadData), for: .valueChanged) + + return refreshControl + }() override func viewDidLoad() { super.viewDidLoad() - callDailyBoxOfficeAPI() - callMovieDetailAPI() + title = Date().yesterday(format: Date.yyyyMMddHyphen) + + collectionView.dataSource = self + collectionView.delegate = self + setupCollectionView() + view.addSubview(activityIndicator) + loadData() } - private func callMovieDetailAPI() { - let url = networkAPI.buildMovieDetailAPI(movieCode: .movieCode) - networking.performRequest(with: url) { result in - switch result { - case .success(let data): - guard let data = data, - let decodedData = JSONParser().parseJSON(data as! Data, DTO: MovieDetailDTO.self) - else { - return - } - print(decodedData.movieInfoResult.movieInfo.companys) - case .failure(let error): - print(error) + @objc + private func loadData() { + Task { + let data = try await networkManager.performRequest(with: apiBuilder.buildDailyBoxOfficeAPI(targetDate: APIURLCompnents.QueryValues.targetDate)) + + guard let parsedData = JSONParser().decode(data, DTO: DailyBoxOfficeDTO.self), + let dailyBoxOfficeData = parsedData.boxOfficeResult.dailyBoxOfficeList + else { + return } + dailyBoxOfficeList = dailyBoxOfficeData + collectionView.reloadData() + activityIndicator.removeFromSuperview() + refreshControl.endRefreshing() } } - - private func callDailyBoxOfficeAPI() { - let url = networkAPI.buildDailyBoxOfficeAPI(targetDate: .targetDate, keys: .itemPerPage, values: "2") - networking.performRequest(with: url) { result in - switch result { - case .success(let data): - guard let data = data, - let decodedData = JSONParser().parseJSON(data as! Data, DTO: DailyBoxOfficeResultDTO.self) - else { - return - } - print(decodedData,"\n -------------------------") - case .failure(let error): - print(error) - } + + private func setupCollectionView() { + collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell") + collectionView.register(MovieListCollectionViewCell.self, forCellWithReuseIdentifier: "MovieListCollectionViewCell") + collectionView.refreshControl = refreshControl + view.addSubview(collectionView) + collectionView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + ]) + } +} + +extension MovieListViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieListCollectionViewCell", for: indexPath) as? MovieListCollectionViewCell else { + fatalError("fatalError") } + cell.configureCell(with: dailyBoxOfficeList[indexPath.row]) + cell.addSubview(MovieListCollectionViewCell()) + return cell + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + dailyBoxOfficeList.count + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + + let width = collectionView.frame.width + return CGSize(width: width, height: 100) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + 0 } } diff --git a/BoxOffice/Network/APIManager/APIURLBuilder.swift b/BoxOffice/Network/APIManager/APIURLBuilder.swift new file mode 100644 index 00000000..618f2a0c --- /dev/null +++ b/BoxOffice/Network/APIManager/APIURLBuilder.swift @@ -0,0 +1,58 @@ +// +// NetworkAPI.swift +// BoxOffice +// +// Created by dopamint on 2/17/24. +// + +import Foundation + +struct APIURLBuilder { + + func buildDailyBoxOfficeAPI(targetDate: String) -> URL? { + return buildUrl(apiType: .dailyBoxOffice, query: targetDate).url + } + + func buildMovieDetailAPI(movieCode: String) -> URL? { + return buildUrl(apiType: .movieDetail, query: movieCode).url + } + + private func buildUrl(apiType: APIType, query: String) -> URLComponents { + var components = URLComponents() + components.scheme = APIURLCompnents.schema + components.host = APIURLCompnents.host + components.path = apiType.makePath() + + components.queryItems = [ + URLQueryItem(name: "key", value: APIURLCompnents.QueryValues.APIKey.rawValue), + URLQueryItem(name: apiType.rawValue , value: query) + ] + return components + } +} + +extension APIURLBuilder { + func buildDailyBoxOfficeAPI(targetDate: String, keys: APIURLCompnents.QueryKeys..., values: Any...) -> URL? { + let dict: [APIURLCompnents.QueryKeys: Any] = zip(keys, values).reduce(into: [:]) { partialResult, now in + partialResult[now.0] = now.1 + } + return buildUrl(apiType: .dailyBoxOffice, requiredQuery: targetDate, queries: dict).url + } + + private func buildUrl(apiType: APIType, requiredQuery: String, queries: [APIURLCompnents.QueryKeys: Any]) -> URLComponents { + var components = URLComponents() + components.scheme = APIURLCompnents.schema + components.host = APIURLCompnents.host + components.path = apiType.makePath() + + components.queryItems = [ + URLQueryItem(name: "key", value: APIURLCompnents.QueryValues.APIKey.rawValue), + URLQueryItem(name: apiType.rawValue , value: requiredQuery) + ] + queries.forEach { (key: APIURLCompnents.QueryKeys, value: Any) in + components.queryItems?.append(URLQueryItem(name: key.rawValue, + value: value as? String)) + } + return components + } +} diff --git a/BoxOffice/Network/APIManager/APIURLCompnents.swift b/BoxOffice/Network/APIManager/APIURLCompnents.swift new file mode 100644 index 00000000..94cef516 --- /dev/null +++ b/BoxOffice/Network/APIManager/APIURLCompnents.swift @@ -0,0 +1,47 @@ +// +// query.swift +// BoxOffice +// +// Created by dopamint on 2/17/24. +// + +import Foundation + +struct APIURLCompnents { + + static let schema = "https" + static let host = "www.kobis.or.kr" + static let basePath = "/kobisopenapi/webservice/rest/" + static let boxOffice = "boxoffice/" + static let movie = "movie/" + + enum QueryKeys: String { + + case itemPerPage + case multiMovieYn + case repNationCd + case wideAreaCd + } + + enum QueryValues: String { + + case APIKey = "23a93cbbbc8fdc2ae474716728465cca" + static var targetDate = Date().yesterday(format: "yyyyMMdd") + case movieCode = "20124079" + } +} + +enum APIType: String { + + case dailyBoxOffice = "targetDt" + case movieDetail = "movieCd" + + func makePath() -> String { + switch self { + case .dailyBoxOffice: + return APIURLCompnents.basePath + APIURLCompnents.boxOffice + "searchDailyBoxOfficeList.json" + case .movieDetail: + return APIURLCompnents.basePath + APIURLCompnents.movie + "searchMovieInfo.json" + } + } +} diff --git a/BoxOffice/Network/Errors/HTTPError.swift b/BoxOffice/Network/Errors/HTTPError.swift new file mode 100644 index 00000000..10b9b72d --- /dev/null +++ b/BoxOffice/Network/Errors/HTTPError.swift @@ -0,0 +1,13 @@ +// +// HTTPError.swift +// BoxOffice +// +// Created by dopamint on 3/8/24. +// + +enum HTTPError: Error { + case redirectionMessages(_ statusCode: Int, _ description: String) + case clientErrorResponses(_ statusCode: Int, _ description: String) + case serverErrorResponses(_ statusCode: Int, _ description: String) + case networkFailError(_ statusCode: Int) +} diff --git a/BoxOffice/Network/Errors.swift b/BoxOffice/Network/Errors/NetworkError.swift similarity index 72% rename from BoxOffice/Network/Errors.swift rename to BoxOffice/Network/Errors/NetworkError.swift index c7441441..3fb9a642 100644 --- a/BoxOffice/Network/Errors.swift +++ b/BoxOffice/Network/Errors/NetworkError.swift @@ -6,8 +6,7 @@ // enum NetworkError: Error { - - case netWorkingError + case invalidUrl + case invalidResponse case parseError - case dataError } diff --git a/BoxOffice/Network/HTTPMethod.swift b/BoxOffice/Network/HTTPMethod.swift new file mode 100644 index 00000000..9d257b07 --- /dev/null +++ b/BoxOffice/Network/HTTPMethod.swift @@ -0,0 +1,14 @@ +// +// HTTPMethod.swift +// BoxOffice +// +// Created by dopamint on 3/8/24. +// + +enum HTTPMethod: String { + case get = "GET" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" +} diff --git a/BoxOffice/Network/JSONParser.swift b/BoxOffice/Network/JSONParser.swift index 9daea7f3..ac29eebd 100644 --- a/BoxOffice/Network/JSONParser.swift +++ b/BoxOffice/Network/JSONParser.swift @@ -7,15 +7,17 @@ import Foundation -struct JSONParser{ +struct JSONParser { - func parseJSON(_ movieData: Data, DTO: T.Type) -> T? { + func decode(_ data: Data, DTO: T.Type) -> T? { do { - let decodedData = try JSONDecoder().decode(T.self, from: movieData) + let decodedData = try JSONDecoder().decode(T.self, from: data) return decodedData } catch { - print("파싱 에러") + print(NetworkError.parseError) return nil } } } + + diff --git a/BoxOffice/Network/Query.swift b/BoxOffice/Network/Query.swift deleted file mode 100644 index 4fb55edd..00000000 --- a/BoxOffice/Network/Query.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// query.swift -// BoxOffice -// -// Created by dopamint on 2/17/24. -// - -struct APIURLCompnents { - - static let schema = "https" - static let host = "www.kobis.or.kr" - static let basePath = "/kobisopenapi/webservice/rest/" - static let boxOffice = "boxoffice/" - static let movie = "movie/" - - enum QueryKeys: String { - - case itemPerPage - case multiMovieYn - case repNationCd - case wideAreaCd - } - - enum QueryValues: String { - - case APIKey = "23a93cbbbc8fdc2ae474716728465cca" - case targetDate = "20240228" - case movieCode = "20124079" - } -} - -enum APIType: String { - - case dailyBoxOffice = "targetDt" - case movieDetail = "movieCd" - - func makePath() -> String { - switch self { - case .dailyBoxOffice: - return APIURLCompnents.basePath + APIURLCompnents.boxOffice + "searchDailyBoxOfficeList.json" - case .movieDetail: - return APIURLCompnents.basePath + APIURLCompnents.movie + "searchMovieInfo.json" - } - } -} - - - -//struct APIURLCompnents { -// static let schema = "https" -// static let host = "www.kobis.or.kr" -// static let basePath = "/kobisopenapi/webservice/rest/" -// static let boxOffice = "boxoffice/" -// static let movie = "movie/" -// static let APIKey = "23a93cbbbc8fdc2ae474716728465cca" -//} -// -//struct DailyBoxOfficeAPIQueries { -// enum QueryKeys: String { -// case itemPerPage -// case multiMovieYn -// case repNationCd -// case wideAreaCd -// } -// -// enum QueryValues: String { -// case targetDate = "20240228" -// } -//} -// -//struct MovieDetailAPIQueries { -// enum QueryValues: String { -// case movieCode = "20124079" -// } -//} -// -//enum APIType: String { -// case dailyBoxOffice = "targetDt" -// case movieDetail = "movieCd" -// -// func makePath() -> String { -// switch self { -// case .dailyBoxOffice: -// return APIURLCompnents.basePath + APIURLCompnents.boxOffice + "searchDailyBoxOfficeList.json" -// case .movieDetail: -// return APIURLCompnents.basePath + APIURLCompnents.movie + "searchMovieInfo.json" -// } -// } -//} diff --git a/BoxOffice/Network/NetworkAPI.swift "b/BoxOffice/Network/\341\204\206\341\205\250\343\205\221\343\205\241\341\204\206\341\205\256\341\206\267\343\205\216\343\204\267\343\204\261/APIURLBuilder.swift" similarity index 97% rename from BoxOffice/Network/NetworkAPI.swift rename to "BoxOffice/Network/\341\204\206\341\205\250\343\205\221\343\205\241\341\204\206\341\205\256\341\206\267\343\205\216\343\204\267\343\204\261/APIURLBuilder.swift" index ca25f89a..e25486d7 100644 --- a/BoxOffice/Network/NetworkAPI.swift +++ "b/BoxOffice/Network/\341\204\206\341\205\250\343\205\221\343\205\241\341\204\206\341\205\256\341\206\267\343\205\216\343\204\267\343\204\261/APIURLBuilder.swift" @@ -7,12 +7,12 @@ import Foundation -struct NetworkAPI { +struct APIURLBuilder { func buildDailyBoxOfficeAPI(targetDate: APIURLCompnents.QueryValues) -> URL? { return buildUrl(apiType: .dailyBoxOffice, query: targetDate).url } - + func buildMovieDetailAPI(movieCode: APIURLCompnents.QueryValues) -> URL? { return buildUrl(apiType: .movieDetail, query: movieCode).url } @@ -31,7 +31,7 @@ struct NetworkAPI { } } -extension NetworkAPI { +extension APIURLBuilder { func buildDailyBoxOfficeAPI(targetDate: APIURLCompnents.QueryValues, keys: APIURLCompnents.QueryKeys..., values: Any...) -> URL? { let dict: [APIURLCompnents.QueryKeys: Any] = zip(keys, values).reduce(into: [:]) { partialResult, now in partialResult[now.0] = now.1 diff --git "a/BoxOffice/Network/\341\204\206\341\205\250\343\205\221\343\205\241\341\204\206\341\205\256\341\206\267\343\205\216\343\204\267\343\204\261/Query.swift" "b/BoxOffice/Network/\341\204\206\341\205\250\343\205\221\343\205\241\341\204\206\341\205\256\341\206\267\343\205\216\343\204\267\343\204\261/Query.swift" new file mode 100644 index 00000000..b69ea627 --- /dev/null +++ "b/BoxOffice/Network/\341\204\206\341\205\250\343\205\221\343\205\241\341\204\206\341\205\256\341\206\267\343\205\216\343\204\267\343\204\261/Query.swift" @@ -0,0 +1,45 @@ +// +// query.swift +// BoxOffice +// +// Created by dopamint on 2/17/24. +// + +struct APIURLCompnents { + + static let schema = "https" + static let host = "www.kobis.or.kr" + static let basePath = "/kobisopenapi/webservice/rest/" + static let boxOffice = "boxoffice/" + static let movie = "movie/" + + enum QueryKeys: String { + + case itemPerPage + case multiMovieYn + case repNationCd + case wideAreaCd + } + + enum QueryValues: String { + + case APIKey = "23a93cbbbc8fdc2ae474716728465cca" + case targetDate = "20240228" + case movieCode = "20124079" + } +} + +enum APIType: String { + + case dailyBoxOffice = "targetDt" + case movieDetail = "movieCd" + + func makePath() -> String { + switch self { + case .dailyBoxOffice: + return APIURLCompnents.basePath + APIURLCompnents.boxOffice + "searchDailyBoxOfficeList.json" + case .movieDetail: + return APIURLCompnents.basePath + APIURLCompnents.movie + "searchMovieInfo.json" + } + } +} diff --git a/NetworkManager.swift b/NetworkManager.swift index 918ba9c7..bd7631b5 100644 --- a/NetworkManager.swift +++ b/NetworkManager.swift @@ -8,36 +8,53 @@ import Foundation class NetworkManager { - + static let shared = NetworkManager() - let networkAPI = NetworkAPI() private init() {} - typealias NetworkCompletion = (Result) -> Void - - func performRequest(with url: URL?, completion: @escaping (Result) -> Void) { + func performRequest(with url: URL?) async throws -> Data { guard let url else { - return + throw NetworkError.invalidUrl } + let session = URLSession(configuration: .default) - let task = session.dataTask(with: url) { data, response, error in - print(url) - guard error == nil else { - print("네트워크 에러") - completion(.failure(.netWorkingError)) - return - } - guard let safeData = data else { - completion(.failure(.dataError)) - return - } - guard let response = response as? HTTPURLResponse, (200 ..< 299) ~= response.statusCode else { - completion(.failure(.netWorkingError)) - return - } - completion(.success(safeData)) + let request = makeRequest(with: url, method: .get, headers: nil) + + let (data, response) = try await session.data(for: request) + + guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { + throw NetworkError.invalidResponse + } + + guard (200 ..< 299) ~= statusCode else { + throw verifyStatusCode(with: response as! HTTPURLResponse) + } + + return data + } + + func makeRequest(with url: URL, method: HTTPMethod, headers: [String: String]?) -> URLRequest { + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + headers?.forEach { key, value in + request.addValue(value, forHTTPHeaderField: key) + } + + return request + } + + private func verifyStatusCode(with HTTPResponse: HTTPURLResponse) -> Error { + switch HTTPResponse.statusCode { + case (300...399): + return HTTPError.redirectionMessages(HTTPResponse.statusCode, HTTPResponse.debugDescription) + case (400...499): + return HTTPError.clientErrorResponses(HTTPResponse.statusCode, HTTPResponse.debugDescription) + case (500...599): + return HTTPError.serverErrorResponses(HTTPResponse.statusCode, HTTPResponse.debugDescription) + default: + return HTTPError.networkFailError(HTTPResponse.statusCode) } - task.resume() } } +