diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e440924a --- /dev/null +++ b/.gitignore @@ -0,0 +1,135 @@ +# Created by https://www.toptal.com/developers/gitignore/api/macos,swift,xcode +# Edit at https://www.toptal.com/developers/gitignore?templates=macos,swift,xcode + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +Private/ +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Xcode ### + +## Xcode 8 and earlier + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/macos,swift,xcode diff --git a/BoxOffice.xcodeproj/project.pbxproj b/BoxOffice.xcodeproj/project.pbxproj index 5ee48151..85b86a23 100644 --- a/BoxOffice.xcodeproj/project.pbxproj +++ b/BoxOffice.xcodeproj/project.pbxproj @@ -7,26 +7,122 @@ objects = { /* Begin PBXBuildFile section */ + 082798262B87771800ACC723 /* EndPointMakable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082798252B87771700ACC723 /* EndPointMakable.swift */; }; + 082798282B87778200ACC723 /* EndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082798272B87778200ACC723 /* EndPoint.swift */; }; + 082798362B882B8400ACC723 /* JsonDecodableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082798352B882B8400ACC723 /* JsonDecodableProtocol.swift */; }; + 082798382B88311400ACC723 /* MappableProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082798372B88311400ACC723 /* MappableProtocol.swift */; }; + 0827983C2B88328B00ACC723 /* BoxOfficeUseCaseProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0827983B2B88328B00ACC723 /* BoxOfficeUseCaseProtocol.swift */; }; + 0827983E2B88339900ACC723 /* DependencyEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0827983D2B88339900ACC723 /* DependencyEnvironment.swift */; }; + 082798402B8833F000ACC723 /* HTTPMethodType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0827983F2B8833F000ACC723 /* HTTPMethodType.swift */; }; + 0827984A2B884F9500ACC723 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082798492B884F9500ACC723 /* DateExtension.swift */; }; + 0827984C2B88519900ACC723 /* MovieDetailInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0827984B2B88519900ACC723 /* MovieDetailInfo.swift */; }; + 0864FC632B9F2EE500CE0725 /* APIHostType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0864FC622B9F2EE500CE0725 /* APIHostType.swift */; }; + 0864FC652B9F318000CE0725 /* ViewControllerFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0864FC642B9F318000CE0725 /* ViewControllerFactoryProtocol.swift */; }; + 08A6CE5D2B99C6CB00F2F2CA /* BoxOfficeDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A6CE5C2B99C6CB00F2F2CA /* BoxOfficeDisplayModel.swift */; }; + 08A6CE6E2B9AA76500F2F2CA /* BoxOfficeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A6CE6D2B9AA76500F2F2CA /* BoxOfficeCell.swift */; }; + 08A6CE702B9AA9F000F2F2CA /* SynchronizedLockPropertyWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A6CE6F2B9AA9EF00F2F2CA /* SynchronizedLockPropertyWrapper.swift */; }; + 08A6CE732B9AAA4E00F2F2CA /* ViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A6CE722B9AAA4E00F2F2CA /* ViewControllerExtension.swift */; }; + 08AB8E8F2B7F479D00564AC5 /* KeyEnvironmentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB8E8E2B7F479D00564AC5 /* KeyEnvironmentHandler.swift */; }; + 08DCFED72B7C5722002E22EA /* BoxOfficeSample.json in Resources */ = {isa = PBXBuildFile; fileRef = 08DCFED62B7C5722002E22EA /* BoxOfficeSample.json */; }; + 08DCFEDF2B7C59D7002E22EA /* NetworkSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DCFEDE2B7C59D7002E22EA /* NetworkSessionTests.swift */; }; 63DF20EF2970E1A0005DF7D1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20EE2970E1A0005DF7D1 /* AppDelegate.swift */; }; 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20F02970E1A0005DF7D1 /* SceneDelegate.swift */; }; - 63DF20F32970E1A0005DF7D1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20F22970E1A0005DF7D1 /* ViewController.swift */; }; - 63DF20F62970E1A0005DF7D1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F42970E1A0005DF7D1 /* Main.storyboard */; }; + 63DF20F32970E1A0005DF7D1 /* BoxOfficeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63DF20F22970E1A0005DF7D1 /* BoxOfficeViewController.swift */; }; 63DF20F82970E1A1005DF7D1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */; }; 63DF20FB2970E1A1005DF7D1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */; }; + C76771842B9AA88400B3B9BD /* BoxOfficeCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76771832B9AA88400B3B9BD /* BoxOfficeCollectionView.swift */; }; + C76965E62B86E83100EF8F87 /* NetworkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76965E52B86E83100EF8F87 /* NetworkResponse.swift */; }; + C76965EA2B86E83D00EF8F87 /* SessionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76965E92B86E83D00EF8F87 /* SessionProvider.swift */; }; + C76965EF2B86E89B00EF8F87 /* MovieRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76965EE2B86E89B00EF8F87 /* MovieRepository.swift */; }; + C76965F12B86ECB800EF8F87 /* BoxOfficeMovie.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76965F02B86ECB800EF8F87 /* BoxOfficeMovie.swift */; }; + C76965F32B86ECBD00EF8F87 /* BoxOfficeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76965F22B86ECBD00EF8F87 /* BoxOfficeUseCase.swift */; }; + C76965F92B86EE7200EF8F87 /* SessionProvidable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76965F82B86EE7200EF8F87 /* SessionProvidable.swift */; }; + C77ADAB82B7C766A0069AE0F /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77ADAB72B7C766A0069AE0F /* NetworkError.swift */; }; + C77ADABA2B7C76B40069AE0F /* JsonDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77ADAB92B7C76B40069AE0F /* JsonDecoder.swift */; }; + C78946B82BA9330B004778A3 /* URLInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78946B72BA9330B004778A3 /* URLInformation.swift */; }; + C78946BC2BA9B36D004778A3 /* Mapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78946BB2BA9B36D004778A3 /* Mapper.swift */; }; + C78D99402B9F300E00D8000A /* RequestProvidable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78D993F2B9F300E00D8000A /* RequestProvidable.swift */; }; + C78D99422B9F301F00D8000A /* RequestProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78D99412B9F301F00D8000A /* RequestProvider.swift */; }; + C790FE522BA838A70086D7DB /* RequestInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C790FE512BA838A70086D7DB /* RequestInformation.swift */; }; + C790FE542BA838EB0086D7DB /* SchemeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C790FE532BA838EB0086D7DB /* SchemeType.swift */; }; + C790FE572BA8394D0086D7DB /* DEBUG-Keys.plist in Resources */ = {isa = PBXBuildFile; fileRef = C790FE552BA8394D0086D7DB /* DEBUG-Keys.plist */; }; + C7A1E7682B882C3A00F9AB6B /* MovieRepositoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A1E7672B882C3A00F9AB6B /* MovieRepositoryProtocol.swift */; }; + C7A1E76A2B8831AA00F9AB6B /* DomainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A1E7692B8831AA00F9AB6B /* DomainError.swift */; }; + C7A1E76D2B883AAC00F9AB6B /* StubJSONData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A1E76C2B883AAC00F9AB6B /* StubJSONData.swift */; }; + C7A1E76F2B883B1200F9AB6B /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A1E76E2B883B1200F9AB6B /* MockURLProtocol.swift */; }; + C7A1E7732B883CE300F9AB6B /* DetailMovieInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A1E7722B883CE300F9AB6B /* DetailMovieInfoDTO.swift */; }; + C7C8D3332B7C5EA9009DE42F /* BoxOfficeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C8D3322B7C5EA9009DE42F /* BoxOfficeDTO.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 08DCFEE02B7C59D7002E22EA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 63DF20E32970E1A0005DF7D1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 63DF20EA2970E1A0005DF7D1; + remoteInfo = BoxOffice; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 082798252B87771700ACC723 /* EndPointMakable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPointMakable.swift; sourceTree = ""; }; + 082798272B87778200ACC723 /* EndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPoint.swift; sourceTree = ""; }; + 082798352B882B8400ACC723 /* JsonDecodableProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonDecodableProtocol.swift; sourceTree = ""; }; + 082798372B88311400ACC723 /* MappableProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MappableProtocol.swift; sourceTree = ""; }; + 0827983B2B88328B00ACC723 /* BoxOfficeUseCaseProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeUseCaseProtocol.swift; sourceTree = ""; }; + 0827983D2B88339900ACC723 /* DependencyEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyEnvironment.swift; sourceTree = ""; }; + 0827983F2B8833F000ACC723 /* HTTPMethodType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethodType.swift; sourceTree = ""; }; + 082798492B884F9500ACC723 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; + 0827984B2B88519900ACC723 /* MovieDetailInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailInfo.swift; sourceTree = ""; }; + 0864FC622B9F2EE500CE0725 /* APIHostType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIHostType.swift; sourceTree = ""; }; + 0864FC642B9F318000CE0725 /* ViewControllerFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerFactoryProtocol.swift; sourceTree = ""; }; + 08A6CE5C2B99C6CB00F2F2CA /* BoxOfficeDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeDisplayModel.swift; sourceTree = ""; }; + 08A6CE6D2B9AA76500F2F2CA /* BoxOfficeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeCell.swift; sourceTree = ""; }; + 08A6CE6F2B9AA9EF00F2F2CA /* SynchronizedLockPropertyWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedLockPropertyWrapper.swift; sourceTree = ""; }; + 08A6CE722B9AAA4E00F2F2CA /* ViewControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerExtension.swift; sourceTree = ""; }; + 08AB8E8E2B7F479D00564AC5 /* KeyEnvironmentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEnvironmentHandler.swift; sourceTree = ""; }; + 08DCFED62B7C5722002E22EA /* BoxOfficeSample.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = BoxOfficeSample.json; sourceTree = ""; }; + 08DCFEDC2B7C59D7002E22EA /* BoxOfficeUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BoxOfficeUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 08DCFEDE2B7C59D7002E22EA /* NetworkSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSessionTests.swift; sourceTree = ""; }; 63DF20EB2970E1A0005DF7D1 /* BoxOffice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BoxOffice.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 63DF20F52970E1A0005DF7D1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 63DF20F22970E1A0005DF7D1 /* BoxOfficeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeViewController.swift; 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 = ""; }; + C76771832B9AA88400B3B9BD /* BoxOfficeCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeCollectionView.swift; sourceTree = ""; }; + C76965E52B86E83100EF8F87 /* NetworkResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResponse.swift; sourceTree = ""; }; + C76965E92B86E83D00EF8F87 /* SessionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProvider.swift; sourceTree = ""; }; + C76965EE2B86E89B00EF8F87 /* MovieRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieRepository.swift; sourceTree = ""; }; + C76965F02B86ECB800EF8F87 /* BoxOfficeMovie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeMovie.swift; sourceTree = ""; }; + C76965F22B86ECBD00EF8F87 /* BoxOfficeUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeUseCase.swift; sourceTree = ""; }; + C76965F82B86EE7200EF8F87 /* SessionProvidable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionProvidable.swift; sourceTree = ""; }; + C77ADAB72B7C766A0069AE0F /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + C77ADAB92B7C76B40069AE0F /* JsonDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonDecoder.swift; sourceTree = ""; }; + C78946B72BA9330B004778A3 /* URLInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLInformation.swift; sourceTree = ""; }; + C78946BB2BA9B36D004778A3 /* Mapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mapper.swift; sourceTree = ""; }; + C78D993F2B9F300E00D8000A /* RequestProvidable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProvidable.swift; sourceTree = ""; }; + C78D99412B9F301F00D8000A /* RequestProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProvider.swift; sourceTree = ""; }; + C790FE512BA838A70086D7DB /* RequestInformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestInformation.swift; sourceTree = ""; }; + C790FE532BA838EB0086D7DB /* SchemeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchemeType.swift; sourceTree = ""; }; + C790FE552BA8394D0086D7DB /* DEBUG-Keys.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "DEBUG-Keys.plist"; sourceTree = ""; }; + C7A1E7672B882C3A00F9AB6B /* MovieRepositoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieRepositoryProtocol.swift; sourceTree = ""; }; + C7A1E7692B8831AA00F9AB6B /* DomainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainError.swift; sourceTree = ""; }; + C7A1E76C2B883AAC00F9AB6B /* StubJSONData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubJSONData.swift; sourceTree = ""; }; + C7A1E76E2B883B1200F9AB6B /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; + C7A1E7722B883CE300F9AB6B /* DetailMovieInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailMovieInfoDTO.swift; sourceTree = ""; }; + C7C8D3322B7C5EA9009DE42F /* BoxOfficeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeDTO.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 08DCFED92B7C59D7002E22EA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 63DF20E82970E1A0005DF7D1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -37,10 +133,126 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0827981A2B8774E700ACC723 /* URLBuilder */ = { + isa = PBXGroup; + children = ( + 082798252B87771700ACC723 /* EndPointMakable.swift */, + 082798272B87778200ACC723 /* EndPoint.swift */, + C78946B72BA9330B004778A3 /* URLInformation.swift */, + ); + path = URLBuilder; + sourceTree = ""; + }; + 082798482B884F8B00ACC723 /* Common */ = { + isa = PBXGroup; + children = ( + 082798492B884F9500ACC723 /* DateExtension.swift */, + 08A6CE6F2B9AA9EF00F2F2CA /* SynchronizedLockPropertyWrapper.swift */, + ); + path = Common; + sourceTree = ""; + }; + 08A6CE5B2B99C66400F2F2CA /* DisplayModel */ = { + isa = PBXGroup; + children = ( + 08A6CE5C2B99C6CB00F2F2CA /* BoxOfficeDisplayModel.swift */, + ); + path = DisplayModel; + sourceTree = ""; + }; + 08A6CE6C2B9AA75400F2F2CA /* View */ = { + isa = PBXGroup; + children = ( + 08A6CE6D2B9AA76500F2F2CA /* BoxOfficeCell.swift */, + C76771832B9AA88400B3B9BD /* BoxOfficeCollectionView.swift */, + ); + path = View; + sourceTree = ""; + }; + 08A6CE712B9AAA3900F2F2CA /* Common */ = { + isa = PBXGroup; + children = ( + 08A6CE722B9AAA4E00F2F2CA /* ViewControllerExtension.swift */, + ); + path = Common; + sourceTree = ""; + }; + 08AB8E772B7F249600564AC5 /* App */ = { + isa = PBXGroup; + children = ( + 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */, + 63DF20F02970E1A0005DF7D1 /* SceneDelegate.swift */, + 63DF20EE2970E1A0005DF7D1 /* AppDelegate.swift */, + 0864FC642B9F318000CE0725 /* ViewControllerFactoryProtocol.swift */, + 0827983D2B88339900ACC723 /* DependencyEnvironment.swift */, + ); + path = App; + sourceTree = ""; + }; + 08AB8E782B7F24AB00564AC5 /* DTO */ = { + isa = PBXGroup; + children = ( + C7C8D3322B7C5EA9009DE42F /* BoxOfficeDTO.swift */, + C7A1E7722B883CE300F9AB6B /* DetailMovieInfoDTO.swift */, + ); + path = DTO; + sourceTree = ""; + }; + 08AB8E792B7F24B000564AC5 /* NetworkService */ = { + isa = PBXGroup; + children = ( + C76965E42B86E7DE00EF8F87 /* Model */, + C78D993C2B9F2F8700D8000A /* RequestBuilder */, + 0827981A2B8774E700ACC723 /* URLBuilder */, + C76965E12B86E74D00EF8F87 /* SessionProvider */, + C76965E22B86E78F00EF8F87 /* Serializer */, + C76965E32B86E7C800EF8F87 /* Error */, + 08AB8E7C2B7F285000564AC5 /* Constants */, + ); + path = NetworkService; + sourceTree = ""; + }; + 08AB8E7B2B7F24BE00564AC5 /* Controller */ = { + isa = PBXGroup; + children = ( + 63DF20F22970E1A0005DF7D1 /* BoxOfficeViewController.swift */, + ); + path = Controller; + sourceTree = ""; + }; + 08AB8E7C2B7F285000564AC5 /* Constants */ = { + isa = PBXGroup; + children = ( + 08AB8E8E2B7F479D00564AC5 /* KeyEnvironmentHandler.swift */, + ); + path = Constants; + sourceTree = ""; + }; + 08AB8E7D2B7F288500564AC5 /* Resource */ = { + isa = PBXGroup; + children = ( + 08DCFED62B7C5722002E22EA /* BoxOfficeSample.json */, + 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */, + 63DF20FC2970E1A1005DF7D1 /* Info.plist */, + ); + path = Resource; + sourceTree = ""; + }; + 08DCFEDD2B7C59D7002E22EA /* BoxOfficeUnitTests */ = { + isa = PBXGroup; + children = ( + 08DCFEDE2B7C59D7002E22EA /* NetworkSessionTests.swift */, + C7A1E76C2B883AAC00F9AB6B /* StubJSONData.swift */, + C7A1E76E2B883B1200F9AB6B /* MockURLProtocol.swift */, + ); + path = BoxOfficeUnitTests; + sourceTree = ""; + }; 63DF20E22970E1A0005DF7D1 = { isa = PBXGroup; children = ( 63DF20ED2970E1A0005DF7D1 /* BoxOffice */, + 08DCFEDD2B7C59D7002E22EA /* BoxOfficeUnitTests */, 63DF20EC2970E1A0005DF7D1 /* Products */, ); sourceTree = ""; @@ -49,6 +261,7 @@ isa = PBXGroup; children = ( 63DF20EB2970E1A0005DF7D1 /* BoxOffice.app */, + 08DCFEDC2B7C59D7002E22EA /* BoxOfficeUnitTests.xctest */, ); name = Products; sourceTree = ""; @@ -56,20 +269,169 @@ 63DF20ED2970E1A0005DF7D1 /* BoxOffice */ = { isa = PBXGroup; children = ( - 63DF20EE2970E1A0005DF7D1 /* AppDelegate.swift */, - 63DF20F02970E1A0005DF7D1 /* SceneDelegate.swift */, - 63DF20F22970E1A0005DF7D1 /* ViewController.swift */, - 63DF20F42970E1A0005DF7D1 /* Main.storyboard */, - 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */, - 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */, - 63DF20FC2970E1A1005DF7D1 /* Info.plist */, + 08AB8E772B7F249600564AC5 /* App */, + C76965DC2B86E66500EF8F87 /* Presentation */, + C76965DD2B86E68B00EF8F87 /* Domain */, + C76965DE2B86E69900EF8F87 /* Data */, + C790FE562BA8394D0086D7DB /* Private */, + 082798482B884F8B00ACC723 /* Common */, + 08AB8E7D2B7F288500564AC5 /* Resource */, ); path = BoxOffice; sourceTree = ""; }; + C76965DC2B86E66500EF8F87 /* Presentation */ = { + isa = PBXGroup; + children = ( + 08A6CE5B2B99C66400F2F2CA /* DisplayModel */, + 08A6CE6C2B9AA75400F2F2CA /* View */, + 08AB8E7B2B7F24BE00564AC5 /* Controller */, + 08A6CE712B9AAA3900F2F2CA /* Common */, + ); + path = Presentation; + sourceTree = ""; + }; + C76965DD2B86E68B00EF8F87 /* Domain */ = { + isa = PBXGroup; + children = ( + C76965EC2B86E86100EF8F87 /* Entity */, + C76965EB2B86E85200EF8F87 /* UseCase */, + C7A3FF4D2BAA81B200FEE6A6 /* Mapper */, + C7A1E76B2B8831FA00F9AB6B /* Error */, + ); + path = Domain; + sourceTree = ""; + }; + C76965DE2B86E69900EF8F87 /* Data */ = { + isa = PBXGroup; + children = ( + 08AB8E782B7F24AB00564AC5 /* DTO */, + C76965ED2B86E89000EF8F87 /* Repository */, + 08AB8E792B7F24B000564AC5 /* NetworkService */, + ); + path = Data; + sourceTree = ""; + }; + C76965E12B86E74D00EF8F87 /* SessionProvider */ = { + isa = PBXGroup; + children = ( + C76965F82B86EE7200EF8F87 /* SessionProvidable.swift */, + C76965E92B86E83D00EF8F87 /* SessionProvider.swift */, + ); + path = SessionProvider; + sourceTree = ""; + }; + C76965E22B86E78F00EF8F87 /* Serializer */ = { + isa = PBXGroup; + children = ( + 082798352B882B8400ACC723 /* JsonDecodableProtocol.swift */, + C77ADAB92B7C76B40069AE0F /* JsonDecoder.swift */, + ); + path = Serializer; + sourceTree = ""; + }; + C76965E32B86E7C800EF8F87 /* Error */ = { + isa = PBXGroup; + children = ( + C77ADAB72B7C766A0069AE0F /* NetworkError.swift */, + ); + path = Error; + sourceTree = ""; + }; + C76965E42B86E7DE00EF8F87 /* Model */ = { + isa = PBXGroup; + children = ( + C76965E52B86E83100EF8F87 /* NetworkResponse.swift */, + 0827983F2B8833F000ACC723 /* HTTPMethodType.swift */, + 0864FC622B9F2EE500CE0725 /* APIHostType.swift */, + C790FE532BA838EB0086D7DB /* SchemeType.swift */, + ); + path = Model; + sourceTree = ""; + }; + C76965EB2B86E85200EF8F87 /* UseCase */ = { + isa = PBXGroup; + children = ( + 0827983B2B88328B00ACC723 /* BoxOfficeUseCaseProtocol.swift */, + C76965F22B86ECBD00EF8F87 /* BoxOfficeUseCase.swift */, + ); + path = UseCase; + sourceTree = ""; + }; + C76965EC2B86E86100EF8F87 /* Entity */ = { + isa = PBXGroup; + children = ( + C76965F02B86ECB800EF8F87 /* BoxOfficeMovie.swift */, + 0827984B2B88519900ACC723 /* MovieDetailInfo.swift */, + ); + path = Entity; + sourceTree = ""; + }; + C76965ED2B86E89000EF8F87 /* Repository */ = { + isa = PBXGroup; + children = ( + C7A1E7672B882C3A00F9AB6B /* MovieRepositoryProtocol.swift */, + C76965EE2B86E89B00EF8F87 /* MovieRepository.swift */, + ); + path = Repository; + sourceTree = ""; + }; + C78D993C2B9F2F8700D8000A /* RequestBuilder */ = { + isa = PBXGroup; + children = ( + C78D993F2B9F300E00D8000A /* RequestProvidable.swift */, + C78D99412B9F301F00D8000A /* RequestProvider.swift */, + C790FE512BA838A70086D7DB /* RequestInformation.swift */, + ); + path = RequestBuilder; + sourceTree = ""; + }; + C790FE562BA8394D0086D7DB /* Private */ = { + isa = PBXGroup; + children = ( + C790FE552BA8394D0086D7DB /* DEBUG-Keys.plist */, + ); + path = Private; + sourceTree = ""; + }; + C7A1E76B2B8831FA00F9AB6B /* Error */ = { + isa = PBXGroup; + children = ( + C7A1E7692B8831AA00F9AB6B /* DomainError.swift */, + ); + path = Error; + sourceTree = ""; + }; + C7A3FF4D2BAA81B200FEE6A6 /* Mapper */ = { + isa = PBXGroup; + children = ( + C78946BB2BA9B36D004778A3 /* Mapper.swift */, + 082798372B88311400ACC723 /* MappableProtocol.swift */, + ); + path = Mapper; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 08DCFEDB2B7C59D7002E22EA /* BoxOfficeUnitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 08DCFEE42B7C59D7002E22EA /* Build configuration list for PBXNativeTarget "BoxOfficeUnitTests" */; + buildPhases = ( + 08DCFED82B7C59D7002E22EA /* Sources */, + 08DCFED92B7C59D7002E22EA /* Frameworks */, + 08DCFEDA2B7C59D7002E22EA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 08DCFEE12B7C59D7002E22EA /* PBXTargetDependency */, + ); + name = BoxOfficeUnitTests; + productName = BoxOfficeUnitTests; + productReference = 08DCFEDC2B7C59D7002E22EA /* BoxOfficeUnitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 63DF20EA2970E1A0005DF7D1 /* BoxOffice */ = { isa = PBXNativeTarget; buildConfigurationList = 63DF20FF2970E1A1005DF7D1 /* Build configuration list for PBXNativeTarget "BoxOffice" */; @@ -94,9 +456,13 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1420; + LastSwiftUpdateCheck = 1520; + LastUpgradeCheck = 1520; TargetAttributes = { + 08DCFEDB2B7C59D7002E22EA = { + CreatedOnToolsVersion = 15.2; + TestTargetID = 63DF20EA2970E1A0005DF7D1; + }; 63DF20EA2970E1A0005DF7D1 = { CreatedOnToolsVersion = 14.2; }; @@ -116,45 +482,99 @@ projectRoot = ""; targets = ( 63DF20EA2970E1A0005DF7D1 /* BoxOffice */, + 08DCFEDB2B7C59D7002E22EA /* BoxOfficeUnitTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 08DCFEDA2B7C59D7002E22EA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 63DF20E92970E1A0005DF7D1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 63DF20FB2970E1A1005DF7D1 /* LaunchScreen.storyboard in Resources */, 63DF20F82970E1A1005DF7D1 /* Assets.xcassets in Resources */, - 63DF20F62970E1A0005DF7D1 /* Main.storyboard in Resources */, + C790FE572BA8394D0086D7DB /* DEBUG-Keys.plist in Resources */, + 08DCFED72B7C5722002E22EA /* BoxOfficeSample.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 08DCFED82B7C59D7002E22EA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C7A1E76F2B883B1200F9AB6B /* MockURLProtocol.swift in Sources */, + 08DCFEDF2B7C59D7002E22EA /* NetworkSessionTests.swift in Sources */, + C7A1E76D2B883AAC00F9AB6B /* StubJSONData.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 63DF20E72970E1A0005DF7D1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 63DF20F32970E1A0005DF7D1 /* ViewController.swift in Sources */, + C7A1E7732B883CE300F9AB6B /* DetailMovieInfoDTO.swift in Sources */, + 08AB8E8F2B7F479D00564AC5 /* KeyEnvironmentHandler.swift in Sources */, + 0827983E2B88339900ACC723 /* DependencyEnvironment.swift in Sources */, + C78D99422B9F301F00D8000A /* RequestProvider.swift in Sources */, + 0827984A2B884F9500ACC723 /* DateExtension.swift in Sources */, + C78946BC2BA9B36D004778A3 /* Mapper.swift in Sources */, + C7A1E76A2B8831AA00F9AB6B /* DomainError.swift in Sources */, + C76771842B9AA88400B3B9BD /* BoxOfficeCollectionView.swift in Sources */, + C76965F12B86ECB800EF8F87 /* BoxOfficeMovie.swift in Sources */, + C77ADAB82B7C766A0069AE0F /* NetworkError.swift in Sources */, + 63DF20F32970E1A0005DF7D1 /* BoxOfficeViewController.swift in Sources */, + C77ADABA2B7C76B40069AE0F /* JsonDecoder.swift in Sources */, + 082798382B88311400ACC723 /* MappableProtocol.swift in Sources */, + C76965F92B86EE7200EF8F87 /* SessionProvidable.swift in Sources */, + C7C8D3332B7C5EA9009DE42F /* BoxOfficeDTO.swift in Sources */, + C76965EA2B86E83D00EF8F87 /* SessionProvider.swift in Sources */, + 0827984C2B88519900ACC723 /* MovieDetailInfo.swift in Sources */, + C76965E62B86E83100EF8F87 /* NetworkResponse.swift in Sources */, + 0864FC652B9F318000CE0725 /* ViewControllerFactoryProtocol.swift in Sources */, + 08A6CE5D2B99C6CB00F2F2CA /* BoxOfficeDisplayModel.swift in Sources */, + 08A6CE732B9AAA4E00F2F2CA /* ViewControllerExtension.swift in Sources */, + 082798282B87778200ACC723 /* EndPoint.swift in Sources */, 63DF20EF2970E1A0005DF7D1 /* AppDelegate.swift in Sources */, + C7A1E7682B882C3A00F9AB6B /* MovieRepositoryProtocol.swift in Sources */, + 082798362B882B8400ACC723 /* JsonDecodableProtocol.swift in Sources */, + C78D99402B9F300E00D8000A /* RequestProvidable.swift in Sources */, + C790FE522BA838A70086D7DB /* RequestInformation.swift in Sources */, + 0864FC632B9F2EE500CE0725 /* APIHostType.swift in Sources */, + 08A6CE702B9AA9F000F2F2CA /* SynchronizedLockPropertyWrapper.swift in Sources */, + C76965F32B86ECBD00EF8F87 /* BoxOfficeUseCase.swift in Sources */, + C78946B82BA9330B004778A3 /* URLInformation.swift in Sources */, + 082798262B87771800ACC723 /* EndPointMakable.swift in Sources */, + C76965EF2B86E89B00EF8F87 /* MovieRepository.swift in Sources */, + 0827983C2B88328B00ACC723 /* BoxOfficeUseCaseProtocol.swift in Sources */, + 08A6CE6E2B9AA76500F2F2CA /* BoxOfficeCell.swift in Sources */, 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */, + C790FE542BA838EB0086D7DB /* SchemeType.swift in Sources */, + 082798402B8833F000ACC723 /* HTTPMethodType.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXVariantGroup section */ - 63DF20F42970E1A0005DF7D1 /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 63DF20F52970E1A0005DF7D1 /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; +/* Begin PBXTargetDependency section */ + 08DCFEE12B7C59D7002E22EA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 63DF20EA2970E1A0005DF7D1 /* BoxOffice */; + targetProxy = 08DCFEE02B7C59D7002E22EA /* PBXContainerItemProxy */; }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ 63DF20F92970E1A1005DF7D1 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -166,6 +586,61 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 08DCFEE22B7C59D7002E22EA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = VGM28F2FH7; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = JongHyuckJeon.BoxOfficeUnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BoxOffice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/BoxOffice"; + }; + name = Debug; + }; + 08DCFEE32B7C59D7002E22EA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; + DEVELOPMENT_TEAM = VGM28F2FH7; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = JongHyuckJeon.BoxOfficeUnitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = auto; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/BoxOffice.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/BoxOffice"; + }; + name = Release; + }; 63DF20FD2970E1A1005DF7D1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -287,20 +762,20 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = VGM28F2FH7; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = BoxOffice/Info.plist; + INFOPLIST_FILE = BoxOffice/Resource/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; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.yagom.BoxOffice; + PRODUCT_BUNDLE_IDENTIFIER = Harry.BoxOffice; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -315,20 +790,20 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = VGM28F2FH7; GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = BoxOffice/Info.plist; + INFOPLIST_FILE = BoxOffice/Resource/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; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.yagom.BoxOffice; + PRODUCT_BUNDLE_IDENTIFIER = Harry.BoxOffice; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -339,6 +814,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 08DCFEE42B7C59D7002E22EA /* Build configuration list for PBXNativeTarget "BoxOfficeUnitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 08DCFEE22B7C59D7002E22EA /* Debug */, + 08DCFEE32B7C59D7002E22EA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 63DF20E62970E1A0005DF7D1 /* Build configuration list for PBXProject "BoxOffice" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/BoxOffice.xcodeproj/xcshareddata/xcschemes/BoxOffice.xcscheme b/BoxOffice.xcodeproj/xcshareddata/xcschemes/BoxOffice.xcscheme new file mode 100644 index 00000000..5e07e797 --- /dev/null +++ b/BoxOffice.xcodeproj/xcshareddata/xcschemes/BoxOffice.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BoxOffice/App/AppDelegate.swift b/BoxOffice/App/AppDelegate.swift new file mode 100644 index 00000000..d141f35d --- /dev/null +++ b/BoxOffice/App/AppDelegate.swift @@ -0,0 +1,17 @@ + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } +} + +var ENV: APIKey { + return DebugEnvironment(resourceName: "DEBUG-Keys") +} diff --git a/BoxOffice/Base.lproj/LaunchScreen.storyboard b/BoxOffice/App/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from BoxOffice/Base.lproj/LaunchScreen.storyboard rename to BoxOffice/App/Base.lproj/LaunchScreen.storyboard diff --git a/BoxOffice/App/DependencyEnvironment.swift b/BoxOffice/App/DependencyEnvironment.swift new file mode 100644 index 00000000..771a1d9a --- /dev/null +++ b/BoxOffice/App/DependencyEnvironment.swift @@ -0,0 +1,33 @@ + +import UIKit + +enum ViewControllerType { + case boxOffice +} + +final class DependencyEnvironment { + private let jsonDecoder: JSONDecoder = { + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .iso8601 + return jsonDecoder + }() + + private lazy var decodeProvider: DecoderProtocol = JsonDecoder(jsonDecoder: jsonDecoder) + + private var sessionProvider: SessionProvidable = SessionProvider() + + private lazy var movieRepository: MovieRepositoryProtocol = MovieRepository(sessionProvider: sessionProvider, decoder: decodeProvider) + + private lazy var mapper: Mappaple = Mapper(movieRepository: movieRepository) + + private lazy var boxOfficeUseCase: BoxOfficeUseCaseProtocol = BoxOfficeUseCase(mapper: mapper) +} + +extension DependencyEnvironment: ViewControllerFactoryProtocol { + func makeViewController(for type: ViewControllerType) -> UIViewController { + switch type { + case .boxOffice: + return BoxOfficeViewController(boxOfficeUseCase: boxOfficeUseCase) + } + } +} diff --git a/BoxOffice/App/SceneDelegate.swift b/BoxOffice/App/SceneDelegate.swift new file mode 100644 index 00000000..830b4397 --- /dev/null +++ b/BoxOffice/App/SceneDelegate.swift @@ -0,0 +1,18 @@ + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + private let viewControllerFactory:ViewControllerFactoryProtocol = DependencyEnvironment() + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + + guard let windowScene = (scene as? UIWindowScene) else { return } + + let mainViewController = UINavigationController(rootViewController: viewControllerFactory.makeViewController(for: .boxOffice)) + + window = UIWindow(windowScene: windowScene) + window?.makeKeyAndVisible() + window?.rootViewController = mainViewController + } +} diff --git a/BoxOffice/App/ViewControllerFactoryProtocol.swift b/BoxOffice/App/ViewControllerFactoryProtocol.swift new file mode 100644 index 00000000..b114079f --- /dev/null +++ b/BoxOffice/App/ViewControllerFactoryProtocol.swift @@ -0,0 +1,6 @@ + +import UIKit + +protocol ViewControllerFactoryProtocol { + func makeViewController(for type: ViewControllerType) -> UIViewController +} diff --git a/BoxOffice/AppDelegate.swift b/BoxOffice/AppDelegate.swift deleted file mode 100644 index f321c71b..00000000 --- a/BoxOffice/AppDelegate.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppDelegate.swift -// BoxOffice -// -// Created by kjs on 13/01/23. -// - -import UIKit - -@main -class AppDelegate: UIResponder, UIApplicationDelegate { - - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. - 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. - return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - - -} - diff --git a/BoxOffice/Base.lproj/Main.storyboard b/BoxOffice/Base.lproj/Main.storyboard deleted file mode 100644 index 25a76385..00000000 --- a/BoxOffice/Base.lproj/Main.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/BoxOffice/Common/DateExtension.swift b/BoxOffice/Common/DateExtension.swift new file mode 100644 index 00000000..1486de66 --- /dev/null +++ b/BoxOffice/Common/DateExtension.swift @@ -0,0 +1,20 @@ + +import Foundation + +extension Date { + static var yesterday: Date { return Date().dayBefore } + + var dayBefore: Date { + return Calendar.current.date(byAdding: .day, value: -1, to: noon)! + } + + var noon: Date { + return Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: self)! + } + + func formattedDate(withFormat format: String) -> String { + let formatter = DateFormatter() + formatter.dateFormat = format + return formatter.string(from: self) + } +} diff --git a/BoxOffice/Common/SynchronizedLockPropertyWrapper.swift b/BoxOffice/Common/SynchronizedLockPropertyWrapper.swift new file mode 100644 index 00000000..faeac980 --- /dev/null +++ b/BoxOffice/Common/SynchronizedLockPropertyWrapper.swift @@ -0,0 +1,26 @@ + +import Foundation + +/// 참고: https://medium.com/@vyacheslavansimov/swift-utilities-thread-safe-property-5498afc2eb53 +@propertyWrapper +struct SynchronizedLock { + private var value: Value + private var lock = NSLock() + + var wrappedValue: Value { + get { lock.synchronized { value } } + set { lock.synchronized { value = newValue } } + } + + init(wrappedValue value: Value) { + self.value = value + } +} + +private extension NSLock { + func synchronized(_ block: () -> T) -> T { + lock() + defer { unlock() } + return block() + } +} diff --git a/BoxOffice/Data/DTO/BoxOfficeDTO.swift b/BoxOffice/Data/DTO/BoxOfficeDTO.swift new file mode 100644 index 00000000..47ebe93e --- /dev/null +++ b/BoxOffice/Data/DTO/BoxOfficeDTO.swift @@ -0,0 +1,49 @@ + +// MARK: - BoxOfficeData +struct BoxOfficeDTO: Decodable { + var boxOfficeResult: BoxOfficeResultDTO +} + +// MARK: - BoxOfficeResult +struct BoxOfficeResultDTO: Decodable { + let boxofficeType, showRange: String + let dailyBoxOfficeList: [DailyBoxOfficeDTO] +} + +// MARK: - DailyBoxOfficeList +struct DailyBoxOfficeDTO: Decodable { + let rankNumber, rank, rankIntensity: String + let rankOldAndNew: RankOldAndNewDTO + let movieCode, movieName, openDate, salesAmount: String + let salesShare, salesIntensty, salesChange, salesAccount: String + let audienceCount, audienceIntenstity, audienceChange, audienceAccount: String + let screenCount, showCount: String + + private enum CodingKeys: String, CodingKey { + case rankNumber = "rnum" + case rank + case rankIntensity = "rankInten" + case rankOldAndNew + case movieCode = "movieCd" + case movieName = "movieNm" + case openDate = "openDt" + case salesAmount = "salesAmt" + case salesShare + case salesIntensty = "salesInten" + case salesChange + case salesAccount = "salesAcc" + case audienceCount = "audiCnt" + case audienceIntenstity = "audiInten" + case audienceChange = "audiChange" + case audienceAccount = "audiAcc" + case screenCount = "scrnCnt" + case showCount = "showCnt" + } +} + +enum RankOldAndNewDTO: String, Decodable { + case new = "NEW" + case old = "OLD" +} + + diff --git a/BoxOffice/Data/DTO/DetailMovieInfoDTO.swift b/BoxOffice/Data/DTO/DetailMovieInfoDTO.swift new file mode 100644 index 00000000..dda92e84 --- /dev/null +++ b/BoxOffice/Data/DTO/DetailMovieInfoDTO.swift @@ -0,0 +1,131 @@ + +import Foundation + +// MARK: - DetailMovieInfoDTO +struct DetailMovieInfoDTO: Decodable { + let movieInfoResult: MovieInfoResult +} + +// MARK: - MovieInfoResult +struct MovieInfoResult: Decodable { + let movieInfo: MovieInfo + let source: String +} + +// MARK: - MovieInfo +struct MovieInfo: Decodable { + let movieCode, movieName, movieNameEnglish, movieNameOriginal: String + let showTime, productionYear, openDate, productionStatusName: String + let typeName: String + let nations: [Nation] + let genres: [Genre] + let directors: [Director] + let actors: [Actors] + let showTypes: [ShowType] + let companies: [Company] + let audits: [Audit] + let staffs: [Staff] + + private enum CodingKeys: String, CodingKey { + case movieCode = "movieCd" + case movieName = "movieNm" + case movieNameEnglish = "movieNmEn" + case movieNameOriginal = "movieNmOg" + case showTime = "showTm" + case productionYear = "prdtYear" + case openDate = "openDt" + case productionStatusName = "prdtStatNm" + case typeName = "typeNm" + case nations + case genres + case directors + case actors + case showTypes + case companies = "companys" + case audits + case staffs + } +} + +// MARK: - Actors +struct Actors: Decodable { + let peopleName, peopleNameEnglish, cast, castEnglish: String + + private enum CodingKeys: String, CodingKey { + case peopleName = "peopleNm" + case peopleNameEnglish = "peopleNmEn" + case cast + case castEnglish = "castEn" + } +} + +// MARK: - Audit +struct Audit: Decodable { + let auditNumber, watchGradeName: String + + private enum CodingKeys: String, CodingKey { + case auditNumber = "auditNo" + case watchGradeName = "watchGradeNm" + } +} + +// MARK: - Company +struct Company: Decodable { + let companyCode, companyName, companyNameEnglish, companyPartName: String + + private enum CodingKeys: String, CodingKey { + case companyCode = "companyCd" + case companyName = "companyNm" + case companyNameEnglish = "companyNmEn" + case companyPartName = "companyPartNm" + } +} + +// MARK: - Director +struct Director: Decodable { + let peopleName, peopleNameEnglish: String + + private enum CodingKeys: String, CodingKey { + case peopleName = "peopleNm" + case peopleNameEnglish = "peopleNmEn" + } +} + +// MARK: - Genre +struct Genre: Decodable { + let genreName: String + + private enum CodingKeys: String, CodingKey { + case genreName = "genreNm" + } +} + +// MARK: - Nation +struct Nation: Decodable { + let nationName: String + + private enum CodingKeys: String, CodingKey { + case nationName = "nationNm" + } +} + +// MARK: - ShowType +struct ShowType: Decodable { + let showTypeGroupName, showTypeName: String + + private enum CodingKeys: String, CodingKey { + case showTypeGroupName = "showTypeGroupNm" + case showTypeName = "showTypeNm" + } +} + +// MARK: - Staff +struct Staff: Decodable { + let peopleName, showTypeName, staffRoleName: String + + private enum CodingKeys: String, CodingKey { + case peopleName = "peopleNm" + case showTypeName = "peopleNmEn" + case staffRoleName = "staffRoleNm" + } +} diff --git a/BoxOffice/Data/NetworkService/Constants/KeyEnvironmentHandler.swift b/BoxOffice/Data/NetworkService/Constants/KeyEnvironmentHandler.swift new file mode 100644 index 00000000..26ffd0ca --- /dev/null +++ b/BoxOffice/Data/NetworkService/Constants/KeyEnvironmentHandler.swift @@ -0,0 +1,21 @@ + +import Foundation + +protocol APIKey { + var APIKey: String { get } +} + +struct DebugEnvironment: APIKey { + var APIKey: String + + init(resourceName: String) { + guard let filePath = Bundle.main.path(forResource: resourceName, ofType: "plist"), + let plist = NSDictionary(contentsOfFile: filePath) else { + fatalError("Couldn't find file '\(resourceName)' plist") + } + + self.APIKey = plist.object(forKey: "API_KEY") as? String ?? "" + } +} + + diff --git a/BoxOffice/Data/NetworkService/Error/NetworkError.swift b/BoxOffice/Data/NetworkService/Error/NetworkError.swift new file mode 100644 index 00000000..339770cd --- /dev/null +++ b/BoxOffice/Data/NetworkService/Error/NetworkError.swift @@ -0,0 +1,55 @@ + +import Foundation + +enum NetworkError: LocalizedError { + case urlError + case connectivity + case timeout + case serverError(statusCode: Int) + case notFound + case decodingError + case requestError + + var errorDescription: String? { + switch self { + case .urlError: + return NSLocalizedString("please check url,", comment: "") + case .connectivity: + return NSLocalizedString("please check network connectivity", comment: "") + case .timeout: + return NSLocalizedString("please check network connectivity", comment: "") + case .serverError(let statusCode): + return handleStatuscodeError(with: statusCode) + case .notFound: + return NSLocalizedString("Unknown error", comment: "") + case .decodingError: + return NSLocalizedString("Decoding error", comment: "") + case .requestError: + return NSLocalizedString("RequestError error", comment: "") + } + } + + private func handleStatuscodeError(with statusCode: Int) -> String { + switch statusCode { + case 400...499: + return NSLocalizedString("A request error occurred.", comment: "") + case 500...599: + return NSLocalizedString("A server error occurred.", comment: "") + default: + return NSLocalizedString("An unexpected error occurred.", comment: "") + } + } +} + +extension NetworkError: ErrorMappable { + func mapToDomainError() -> DomainError { + switch self { + case .urlError, .connectivity, .timeout, .requestError: + return .networkIssue + case .serverError, .notFound: + return .dataUnavailable + case .decodingError: + return .unknown + } + } +} diff --git a/BoxOffice/Data/NetworkService/Model/APIHostType.swift b/BoxOffice/Data/NetworkService/Model/APIHostType.swift new file mode 100644 index 00000000..4c0eba83 --- /dev/null +++ b/BoxOffice/Data/NetworkService/Model/APIHostType.swift @@ -0,0 +1,5 @@ + +enum APIHostType: String { + case kobis = "www.kobis.or.kr" + case kakao +} diff --git a/BoxOffice/Data/NetworkService/Model/HTTPMethodType.swift b/BoxOffice/Data/NetworkService/Model/HTTPMethodType.swift new file mode 100644 index 00000000..b7f87ee6 --- /dev/null +++ b/BoxOffice/Data/NetworkService/Model/HTTPMethodType.swift @@ -0,0 +1,7 @@ + +import Foundation + +enum HTTPMethodType: String { + case get = "GET" + case post = "POST" +} diff --git a/BoxOffice/Data/NetworkService/Model/NetworkResponse.swift b/BoxOffice/Data/NetworkService/Model/NetworkResponse.swift new file mode 100644 index 00000000..4bcf7e11 --- /dev/null +++ b/BoxOffice/Data/NetworkService/Model/NetworkResponse.swift @@ -0,0 +1,7 @@ + +import Foundation + +struct NetworkResponse { + let response: HTTPURLResponse + let data: Data? +} diff --git a/BoxOffice/Data/NetworkService/Model/SchemeType.swift b/BoxOffice/Data/NetworkService/Model/SchemeType.swift new file mode 100644 index 00000000..51310d58 --- /dev/null +++ b/BoxOffice/Data/NetworkService/Model/SchemeType.swift @@ -0,0 +1,5 @@ + +enum SchemeType: String { + case http = "http" + case https = "https" +} diff --git a/BoxOffice/Data/NetworkService/NetworkManager/NetworkManagable.swift b/BoxOffice/Data/NetworkService/NetworkManager/NetworkManagable.swift new file mode 100644 index 00000000..e3dc8f0c --- /dev/null +++ b/BoxOffice/Data/NetworkService/NetworkManager/NetworkManagable.swift @@ -0,0 +1,6 @@ + +import Foundation + +protocol NetworkManagerProtocol { + func performRequest(from request: URLRequest) async -> Result +} diff --git a/BoxOffice/Data/NetworkService/NetworkManager/NetworkManager.swift b/BoxOffice/Data/NetworkService/NetworkManager/NetworkManager.swift new file mode 100644 index 00000000..dbb2116f --- /dev/null +++ b/BoxOffice/Data/NetworkService/NetworkManager/NetworkManager.swift @@ -0,0 +1,25 @@ + +import Foundation + +final class NetworkManager: NetworkManagerProtocol { + private let session: SessionProvidable + private let decoder: DecoderProtocol + + init(sessionProvider: SessionProvidable, decoder: DecoderProtocol) { + self.session = sessionProvider + self.decoder = decoder + } + + func performRequest(from request: URLRequest) async -> Result { + + let result = await session.requestAPI(using: request) + + switch result { + case .success(let networkResponse): + guard let data = networkResponse.data else { return .failure(.connectivity) } + return decoder.decode(data) + case .failure(let networkError): + return .failure(networkError) + } + } +} diff --git a/BoxOffice/Data/NetworkService/RequestBuilder/RequestInformation.swift b/BoxOffice/Data/NetworkService/RequestBuilder/RequestInformation.swift new file mode 100644 index 00000000..e970d2d8 --- /dev/null +++ b/BoxOffice/Data/NetworkService/RequestBuilder/RequestInformation.swift @@ -0,0 +1,34 @@ + +import Foundation + +enum RequestInformation { + case dailyMovie + case detailMovie(code: String) + + var url: URL? { + switch self { + case .dailyMovie: + return EndPoint(urlInformation: .daily(date: Date().dayBefore.formattedDate(withFormat: "yyyyMMdd")), apiHost: .kobis, scheme: .https).url + case .detailMovie(let code): + return EndPoint(urlInformation: .detail(code: code), apiHost: .kobis, scheme: .https).url + } + } + + var httpMethod: String { + switch self { + case .dailyMovie: + return HTTPMethodType.get.rawValue + case .detailMovie: + return HTTPMethodType.get.rawValue + } + } + + var allHTTPHeaderFields: [String : String] { + switch self { + case .dailyMovie: + return [:] + case .detailMovie: + return [:] + } + } +} diff --git a/BoxOffice/Data/NetworkService/RequestBuilder/RequestProvidable.swift b/BoxOffice/Data/NetworkService/RequestBuilder/RequestProvidable.swift new file mode 100644 index 00000000..814a513b --- /dev/null +++ b/BoxOffice/Data/NetworkService/RequestBuilder/RequestProvidable.swift @@ -0,0 +1,9 @@ + +import Foundation + +protocol RequestProvidable { + var request: URLRequest? { get } +} + + + diff --git a/BoxOffice/Data/NetworkService/RequestBuilder/RequestProvider.swift b/BoxOffice/Data/NetworkService/RequestBuilder/RequestProvider.swift new file mode 100644 index 00000000..70789ed4 --- /dev/null +++ b/BoxOffice/Data/NetworkService/RequestBuilder/RequestProvider.swift @@ -0,0 +1,19 @@ + +import Foundation + +final class RequestProvider: RequestProvidable { + private let requestInformation: RequestInformation + + init(requestInformation: RequestInformation) { + self.requestInformation = requestInformation + } + + var request: URLRequest? { + guard let requestUrl = requestInformation.url else { return nil } + var urlRequest = URLRequest(url: requestUrl) + urlRequest.httpMethod = requestInformation.httpMethod + urlRequest.allHTTPHeaderFields = requestInformation.allHTTPHeaderFields + + return urlRequest + } +} diff --git a/BoxOffice/Data/NetworkService/Serializer/JsonDecodableProtocol.swift b/BoxOffice/Data/NetworkService/Serializer/JsonDecodableProtocol.swift new file mode 100644 index 00000000..66dbf30c --- /dev/null +++ b/BoxOffice/Data/NetworkService/Serializer/JsonDecodableProtocol.swift @@ -0,0 +1,7 @@ + +import Foundation + +protocol DecoderProtocol { + func decode(_ data: Data) -> Result + +} diff --git a/BoxOffice/Data/NetworkService/Serializer/JsonDecoder.swift b/BoxOffice/Data/NetworkService/Serializer/JsonDecoder.swift new file mode 100644 index 00000000..02d45a08 --- /dev/null +++ b/BoxOffice/Data/NetworkService/Serializer/JsonDecoder.swift @@ -0,0 +1,16 @@ + +import Foundation + +struct JsonDecoder: DecoderProtocol { + private let jsonDecoder: JSONDecoder + + init(jsonDecoder: JSONDecoder) { + self.jsonDecoder = jsonDecoder + } + + func decode(_ data: Data) -> Result { + guard let decodedData = try? jsonDecoder.decode(T.self, from: data) + else { return .failure(.decodingError) } + return .success(decodedData) + } +} diff --git a/BoxOffice/Data/NetworkService/SessionProvider/SessionProvidable.swift b/BoxOffice/Data/NetworkService/SessionProvider/SessionProvidable.swift new file mode 100644 index 00000000..42e89d21 --- /dev/null +++ b/BoxOffice/Data/NetworkService/SessionProvider/SessionProvidable.swift @@ -0,0 +1,6 @@ + +import Foundation + +protocol SessionProvidable { + func requestAPI (using urlRequest: URLRequest) async -> Result +} diff --git a/BoxOffice/Data/NetworkService/SessionProvider/SessionProvider.swift b/BoxOffice/Data/NetworkService/SessionProvider/SessionProvider.swift new file mode 100644 index 00000000..cd9a09ea --- /dev/null +++ b/BoxOffice/Data/NetworkService/SessionProvider/SessionProvider.swift @@ -0,0 +1,23 @@ + +import Foundation + +final class SessionProvider: SessionProvidable { + private let session: URLSession + + init(session: URLSession = .shared) { + self.session = session + } + + func requestAPI(using urlRequest: URLRequest) async -> Result { + guard let (data, response) = try? await session.data(for: urlRequest) + else { return .failure(.connectivity) } + + guard let httpResponse = response as? HTTPURLResponse + else { return .failure(.notFound) } + + guard 200...299 ~= httpResponse.statusCode + else { return .failure(.serverError(statusCode: httpResponse.statusCode)) } + + return .success(NetworkResponse(response: httpResponse, data: data)) + } +} diff --git a/BoxOffice/Data/NetworkService/URLBuilder/EndPoint.swift b/BoxOffice/Data/NetworkService/URLBuilder/EndPoint.swift new file mode 100644 index 00000000..c54e1fbe --- /dev/null +++ b/BoxOffice/Data/NetworkService/URLBuilder/EndPoint.swift @@ -0,0 +1,23 @@ + +import Foundation + +struct EndPoint: EndPointMakable { + private let apiHost: String + private let urlInformation: URLInformation + private let scheme: String + + init(urlInformation: URLInformation, apiHost: APIHostType, scheme: SchemeType) { + self.urlInformation = urlInformation + self.apiHost = apiHost.rawValue + self.scheme = scheme.rawValue + } + + var url: URL? { + var urlComponent = URLComponents() + urlComponent.scheme = scheme + urlComponent.host = apiHost + urlComponent.path = urlInformation.path + urlComponent.queryItems = urlInformation.queryItems + return urlComponent.url + } +} diff --git a/BoxOffice/Data/NetworkService/URLBuilder/EndPointMakable.swift b/BoxOffice/Data/NetworkService/URLBuilder/EndPointMakable.swift new file mode 100644 index 00000000..a2a92350 --- /dev/null +++ b/BoxOffice/Data/NetworkService/URLBuilder/EndPointMakable.swift @@ -0,0 +1,6 @@ + +import Foundation + +protocol EndPointMakable { + var url: URL? { get } +} diff --git a/BoxOffice/Data/NetworkService/URLBuilder/URLInformation.swift b/BoxOffice/Data/NetworkService/URLBuilder/URLInformation.swift new file mode 100644 index 00000000..d1cd8433 --- /dev/null +++ b/BoxOffice/Data/NetworkService/URLBuilder/URLInformation.swift @@ -0,0 +1,31 @@ + +import Foundation + +enum URLInformation { + case daily(date: String) + case detail(code: String) + + var path: String { + switch self { + case .daily: + return "/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json" + case .detail: + return "/kobisopenapi/webservice/rest/movie/searchMovieInfo.json" + } + } + + var queryItems: [URLQueryItem] { + var urlQueryItems: [URLQueryItem] = [] + + switch self { + case .daily(let date): + urlQueryItems.append(URLQueryItem(name: "key", value: ENV.APIKey)) + urlQueryItems.append(URLQueryItem(name: "targetDt", value: date)) + case .detail(let code): + urlQueryItems.append(URLQueryItem(name: "key", value: ENV.APIKey)) + urlQueryItems.append(URLQueryItem(name: "movieCd", value: code)) + } + + return urlQueryItems + } +} diff --git a/BoxOffice/Data/Repository/MovieRepository.swift b/BoxOffice/Data/Repository/MovieRepository.swift new file mode 100644 index 00000000..68e621b6 --- /dev/null +++ b/BoxOffice/Data/Repository/MovieRepository.swift @@ -0,0 +1,58 @@ + +import Foundation + +final class MovieRepository: MovieRepositoryProtocol { + private let sessionProvider: SessionProvidable + private let decoder: DecoderProtocol + + init(sessionProvider: SessionProvidable, decoder: DecoderProtocol) { + self.sessionProvider = sessionProvider + self.decoder = decoder + } + + func requestBoxOfficeData() async -> T? { + guard let request = RequestProvider(requestInformation: .dailyMovie).request else { + return nil + } + return await requestAPI(request: request) + } + + func requestDetailMovieData(movieCode: String) async -> T? { + guard let request = RequestProvider(requestInformation: .detailMovie(code: movieCode)).request else { + return nil + } + return await requestAPI(request: request) + } + + private func requestAPI(request: URLRequest) async -> T? { + let result: Result = await sessionProvider.requestAPI(using: request) + + switch result { + case .success(let networkResponse): + guard let data = networkResponse.data else { return nil } + return decode(data) + + case .failure(let networkError): + logNetworkError(networkError) + return nil + } + } + + private func decode(_ data: Data) -> T? { + let decodedResult: Result = decoder.decode(data) + + switch decodedResult { + case .success(let decodedData): + return decodedData + case .failure(_): + return nil + } + } +} + +private func logNetworkError(_ error: NetworkError) { + print("Network Error: \(error.localizedDescription)") +} + + + diff --git a/BoxOffice/Data/Repository/MovieRepositoryProtocol.swift b/BoxOffice/Data/Repository/MovieRepositoryProtocol.swift new file mode 100644 index 00000000..cbc88d45 --- /dev/null +++ b/BoxOffice/Data/Repository/MovieRepositoryProtocol.swift @@ -0,0 +1,7 @@ + +protocol MovieRepositoryProtocol { + func requestBoxOfficeData() async -> T? + func requestDetailMovieData(movieCode: String) async -> T? +} + + diff --git a/BoxOffice/Domain/Entity/BoxOfficeMovie.swift b/BoxOffice/Domain/Entity/BoxOfficeMovie.swift new file mode 100644 index 00000000..eb3e5996 --- /dev/null +++ b/BoxOffice/Domain/Entity/BoxOfficeMovie.swift @@ -0,0 +1,15 @@ + +import Foundation + +struct BoxOfficeMovie { + let name: String + let releaseDate: String + let rank: String + let salesAmount: String + let movieCode: String + let dalilyAudience: String + let cumulateAudience: String + let rankChange: String + let isNew: Bool +} + diff --git a/BoxOffice/Domain/Entity/MovieDetailInfo.swift b/BoxOffice/Domain/Entity/MovieDetailInfo.swift new file mode 100644 index 00000000..e9e0b87f --- /dev/null +++ b/BoxOffice/Domain/Entity/MovieDetailInfo.swift @@ -0,0 +1,7 @@ + +import Foundation + +struct MovieDetailInfo { + let movieName: String + let openDate: String +} diff --git a/BoxOffice/Domain/Error/DomainError.swift b/BoxOffice/Domain/Error/DomainError.swift new file mode 100644 index 00000000..1b4b6d84 --- /dev/null +++ b/BoxOffice/Domain/Error/DomainError.swift @@ -0,0 +1,19 @@ + +import Foundation + +enum DomainError: LocalizedError { + case networkIssue + case dataUnavailable + case unknown + + var errorDescription: String? { + switch self { + case .networkIssue: + return NSLocalizedString("please check network status again,", comment: "") + case .dataUnavailable: + return NSLocalizedString("data not found", comment: "") + case .unknown: + return NSLocalizedString("unknown error.", comment: "") + } + } +} diff --git a/BoxOffice/Domain/Mapper/MappableProtocol.swift b/BoxOffice/Domain/Mapper/MappableProtocol.swift new file mode 100644 index 00000000..f21077e3 --- /dev/null +++ b/BoxOffice/Domain/Mapper/MappableProtocol.swift @@ -0,0 +1,11 @@ + +import Foundation + +protocol Mappaple { + func mapBoxOfficeMovieData() async -> [BoxOfficeMovie] + func mapBoxOfficeDetailData(movie: String) async -> MovieDetailInfo +} + +protocol ErrorMappable { + func mapToDomainError() -> DomainError +} diff --git a/BoxOffice/Domain/Mapper/Mapper.swift b/BoxOffice/Domain/Mapper/Mapper.swift new file mode 100644 index 00000000..9b340512 --- /dev/null +++ b/BoxOffice/Domain/Mapper/Mapper.swift @@ -0,0 +1,34 @@ + +import Foundation + +final class Mapper: Mappaple { + private var boxOfficeData: BoxOfficeDTO? + private var detailBoxOfficeData: DetailMovieInfoDTO? + private let movieRepository: MovieRepositoryProtocol + + init(movieRepository: MovieRepositoryProtocol) { + self.movieRepository = movieRepository + } + + func mapBoxOfficeMovieData() async -> [BoxOfficeMovie] { + boxOfficeData = await movieRepository.requestBoxOfficeData() + guard let data = boxOfficeData else { fatalError() } + return data.boxOfficeResult.dailyBoxOfficeList.map { + BoxOfficeMovie(name: $0.movieName, + releaseDate: $0.openDate, + rank: $0.rank, + salesAmount: $0.salesAmount, + movieCode: $0.movieCode, + dalilyAudience: $0.audienceCount, + cumulateAudience: $0.audienceAccount, + rankChange: $0.rankIntensity, + isNew: $0.rankOldAndNew == RankOldAndNewDTO.new ? true : false) + } + } + + func mapBoxOfficeDetailData(movie: String) async -> MovieDetailInfo { + detailBoxOfficeData = await movieRepository.requestDetailMovieData(movieCode: movie) + guard let data = detailBoxOfficeData else { fatalError() } + return MovieDetailInfo(movieName: data.movieInfoResult.movieInfo.movieName, openDate: data.movieInfoResult.movieInfo.openDate) + } +} diff --git a/BoxOffice/Domain/UseCase/BoxOfficeUseCase.swift b/BoxOffice/Domain/UseCase/BoxOfficeUseCase.swift new file mode 100644 index 00000000..af7f781a --- /dev/null +++ b/BoxOffice/Domain/UseCase/BoxOfficeUseCase.swift @@ -0,0 +1,18 @@ + +import Foundation + +final class BoxOfficeUseCase: BoxOfficeUseCaseProtocol { + private let mapper: Mappaple + + init(mapper: Mappaple) { + self.mapper = mapper + } + + func fetchBoxOfficeData() async -> [BoxOfficeMovie] { + return await mapper.mapBoxOfficeMovieData() + } + + func fetchDetailMovieData(movie: String) async -> MovieDetailInfo { + return await mapper.mapBoxOfficeDetailData(movie: movie) + } +} diff --git a/BoxOffice/Domain/UseCase/BoxOfficeUseCaseProtocol.swift b/BoxOffice/Domain/UseCase/BoxOfficeUseCaseProtocol.swift new file mode 100644 index 00000000..ef2b8141 --- /dev/null +++ b/BoxOffice/Domain/UseCase/BoxOfficeUseCaseProtocol.swift @@ -0,0 +1,8 @@ + +import Foundation + +protocol BoxOfficeUseCaseProtocol { + func fetchBoxOfficeData() async -> [BoxOfficeMovie] + func fetchDetailMovieData(movie: String) async -> MovieDetailInfo +} + diff --git a/BoxOffice/Presentation/Common/ViewControllerExtension.swift b/BoxOffice/Presentation/Common/ViewControllerExtension.swift new file mode 100644 index 00000000..83eb1a64 --- /dev/null +++ b/BoxOffice/Presentation/Common/ViewControllerExtension.swift @@ -0,0 +1,20 @@ + +import UIKit + +extension UIViewController { + // MARK: - Alert Controller + func presentAlert(title: String, + message: String, + confirmTitle: String, + confirmAction: ((UIAlertAction) -> Void)? = nil, + completion: (() -> Void)? = nil) { + let alertViewController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + let confirmAction = UIAlertAction(title: confirmTitle, style: .default) + alertViewController.addAction(confirmAction) + + self.present(alertViewController, animated: true, completion: completion) + } +} + + diff --git a/BoxOffice/Presentation/Controller/BoxOfficeViewController.swift b/BoxOffice/Presentation/Controller/BoxOfficeViewController.swift new file mode 100644 index 00000000..2049ebf5 --- /dev/null +++ b/BoxOffice/Presentation/Controller/BoxOfficeViewController.swift @@ -0,0 +1,143 @@ + +import UIKit + +final class BoxOfficeViewController: UIViewController { + private let boxOfficeUseCase: BoxOfficeUseCaseProtocol + + @SynchronizedLock private var movies = [BoxOfficeDisplayModel]() + private var fetchTask: Task? + + private var boxOfficeCollectionView: BoxOfficeCollectionView! + private var dataSource: UICollectionViewDiffableDataSource! + private var cellRegistration: UICollectionView.CellRegistration! + + init(boxOfficeUseCase: BoxOfficeUseCaseProtocol) { + self.boxOfficeUseCase = boxOfficeUseCase + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { fatalError() } + + deinit { + fetchTask?.cancel() + print("\(Self.description()) \(#function)") + } +} + +// MARK: - 생명주기 +extension BoxOfficeViewController { + override func loadView() { + self.boxOfficeCollectionView = BoxOfficeCollectionView(frame: .zero) + self.view = boxOfficeCollectionView + + } + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + configureDataSource() + fetchBoxOfficeData() + fetchBoxOfficeDetailData() + } +} + +// MARK: - Setup UI +private extension BoxOfficeViewController { + func setupUI() { + configureCellRegistration() + configureNavigationBar() + setupRefreshControl() + } + + func configureNavigationBar() { + navigationItem.title = Date().formattedDate(withFormat: "YYYY-MM-dd") + } + + func configureCellRegistration() { + cellRegistration = UICollectionView.CellRegistration { (cell, indexPath, movie) in + cell.accessories = [.disclosureIndicator()] + cell.rankLabel.text = movie.rank + cell.movieNameLabel.text = movie.movieName + guard let rankIntensity = Int(movie.rankIntensity) else { return } + cell.showRankIntensity(of: movie.isNew, with: rankIntensity) + cell.showAudienceAccount(of: movie) + } + } + + func setupRefreshControl() { + let refreshControl = UIRefreshControl() + refreshControl.addTarget(self, action: #selector(refreshBoxOfficeData), for: .valueChanged) + boxOfficeCollectionView.refreshControl = refreshControl + } + + @objc private func refreshBoxOfficeData() { + fetchBoxOfficeData() + } +} + +// MARK: - Fetch Data +private extension BoxOfficeViewController { + func fetchBoxOfficeData() { + fetchTask = Task { + let result = await boxOfficeUseCase.fetchBoxOfficeData() + handleBoxOfficeResult(result) + } + self.boxOfficeCollectionView.refreshControl?.endRefreshing() + } + + func handleBoxOfficeResult(_ result: [BoxOfficeMovie]?) { + guard let boxOfficeMovies = result else { return } + let displayMovies = mapEntityToDisplayModel(boxOfficeMovies) + self.movies = displayMovies + applySnapshot(movies: displayMovies, animatingDifferences: true) + } + + func fetchBoxOfficeDetailData() { + fetchTask = Task { + let result = await boxOfficeUseCase.fetchDetailMovieData(movie: "20236488") + handleBoxOfficeDetailResult(result) + } + } + + func handleBoxOfficeDetailResult(_ result: MovieDetailInfo) { + print(result) + } + + @MainActor + func presentAlert(error: DomainError) { + self.presentAlert(title: "네트워크 오류", message: "네트워크에 문제가 있습니다 \(error)", confirmTitle: "확인") + } + + func mapEntityToDisplayModel(_ boxOfficeMovies: [BoxOfficeMovie]) -> [BoxOfficeDisplayModel] { + return boxOfficeMovies.map { + BoxOfficeDisplayModel( + rank: $0.rank, + rankIntensity: $0.rankChange, + isNew: $0.isNew, + movieName: $0.name, + audienceCount: $0.dalilyAudience, + audienceAccount: $0.cumulateAudience)} + } +} + +// MARK: - Apply Diffable DataSource +private extension BoxOfficeViewController { + func configureDataSource() { + dataSource = UICollectionViewDiffableDataSource(collectionView: boxOfficeCollectionView) { + (collectionView, indexPath, movie) -> UICollectionViewCell? in + return collectionView.dequeueConfiguredReusableCell(using: self.cellRegistration, for: indexPath, item: movie) + } + let loadingPlaceholder = [BoxOfficeDisplayModel.placeholder] + var initialSnapshot = NSDiffableDataSourceSnapshot() + initialSnapshot.appendSections([.main]) + initialSnapshot.appendItems(loadingPlaceholder, toSection: .main) + dataSource.apply(initialSnapshot, animatingDifferences: false) + } + + func applySnapshot(movies: [BoxOfficeDisplayModel], animatingDifferences: Bool) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(movies, toSection: .main) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + } +} diff --git a/BoxOffice/Presentation/DisplayModel/BoxOfficeDisplayModel.swift b/BoxOffice/Presentation/DisplayModel/BoxOfficeDisplayModel.swift new file mode 100644 index 00000000..5b9e9c86 --- /dev/null +++ b/BoxOffice/Presentation/DisplayModel/BoxOfficeDisplayModel.swift @@ -0,0 +1,22 @@ + +import Foundation + +enum Section { + case main +} + +struct BoxOfficeDisplayModel: Hashable { + let id: UUID = UUID() + let rank: String + let rankIntensity: String + let isNew: Bool + let movieName: String + let audienceCount: String + let audienceAccount: String +} + +extension BoxOfficeDisplayModel { + static var placeholder: BoxOfficeDisplayModel { + return BoxOfficeDisplayModel(rank: "", rankIntensity: "0", isNew: false, movieName: "Loading...", audienceCount: "", audienceAccount: "") + } +} diff --git a/BoxOffice/Presentation/View/BoxOfficeCell.swift b/BoxOffice/Presentation/View/BoxOfficeCell.swift new file mode 100644 index 00000000..f3bcdcff --- /dev/null +++ b/BoxOffice/Presentation/View/BoxOfficeCell.swift @@ -0,0 +1,160 @@ + +import UIKit + +final class BoxOfficeCell: UICollectionViewListCell { + let rankIntensityLabel = UILabel() + let audienceAccountLabel = UILabel() + + let rankLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 30.0) + return label + }() + + let movieNameLabel: UILabel = { + let label = UILabel() + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.5 + label.numberOfLines = 2 + label.lineBreakMode = .byWordWrapping + return label + }() + + private let horizontalStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.alignment = .center + stackView.distribution = .fill + stackView.spacing = 8 + return stackView + }() + + private let leftStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .center + stackView.distribution = .fill + stackView.spacing = 0 + return stackView + }() + + private let rightStackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .leading + stackView.distribution = .fillEqually + stackView.spacing = 0 + return stackView + }() + + private let separatorView = UIView() + + override init(frame: CGRect) { + super.init(frame: frame) + configureLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension BoxOfficeCell { + private func configureLayout() { + configureLabels() + configureStackViews() + configureHorizontalStackView() + configureSeparatorView() + } + + private func configureLabels() { + [rankLabel, rankIntensityLabel, movieNameLabel, audienceAccountLabel].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.adjustsFontForContentSizeCategory = true + } + } + + private func configureStackViews() { + [leftStackView, rightStackView, horizontalStackView].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + $0.spacing = 0 + } + + [rankLabel, rankIntensityLabel].forEach { + leftStackView.addArrangedSubview($0) + } + + [movieNameLabel, audienceAccountLabel].forEach { + rightStackView.addArrangedSubview($0) + } + + [leftStackView, rightStackView].forEach { + horizontalStackView.addArrangedSubview($0) + } + + addSubview(horizontalStackView) + } + + private func configureHorizontalStackView() { + NSLayoutConstraint.activate([ + horizontalStackView.centerXAnchor.constraint(equalTo: centerXAnchor), + horizontalStackView.centerYAnchor.constraint(equalTo: centerYAnchor), + horizontalStackView.widthAnchor.constraint(equalTo: widthAnchor), + horizontalStackView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.7) + ]) + + let leftStackWidthConstraint = leftStackView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.2) + leftStackWidthConstraint.priority = .defaultHigh + leftStackWidthConstraint.isActive = true + } + + private func configureSeparatorView() { + addSubview(separatorView) + + separatorView.backgroundColor = .opaqueSeparator + separatorView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + separatorView.heightAnchor.constraint(equalToConstant: 0.3), + separatorView.leadingAnchor.constraint(equalTo: leadingAnchor), + separatorView.trailingAnchor.constraint(equalTo: trailingAnchor), + separatorView.topAnchor.constraint(equalTo: topAnchor), + ]) + } + + func showRankIntensity(of isNew: Bool, with rankNumber: Int) { + if isNew { + rankIntensityLabel.textColor = .red + rankIntensityLabel.text = "신작" + return + } + + switch rankNumber { + case let x where x > 0: + matchRankIntensity(for: rankNumber, withImage: "arrowtriangle.up.fill", setColor: .red) + case let x where x < 0: + matchRankIntensity(for: rankNumber, withImage: "arrowtriangle.down.fill", setColor: .blue) + default: + rankIntensityLabel.text = "-" + } + } + + private func matchRankIntensity(for number: Int, withImage image: String, setColor color: UIColor) { + let imageAttachement = NSTextAttachment() + imageAttachement.image = UIImage(systemName: image)?.withTintColor(color, renderingMode: .alwaysTemplate) + let attributedString = NSMutableAttributedString(attachment: imageAttachement) + attributedString.append(NSAttributedString(string: String(abs(number)))) + rankIntensityLabel.attributedText = attributedString + } + + func showAudienceAccount(of cell: BoxOfficeDisplayModel) { + audienceAccountLabel.text = "오늘 \(self.numberFormatter(for: cell.audienceCount)) / 총 \(self.numberFormatter(for: cell.audienceAccount))" + } + + private func numberFormatter(for data: String) -> String { + let numberFormatter: NumberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + guard let result = numberFormatter.string(from: NSNumber(value: Double(data) ?? 0)) else { return "error" } + return result + } +} diff --git a/BoxOffice/Presentation/View/BoxOfficeCollectionView.swift b/BoxOffice/Presentation/View/BoxOfficeCollectionView.swift new file mode 100644 index 00000000..232caba3 --- /dev/null +++ b/BoxOffice/Presentation/View/BoxOfficeCollectionView.swift @@ -0,0 +1,34 @@ + +import UIKit + +final class BoxOfficeCollectionView: UICollectionView { + override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { + super.init(frame: frame, collectionViewLayout: boxOfficeViewLayout) + configureCollectionView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureCollectionView() { + self.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.backgroundColor = .systemBackground + } + + let boxOfficeViewLayout: UICollectionViewLayout = { + let estimatedHeight = CGFloat(78) + let layoutSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), + heightDimension: .estimated(estimatedHeight)) + let item = NSCollectionLayoutItem(layoutSize: layoutSize) + let group = NSCollectionLayoutGroup.horizontal(layoutSize: layoutSize, + subitem: item, + count: 1) + let section = NSCollectionLayoutSection(group: group) + section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + section.interGroupSpacing = 0 + let layout = UICollectionViewCompositionalLayout(section: section) + return layout + }() +} + diff --git a/BoxOffice/Assets.xcassets/AccentColor.colorset/Contents.json b/BoxOffice/Resource/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from BoxOffice/Assets.xcassets/AccentColor.colorset/Contents.json rename to BoxOffice/Resource/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/BoxOffice/Assets.xcassets/AppIcon.appiconset/Contents.json b/BoxOffice/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from BoxOffice/Assets.xcassets/AppIcon.appiconset/Contents.json rename to BoxOffice/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/BoxOffice/Assets.xcassets/Contents.json b/BoxOffice/Resource/Assets.xcassets/Contents.json similarity index 100% rename from BoxOffice/Assets.xcassets/Contents.json rename to BoxOffice/Resource/Assets.xcassets/Contents.json diff --git a/BoxOffice/Resource/BoxOfficeSample.json b/BoxOffice/Resource/BoxOfficeSample.json new file mode 100644 index 00000000..6c59982a --- /dev/null +++ b/BoxOffice/Resource/BoxOfficeSample.json @@ -0,0 +1 @@ +{"boxOfficeResult":{"boxofficeType":"일별 박스오피스","showRange":"20220105~20220105","dailyBoxOfficeList":[{"rnum":"1","rank":"1","rankInten":"0","rankOldAndNew":"NEW","movieCd":"20199882","movieNm":"경관의 피","openDt":"2022-01-05","salesAmt":"584559330","salesShare":"34.2","salesInten":"584559330","salesChange":"100","salesAcc":"631402330","audiCnt":"64050","audiInten":"64050","audiChange":"100","audiAcc":"69228","scrnCnt":"1171","showCnt":"4416"},{"rnum":"2","rank":"2","rankInten":"-1","rankOldAndNew":"OLD","movieCd":"20210028","movieNm":"스파이더맨: 노 웨이 홈","openDt":"2021-12-15","salesAmt":"507028380","salesShare":"29.6","salesInten":"-91443730","salesChange":"-15.3","salesAcc":"62772471900","audiCnt":"50399","audiInten":"-9564","audiChange":"-15.9","audiAcc":"6252827","scrnCnt":"1357","showCnt":"4314"},{"rnum":"3","rank":"3","rankInten":"0","rankOldAndNew":"NEW","movieCd":"20218764","movieNm":"씽2게더","openDt":"2022-01-05","salesAmt":"379941010","salesShare":"22.2","salesInten":"379941010","salesChange":"100","salesAcc":"384962010","audiCnt":"44221","audiInten":"44221","audiChange":"100","audiAcc":"44778","scrnCnt":"999","showCnt":"3156"},{"rnum":"4","rank":"4","rankInten":"-2","rankOldAndNew":"OLD","movieCd":"20194403","movieNm":"킹스맨: 퍼스트 에이전트","openDt":"2021-12-22","salesAmt":"140823730","salesShare":"8.2","salesInten":"-65077090","salesChange":"-31.6","salesAcc":"8325835830","audiCnt":"13916","audiInten":"-7219","audiChange":"-34.2","audiAcc":"835601","scrnCnt":"657","showCnt":"1481"},{"rnum":"5","rank":"5","rankInten":"-2","rankOldAndNew":"OLD","movieCd":"20217807","movieNm":"해피 뉴 이어","openDt":"2021-12-29","salesAmt":"38850000","salesShare":"2.3","salesInten":"-62624160","salesChange":"-61.7","salesAcc":"1973695930","audiCnt":"4152","audiInten":"-6835","audiChange":"-62.2","audiAcc":"211533","scrnCnt":"414","showCnt":"643"},{"rnum":"6","rank":"6","rankInten":"6","rankOldAndNew":"OLD","movieCd":"20192354","movieNm":"특송","openDt":"2022-01-12","salesAmt":"13378000","salesShare":"0.8","salesInten":"10760000","salesChange":"411","salesAcc":"30157000","audiCnt":"1213","audiInten":"923","audiChange":"318.3","audiAcc":"2984","scrnCnt":"4","showCnt":"4"},{"rnum":"7","rank":"7","rankInten":"-1","rankOldAndNew":"OLD","movieCd":"20218415","movieNm":"드라이브 마이 카","openDt":"2021-12-23","salesAmt":"9238520","salesShare":"0.5","salesInten":"-1637380","salesChange":"-15.1","salesAcc":"238182180","audiCnt":"1013","audiInten":"-156","audiChange":"-13.3","audiAcc":"25877","scrnCnt":"74","showCnt":"99"},{"rnum":"8","rank":"8","rankInten":"14","rankOldAndNew":"OLD","movieCd":"20210672","movieNm":"피드백","openDt":"2022-01-05","salesAmt":"4896500","salesShare":"0.3","salesInten":"4666500","salesChange":"2028.9","salesAcc":"8078500","audiCnt":"748","audiInten":"637","audiChange":"573.9","audiAcc":"1187","scrnCnt":"32","showCnt":"41"},{"rnum":"9","rank":"9","rankInten":"-1","rankOldAndNew":"OLD","movieCd":"20218390","movieNm":"램","openDt":"2021-12-29","salesAmt":"3244700","salesShare":"0.2","salesInten":"-3955470","salesChange":"-54.9","salesAcc":"109603940","audiCnt":"329","audiInten":"-481","audiChange":"-59.4","audiAcc":"12197","scrnCnt":"44","showCnt":"49"},{"rnum":"10","rank":"10","rankInten":"-3","rankOldAndNew":"OLD","movieCd":"20210864","movieNm":"엔칸토: 마법의 세계","openDt":"2021-11-24","salesAmt":"2290000","salesShare":"0.1","salesInten":"-7180050","salesChange":"-75.8","salesAcc":"5964273080","audiCnt":"291","audiInten":"-765","audiChange":"-72.4","audiAcc":"628878","scrnCnt":"23","showCnt":"24"}]}} \ No newline at end of file diff --git a/BoxOffice/Info.plist b/BoxOffice/Resource/Info.plist similarity index 90% rename from BoxOffice/Info.plist rename to BoxOffice/Resource/Info.plist index dd3c9afd..0eb786dc 100644 --- a/BoxOffice/Info.plist +++ b/BoxOffice/Resource/Info.plist @@ -15,8 +15,6 @@ Default Configuration UISceneDelegateClassName $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main diff --git a/BoxOffice/SceneDelegate.swift b/BoxOffice/SceneDelegate.swift deleted file mode 100644 index 1ac597ee..00000000 --- a/BoxOffice/SceneDelegate.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// SceneDelegate.swift -// BoxOffice -// -// Created by kjs on 13/01/23. -// - -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 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/ViewController.swift b/BoxOffice/ViewController.swift deleted file mode 100644 index 208e0ecf..00000000 --- a/BoxOffice/ViewController.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ViewController.swift -// BoxOffice -// -// Created by kjs on 13/01/23. -// - -import UIKit - -class ViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view. - } - - -} - diff --git a/BoxOfficeUnitTests/MockURLProtocol.swift b/BoxOfficeUnitTests/MockURLProtocol.swift new file mode 100644 index 00000000..16a0de47 --- /dev/null +++ b/BoxOfficeUnitTests/MockURLProtocol.swift @@ -0,0 +1,34 @@ + +import Foundation +import XCTest + +final class MockURLProtocol: URLProtocol { + + static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))? + + override class func canInit(with request: URLRequest) -> Bool { + return true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + XCTFail("Receive unexpected request with no handler set") + return + } + + do { + let (response, data) = try handler(request) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + client?.urlProtocol(self, didFailWithError: error) + } + } + + override func stopLoading() { } +} diff --git a/BoxOfficeUnitTests/NetworkSessionTests.swift b/BoxOfficeUnitTests/NetworkSessionTests.swift new file mode 100644 index 00000000..7e902179 --- /dev/null +++ b/BoxOfficeUnitTests/NetworkSessionTests.swift @@ -0,0 +1,72 @@ + +import XCTest +@testable import BoxOffice + +class NetworkSessionTests: XCTestCase { + + var sessionProvider: SessionProvider! + var urlSession: URLSession! + + override func setUp() { + super.setUp() + let config = URLSessionConfiguration.ephemeral + config.protocolClasses = [MockURLProtocol.self] + urlSession = URLSession(configuration: config) + + sessionProvider = SessionProvider(session: urlSession) + } + + override func tearDown() { + super.tearDown() + urlSession = nil + sessionProvider = nil + } + + func test_givenMockSuccessfulResponse_whenLoadAPIRequest_thenSuccessResult() async { + + // Given + let stubData = StubData.boxOfficeJSON + let stubReqeust = URLRequest(url: URL(string:"https://example.com")!) + let mockResponse = HTTPURLResponse(url: URL(string: "https://example.com")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil)! + + MockURLProtocol.requestHandler = { request in + return (mockResponse, stubData) + } + + // When + let result = await sessionProvider.requestAPI(using: stubReqeust) + + // Then + switch result { + case .success(let networkResponse): + XCTAssertNotNil(networkResponse.data) + XCTAssertEqual(networkResponse.response.statusCode, 200) + case .failure(let error): + XCTFail("\(error)") + } + } + + func test_givenMockConnectivityError_whenLoadAPIRequest_thenConnectivityErrorResult() async { + + // Given + let stubReqeust = URLRequest(url: URL(string:"https://example.com")!) + + MockURLProtocol.requestHandler = { request in + throw URLError(.notConnectedToInternet) + } + + // When + let result = await sessionProvider.requestAPI(using: stubReqeust) + + // Then + if case .failure(let error) = result, + case .connectivity = error { + XCTAssertTrue(true) + } else { + XCTFail("error") + } + } +} diff --git a/BoxOfficeUnitTests/StubJSONData.swift b/BoxOfficeUnitTests/StubJSONData.swift new file mode 100644 index 00000000..002425f1 --- /dev/null +++ b/BoxOfficeUnitTests/StubJSONData.swift @@ -0,0 +1,36 @@ + +import Foundation + +struct StubData { + static let boxOfficeJSON = """ + { + "boxOfficeResult": { + "boxofficeType": "일별 박스오피스", + "showRange": "20220105~20220105", + "dailyBoxOfficeList": [ + { + "rnum": "1", + "rank": "1", + "rankInten": "0", + "rankOldAndNew": "NEW", + "movieCd": "20199882", + "movieNm": "경관의 피", + "openDt": "2022-01-05", + "salesAmt": "584559330", + "salesShare": "34.2", + "salesInten": "584559330", + "salesChange": "100", + "salesAcc": "631402330", + "audiCnt": "64050", + "audiInten": "64050", + "audiChange": "100", + "audiAcc": "69228", + "scrnCnt": "1171", + "showCnt": "4416" + } + ] + } + } + """.data(using: .utf8)! +} +