diff --git a/BoxOffice.xcodeproj/project.pbxproj b/BoxOffice.xcodeproj/project.pbxproj index 77c6dab9..a4f13c40 100644 --- a/BoxOffice.xcodeproj/project.pbxproj +++ b/BoxOffice.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ 7727D2442B9AA7FF001463E1 /* ViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7727D2432B9AA7FF001463E1 /* ViewModelType.swift */; }; 7727D2462B9AAA16001463E1 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7727D2452B9AAA16001463E1 /* Observable.swift */; }; 7727D2492B9B1770001463E1 /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7727D2482B9B1770001463E1 /* UIViewController+.swift */; }; + 773B32E72BA0222400F1486A /* MovieDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773B32E62BA0222400F1486A /* MovieDetailViewController.swift */; }; + 773B32E92BA02D5100F1486A /* MovieInfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773B32E82BA02D5100F1486A /* MovieInfoTableViewCell.swift */; }; + 773B32EB2BA1365700F1486A /* MovieEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 773B32EA2BA1365700F1486A /* MovieEntity.swift */; }; 776DF5842B955A880049BF66 /* BoxOfficeUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 776DF5832B955A880049BF66 /* BoxOfficeUseCase.swift */; }; 776DF58E2B95680F0049BF66 /* BoxOfficeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 776DF58D2B95680F0049BF66 /* BoxOfficeRepository.swift */; }; 776DF5902B956A720049BF66 /* DefaultBoxOfficeRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 776DF58F2B956A720049BF66 /* DefaultBoxOfficeRepository.swift */; }; @@ -39,6 +42,18 @@ 95AC03CF2B7B21F200173FB5 /* .swiftlint.yml in Resources */ = {isa = PBXBuildFile; fileRef = 95AC03CE2B7B21F200173FB5 /* .swiftlint.yml */; }; 95AC03D42B7C4F0A00173FB5 /* BoxOfficeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AC03D32B7C4F0A00173FB5 /* BoxOfficeDTO.swift */; }; 95AC03D72B7C9A9200173FB5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 63DF20F72970E1A1005DF7D1 /* Assets.xcassets */; }; + 95B4F2FE2BA1C25600AB4952 /* MovieRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F2FD2BA1C25600AB4952 /* MovieRepository.swift */; }; + 95B4F3002BA1C32A00AB4952 /* DefaultMovieRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F2FF2BA1C32A00AB4952 /* DefaultMovieRepository.swift */; }; + 95B4F3022BA1CB5700AB4952 /* MovieDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F3012BA1CB5700AB4952 /* MovieDetailViewModel.swift */; }; + 95B4F3042BA1CC0F00AB4952 /* MovieUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F3032BA1CC0F00AB4952 /* MovieUseCase.swift */; }; + 95B4F3082BA9582700AB4952 /* MoviePosterAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F3072BA9582700AB4952 /* MoviePosterAPI.swift */; }; + 95B4F30A2BA95CD500AB4952 /* MoviePosterAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F3092BA95CD500AB4952 /* MoviePosterAPIService.swift */; }; + 95B4F30C2BA95D5D00AB4952 /* MoviePosterDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F30B2BA95D5D00AB4952 /* MoviePosterDTO.swift */; }; + 95B4F30E2BA95FFC00AB4952 /* MoviePosterEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F30D2BA95FFC00AB4952 /* MoviePosterEntity.swift */; }; + 95B4F3102BA9611000AB4952 /* DefaultMoviePosterRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F30F2BA9611000AB4952 /* DefaultMoviePosterRepository.swift */; }; + 95B4F3122BA9615C00AB4952 /* MoviePosterRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F3112BA9615C00AB4952 /* MoviePosterRepository.swift */; }; + 95B4F3142BA974F500AB4952 /* MoviePosterUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F3132BA974F500AB4952 /* MoviePosterUseCase.swift */; }; + 95B4F3162BA976DD00AB4952 /* MovieImageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B4F3152BA976DD00AB4952 /* MovieImageTableViewCell.swift */; }; 95C84DE22B8B9AD600B77048 /* BoxOfficeCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C84DE12B8B9AD600B77048 /* BoxOfficeCollectionViewCell.swift */; }; 95C84DE42B8B9DB200B77048 /* BoxOfficeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C84DE32B8B9DB200B77048 /* BoxOfficeViewController.swift */; }; 95C84DE62B8BA9E200B77048 /* BoxOfficeEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C84DE52B8BA9E200B77048 /* BoxOfficeEntity.swift */; }; @@ -66,6 +81,9 @@ 7727D2432B9AA7FF001463E1 /* ViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelType.swift; sourceTree = ""; }; 7727D2452B9AAA16001463E1 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; 7727D2482B9B1770001463E1 /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; + 773B32E62BA0222400F1486A /* MovieDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailViewController.swift; sourceTree = ""; }; + 773B32E82BA02D5100F1486A /* MovieInfoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieInfoTableViewCell.swift; sourceTree = ""; }; + 773B32EA2BA1365700F1486A /* MovieEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieEntity.swift; sourceTree = ""; }; 776DF5832B955A880049BF66 /* BoxOfficeUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeUseCase.swift; sourceTree = ""; }; 776DF58D2B95680F0049BF66 /* BoxOfficeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeRepository.swift; sourceTree = ""; }; 776DF58F2B956A720049BF66 /* DefaultBoxOfficeRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBoxOfficeRepository.swift; sourceTree = ""; }; @@ -91,6 +109,18 @@ 95AC03C22B7B0E4700173FB5 /* BoxOfficeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeTests.swift; sourceTree = ""; }; 95AC03CE2B7B21F200173FB5 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; 95AC03D32B7C4F0A00173FB5 /* BoxOfficeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeDTO.swift; sourceTree = ""; }; + 95B4F2FD2BA1C25600AB4952 /* MovieRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieRepository.swift; sourceTree = ""; }; + 95B4F2FF2BA1C32A00AB4952 /* DefaultMovieRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultMovieRepository.swift; sourceTree = ""; }; + 95B4F3012BA1CB5700AB4952 /* MovieDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieDetailViewModel.swift; sourceTree = ""; }; + 95B4F3032BA1CC0F00AB4952 /* MovieUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieUseCase.swift; sourceTree = ""; }; + 95B4F3072BA9582700AB4952 /* MoviePosterAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviePosterAPI.swift; sourceTree = ""; }; + 95B4F3092BA95CD500AB4952 /* MoviePosterAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviePosterAPIService.swift; sourceTree = ""; }; + 95B4F30B2BA95D5D00AB4952 /* MoviePosterDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviePosterDTO.swift; sourceTree = ""; }; + 95B4F30D2BA95FFC00AB4952 /* MoviePosterEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviePosterEntity.swift; sourceTree = ""; }; + 95B4F30F2BA9611000AB4952 /* DefaultMoviePosterRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultMoviePosterRepository.swift; sourceTree = ""; }; + 95B4F3112BA9615C00AB4952 /* MoviePosterRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviePosterRepository.swift; sourceTree = ""; }; + 95B4F3132BA974F500AB4952 /* MoviePosterUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviePosterUseCase.swift; sourceTree = ""; }; + 95B4F3152BA976DD00AB4952 /* MovieImageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieImageTableViewCell.swift; sourceTree = ""; }; 95C84DE12B8B9AD600B77048 /* BoxOfficeCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeCollectionViewCell.swift; sourceTree = ""; }; 95C84DE32B8B9DB200B77048 /* BoxOfficeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeViewController.swift; sourceTree = ""; }; 95C84DE52B8BA9E200B77048 /* BoxOfficeEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxOfficeEntity.swift; sourceTree = ""; }; @@ -160,19 +190,41 @@ sourceTree = ""; }; 772025652B7C7D6E00439CFD /* Presentation */ = { + isa = PBXGroup; + children = ( + 773B32E42BA021AC00F1486A /* BoxOfficeScene */, + 773B32E52BA021FF00F1486A /* MovieDetailScene */, + ); + path = Presentation; + sourceTree = ""; + }; + 773B32E42BA021AC00F1486A /* BoxOfficeScene */ = { isa = PBXGroup; children = ( 95C84DE12B8B9AD600B77048 /* BoxOfficeCollectionViewCell.swift */, 95C84DE32B8B9DB200B77048 /* BoxOfficeViewController.swift */, 95C84DE72B8BAACB00B77048 /* BoxOfficeViewModel.swift */, ); - path = Presentation; + path = BoxOfficeScene; + sourceTree = ""; + }; + 773B32E52BA021FF00F1486A /* MovieDetailScene */ = { + isa = PBXGroup; + children = ( + 773B32E62BA0222400F1486A /* MovieDetailViewController.swift */, + 773B32E82BA02D5100F1486A /* MovieInfoTableViewCell.swift */, + 95B4F3012BA1CB5700AB4952 /* MovieDetailViewModel.swift */, + 95B4F3152BA976DD00AB4952 /* MovieImageTableViewCell.swift */, + ); + path = MovieDetailScene; sourceTree = ""; }; 776DF5812B955A470049BF66 /* UseCases */ = { isa = PBXGroup; children = ( 776DF5832B955A880049BF66 /* BoxOfficeUseCase.swift */, + 95B4F3032BA1CC0F00AB4952 /* MovieUseCase.swift */, + 95B4F3132BA974F500AB4952 /* MoviePosterUseCase.swift */, ); path = UseCases; sourceTree = ""; @@ -181,6 +233,8 @@ isa = PBXGroup; children = ( 95C84DE52B8BA9E200B77048 /* BoxOfficeEntity.swift */, + 773B32EA2BA1365700F1486A /* MovieEntity.swift */, + 95B4F30D2BA95FFC00AB4952 /* MoviePosterEntity.swift */, ); path = Entities; sourceTree = ""; @@ -200,6 +254,7 @@ children = ( 95AC03D32B7C4F0A00173FB5 /* BoxOfficeDTO.swift */, 952AD2C02B8484B300598680 /* MovieDTO.swift */, + 95B4F30B2BA95D5D00AB4952 /* MoviePosterDTO.swift */, ); path = DTOs; sourceTree = ""; @@ -209,6 +264,7 @@ children = ( 7773388F2B849BFB00143672 /* MovieAPI.swift */, 952AD2CC2B84A01300598680 /* BoxOfficeAPI.swift */, + 95B4F3072BA9582700AB4952 /* MoviePosterAPI.swift */, ); path = APIs; sourceTree = ""; @@ -218,6 +274,7 @@ children = ( 7773388C2B849BBD00143672 /* MovieAPIService.swift */, 952AD2CE2B84A31500598680 /* BoxOfficeAPIService.swift */, + 95B4F3092BA95CD500AB4952 /* MoviePosterAPIService.swift */, ); path = Services; sourceTree = ""; @@ -234,6 +291,8 @@ isa = PBXGroup; children = ( 776DF58D2B95680F0049BF66 /* BoxOfficeRepository.swift */, + 95B4F2FD2BA1C25600AB4952 /* MovieRepository.swift */, + 95B4F3112BA9615C00AB4952 /* MoviePosterRepository.swift */, ); path = Repositories; sourceTree = ""; @@ -242,6 +301,8 @@ isa = PBXGroup; children = ( 776DF58F2B956A720049BF66 /* DefaultBoxOfficeRepository.swift */, + 95B4F2FF2BA1C32A00AB4952 /* DefaultMovieRepository.swift */, + 95B4F30F2BA9611000AB4952 /* DefaultMoviePosterRepository.swift */, ); path = Repositories; sourceTree = ""; @@ -467,28 +528,43 @@ 95AC03D42B7C4F0A00173FB5 /* BoxOfficeDTO.swift in Sources */, 952AD2C62B84898000598680 /* HTTPMethod.swift in Sources */, 7773388D2B849BBD00143672 /* MovieAPIService.swift in Sources */, + 773B32E92BA02D5100F1486A /* MovieInfoTableViewCell.swift in Sources */, 7773387B2B848C0200143672 /* Requestable.swift in Sources */, + 95B4F3022BA1CB5700AB4952 /* MovieDetailViewModel.swift in Sources */, 7727D2442B9AA7FF001463E1 /* ViewModelType.swift in Sources */, + 95B4F3082BA9582700AB4952 /* MoviePosterAPI.swift in Sources */, 777338852B848E0800143672 /* TargetType.swift in Sources */, 776DF58E2B95680F0049BF66 /* BoxOfficeRepository.swift in Sources */, + 95B4F3142BA974F500AB4952 /* MoviePosterUseCase.swift in Sources */, 95C84DE22B8B9AD600B77048 /* BoxOfficeCollectionViewCell.swift in Sources */, + 95B4F30E2BA95FFC00AB4952 /* MoviePosterEntity.swift in Sources */, 952AD2CD2B84A01300598680 /* BoxOfficeAPI.swift in Sources */, 95C84DE42B8B9DB200B77048 /* BoxOfficeViewController.swift in Sources */, 777338882B84961800143672 /* Date+.swift in Sources */, + 95B4F3002BA1C32A00AB4952 /* DefaultMovieRepository.swift in Sources */, 9578065F2B8C258A00D7CDFE /* String+.swift in Sources */, 95C84DE82B8BAACB00B77048 /* BoxOfficeViewModel.swift in Sources */, + 95B4F30A2BA95CD500AB4952 /* MoviePosterAPIService.swift in Sources */, 63DF20EF2970E1A0005DF7D1 /* AppDelegate.swift in Sources */, + 95B4F3162BA976DD00AB4952 /* MovieImageTableViewCell.swift in Sources */, 952AD2C42B84880A00598680 /* NetworkError.swift in Sources */, 776DF5902B956A720049BF66 /* DefaultBoxOfficeRepository.swift in Sources */, + 773B32E72BA0222400F1486A /* MovieDetailViewController.swift in Sources */, 952AD2CF2B84A31500598680 /* BoxOfficeAPIService.swift in Sources */, + 95B4F2FE2BA1C25600AB4952 /* MovieRepository.swift in Sources */, + 95B4F30C2BA95D5D00AB4952 /* MoviePosterDTO.swift in Sources */, + 95B4F3042BA1CC0F00AB4952 /* MovieUseCase.swift in Sources */, 7773387F2B848D1200143672 /* NetworkProvider.swift in Sources */, + 95B4F3122BA9615C00AB4952 /* MoviePosterRepository.swift in Sources */, 952AD2C12B8484B300598680 /* MovieDTO.swift in Sources */, 63DF20F12970E1A0005DF7D1 /* SceneDelegate.swift in Sources */, 7773388B2B849B0E00143672 /* BaseAPIService.swift in Sources */, 952AD2C82B848A4800598680 /* NetworkResult.swift in Sources */, + 773B32EB2BA1365700F1486A /* MovieEntity.swift in Sources */, 7727D2462B9AAA16001463E1 /* Observable.swift in Sources */, 777338902B849BFB00143672 /* MovieAPI.swift in Sources */, 777338832B848DE600143672 /* NetworkEnvironment.swift in Sources */, + 95B4F3102BA9611000AB4952 /* DefaultMoviePosterRepository.swift in Sources */, 776DF5842B955A880049BF66 /* BoxOfficeUseCase.swift in Sources */, 95C84DE62B8BA9E200B77048 /* BoxOfficeEntity.swift in Sources */, ); @@ -645,8 +721,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = RX53PD2JKA; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BoxOffice/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -659,8 +737,9 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.yagom.BoxOffice; + PRODUCT_BUNDLE_IDENTIFIER = com.jeto.BoxOffice; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/BoxOffice/App/AppDelegate.swift b/BoxOffice/App/AppDelegate.swift index f321c71b..9c13e23d 100644 --- a/BoxOffice/App/AppDelegate.swift +++ b/BoxOffice/App/AppDelegate.swift @@ -10,8 +10,6 @@ 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 diff --git a/BoxOffice/Data/Network/APIs/BoxOfficeAPI.swift b/BoxOffice/Data/Network/APIs/BoxOfficeAPI.swift index 5134989f..7e7a3f44 100644 --- a/BoxOffice/Data/Network/APIs/BoxOfficeAPI.swift +++ b/BoxOffice/Data/Network/APIs/BoxOfficeAPI.swift @@ -13,6 +13,10 @@ enum BoxOfficeAPI { extension BoxOfficeAPI: TargetType { + var baseURL: String { + return NetworkEnvironment.baseURL + } + var method: HTTPMethod { switch self { case .requestDailyBoxOfficeInfo(_, _): diff --git a/BoxOffice/Data/Network/APIs/MovieAPI.swift b/BoxOffice/Data/Network/APIs/MovieAPI.swift index 29763d9b..44ace5f5 100644 --- a/BoxOffice/Data/Network/APIs/MovieAPI.swift +++ b/BoxOffice/Data/Network/APIs/MovieAPI.swift @@ -13,6 +13,10 @@ enum MovieAPI { extension MovieAPI: TargetType { + var baseURL: String { + return NetworkEnvironment.baseURL + } + var method: HTTPMethod { switch self { case .requestMovieDetailInfo(_, _): diff --git a/BoxOffice/Data/Network/APIs/MoviePosterAPI.swift b/BoxOffice/Data/Network/APIs/MoviePosterAPI.swift new file mode 100644 index 00000000..12dd519d --- /dev/null +++ b/BoxOffice/Data/Network/APIs/MoviePosterAPI.swift @@ -0,0 +1,46 @@ +// +// MoviePosterAPI.swift +// BoxOffice +// +// Created by nayeon on 3/19/24. +// + +import Foundation + +enum MoviePosterAPI { + case requestMoviePosterImage(userkey: String, query: String) +} + +extension MoviePosterAPI: TargetType { + + var baseURL: String { + return "https://dapi.kakao.com/v2" + } + + var method: HTTPMethod { + return .get + } + + var path: String { + switch self { + case .requestMoviePosterImage: + return "/search/image" + } + } + + var parameters: RequestParameters { + switch self { + case let .requestMoviePosterImage(_, movieName): + return .query(["query": "\(movieName) 영화 포스터", + "size": "1", + "sort": "accuracy"]) + } + } + + var header: HeaderType { + switch self { + case let .requestMoviePosterImage(apiKey, _): + return .custom(["Authorization": "KakaoAK \(apiKey)"]) + } + } +} diff --git a/BoxOffice/Data/Network/DTOs/BoxOfficeDTO.swift b/BoxOffice/Data/Network/DTOs/BoxOfficeDTO.swift index f16805b1..6a033e80 100644 --- a/BoxOffice/Data/Network/DTOs/BoxOfficeDTO.swift +++ b/BoxOffice/Data/Network/DTOs/BoxOfficeDTO.swift @@ -72,7 +72,8 @@ extension DailyBoxOfficeList { movieName: movieName, salesAmount: salesAmount, audienceCount: audienceCount, - rankChangeValue: rankChangeValue, + rankChangeValue: rankChangeValue, + movieCode: movieCode, isNewMovie: rankOldAndNew == "New" ? true : false) } } diff --git a/BoxOffice/Data/Network/DTOs/MovieDTO.swift b/BoxOffice/Data/Network/DTOs/MovieDTO.swift index f86fe98d..3c29c2ee 100644 --- a/BoxOffice/Data/Network/DTOs/MovieDTO.swift +++ b/BoxOffice/Data/Network/DTOs/MovieDTO.swift @@ -164,3 +164,27 @@ extension Staff { case roleName = "staffRoleNm" } } + +extension DetailMovieInformation { + func toEntity() -> MovieEntity { + let movieName = self.movieName + let director = directors.map { $0.name }.joined(separator: ", ") + let productYear = self.productYear + let openDate = self.openDate + let showTime = self.showTime + let watchGrade = audits.map { $0.watchGrade }.joined(separator: ", ") + let nation = nations.map { $0.name }.joined(separator: ", ") + let genres = self.genres.map { $0.name }.joined(separator: ", ") + let actors = self.actors.map { $0.name }.joined(separator: ", ") + + return MovieEntity(movieName: movieName, + director: director, + productYear: productYear, + openDate: openDate, + showTime: showTime, + watchGrade: watchGrade, + nation: nation, + genres: genres, + actors: actors) + } +} diff --git a/BoxOffice/Data/Network/DTOs/MoviePosterDTO.swift b/BoxOffice/Data/Network/DTOs/MoviePosterDTO.swift new file mode 100644 index 00000000..213227c4 --- /dev/null +++ b/BoxOffice/Data/Network/DTOs/MoviePosterDTO.swift @@ -0,0 +1,47 @@ +// +// MoviePosterDTO.swift +// BoxOffice +// +// Created by nayeon on 3/19/24. +// + +import Foundation + +struct MoviePosterDTO: Codable { + let meta: Meta + let documents: [Document] +} + +struct Meta: Codable { + let totalCount: Int + let pageableCount: Int + let isEnd: Bool + + enum CodingKeys: String, CodingKey { + case totalCount = "total_count" + case pageableCount = "pageable_count" + case isEnd = "is_end" + } +} + +struct Document: Codable { + let collection: String + let thumbnailURL: String + let imageURL: String + let width: Int + let height: Int + let displaySiteName: String + let docURL: String + let datetime: String + + enum CodingKeys: String, CodingKey { + case collection + case thumbnailURL = "thumbnail_url" + case imageURL = "image_url" + case width + case height + case displaySiteName = "display_sitename" + case docURL = "doc_url" + case datetime + } +} diff --git a/BoxOffice/Data/Network/Services/MovieAPIService.swift b/BoxOffice/Data/Network/Services/MovieAPIService.swift index 7de27a2a..8dd9dfe2 100644 --- a/BoxOffice/Data/Network/Services/MovieAPIService.swift +++ b/BoxOffice/Data/Network/Services/MovieAPIService.swift @@ -9,14 +9,12 @@ import Foundation final class MovieAPIService: BaseAPIService { - static let shared = MovieAPIService(provider: NetworkProvider()) - - private override init(provider: Requestable) { - super.init(provider: provider) + override init(provider: Requestable) { + super.init(provider: NetworkProvider()) } func requestMovieDetailAPI(userKey: String, movieCode: String, - completion: @escaping ((NetworkResult) -> Void)) { + completion: @escaping ((NetworkResult) -> Void)) { guard let request = try? MovieAPI .requestMovieDetailInfo(userKey: userKey, movieCode: movieCode) .creatURLRequest() diff --git a/BoxOffice/Data/Network/Services/MoviePosterAPIService.swift b/BoxOffice/Data/Network/Services/MoviePosterAPIService.swift new file mode 100644 index 00000000..dd86cba0 --- /dev/null +++ b/BoxOffice/Data/Network/Services/MoviePosterAPIService.swift @@ -0,0 +1,37 @@ +// +// MoviePosterAPIService.swift +// BoxOffice +// +// Created by nayeon on 3/19/24. +// + +import Foundation + +final class MoviePosterAPIService: BaseAPIService { + + override init(provider: Requestable) { + super.init(provider: NetworkProvider()) + } + + func requestMoviePosterAPI(userKey: String, query: String, completion: @escaping ((NetworkResult) -> Void)) { + guard let request = try? MoviePosterAPI + .requestMoviePosterImage(userkey: userKey, query: query) + .creatURLRequest() + else { + completion(.networkFail) + return + } + + provider.request(request) { result in + switch result { + case .success(let result): + let networkResult = self.judgeStatus(by: result.response.statusCode, + result.data, + MoviePosterDTO.self) + completion(networkResult) + case .failure(_): + completion(.networkFail) + } + } + } +} diff --git a/BoxOffice/Data/Repositories/DefaultBoxOfficeRepository.swift b/BoxOffice/Data/Repositories/DefaultBoxOfficeRepository.swift index 10b845d8..d4fa1f42 100644 --- a/BoxOffice/Data/Repositories/DefaultBoxOfficeRepository.swift +++ b/BoxOffice/Data/Repositories/DefaultBoxOfficeRepository.swift @@ -29,6 +29,8 @@ final class DefaultBoxOfficeRepository: BoxOfficeRepository { } case .pathError: completion(.pathError) + case .parsingError: + completion(.parsingError) case .requestError: completion(.requestError) case .serverError: diff --git a/BoxOffice/Data/Repositories/DefaultMoviePosterRepository.swift b/BoxOffice/Data/Repositories/DefaultMoviePosterRepository.swift new file mode 100644 index 00000000..9fda9b47 --- /dev/null +++ b/BoxOffice/Data/Repositories/DefaultMoviePosterRepository.swift @@ -0,0 +1,73 @@ +// +// DefaultMoviePosterRepository.swift +// BoxOffice +// +// Created by nayeon on 3/19/24. +// + +import UIKit + +final class DefaultMoviePosterRepository: MoviePosterRepository { + + private let apiService: MoviePosterAPIService + + init(apiService: MoviePosterAPIService) { + self.apiService = apiService + } + + func fetchMoviePoster(query: String, completion: @escaping (NetworkResult) -> Void) { + guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "KAKAO_KEY") as? String else { return } + + apiService.requestMoviePosterAPI(userKey: apiKey, query: query) { result in + switch result { + case .success(let data): + if let dto = data as? MoviePosterDTO, + let firstImageUrl = dto.documents.first?.imageURL { + self.loadImage(from: firstImageUrl) { result in + switch result { + case .success(let image): + let posterEntity = MoviePosterEntity(image: image) + DispatchQueue.main.async { + completion(.success(posterEntity)) + } + case .failure: + completion(.networkFail) + } + } + } else { + completion(.parsingError) + } + case .pathError: + completion(.pathError) + case .parsingError: + completion(.parsingError) + case .requestError: + completion(.requestError) + case .serverError: + completion(.serverError) + case .networkFail: + completion(.networkFail) + } + } + } + + private func loadImage(from imageUrl: String, completion: @escaping (Result) -> Void) { + guard let url = URL(string: imageUrl) else { + completion(.failure(NetworkError.invalidURL)) + return + } + + URLSession.shared.dataTask(with: url) { data, response, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data, let image = UIImage(data: data) else { + completion(.failure(NetworkError.invalidURL)) + return + } + completion(.success(image)) + }.resume() + } +} diff --git a/BoxOffice/Data/Repositories/DefaultMovieRepository.swift b/BoxOffice/Data/Repositories/DefaultMovieRepository.swift new file mode 100644 index 00000000..6dbd5867 --- /dev/null +++ b/BoxOffice/Data/Repositories/DefaultMovieRepository.swift @@ -0,0 +1,43 @@ +// +// DefaultMovieRepository.swift +// BoxOffice +// +// Created by nayeon on 3/13/24. +// + +import Foundation + +final class DefaultMovieRepository: MovieRepository { + + private let apiService: MovieAPIService + + init(apiService: MovieAPIService) { + self.apiService = apiService + } + + func fetchMovieDetail(movieCode: String, completion: @escaping (NetworkResult) -> Void) { + guard let apiKey = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String else { return } + + apiService.requestMovieDetailAPI(userKey: apiKey, movieCode: movieCode) { result in + switch result { + case .success(let data): + if let dto = data as? MovieDTO { + DispatchQueue.main.async { + let movieEntity = dto.movieInformationResult.detailMovieInformation.toEntity() + completion(.success(movieEntity)) + } + } + case .pathError: + completion(.pathError) + case .parsingError: + completion(.parsingError) + case .requestError: + completion(.requestError) + case .serverError: + completion(.serverError) + case .networkFail: + completion(.networkFail) + } + } + } +} diff --git a/BoxOffice/Domain/Entities/BoxOfficeEntity.swift b/BoxOffice/Domain/Entities/BoxOfficeEntity.swift index b68ee65b..b8e1b401 100644 --- a/BoxOffice/Domain/Entities/BoxOfficeEntity.swift +++ b/BoxOffice/Domain/Entities/BoxOfficeEntity.swift @@ -11,5 +11,6 @@ struct BoxOfficeEntity { let salesAmount: String let audienceCount: String let rankChangeValue: String + let movieCode: String let isNewMovie: Bool } diff --git a/BoxOffice/Domain/Entities/MovieEntity.swift b/BoxOffice/Domain/Entities/MovieEntity.swift new file mode 100644 index 00000000..8d26bef8 --- /dev/null +++ b/BoxOffice/Domain/Entities/MovieEntity.swift @@ -0,0 +1,31 @@ +// +// MovieEntity.swift +// BoxOffice +// +// Created by EUNJU on 3/13/24. +// + +struct MovieEntity { + let movieName: String + let director: String + let productYear: String + let openDate: String + let showTime: String + let watchGrade: String + let nation: String + let genres: String + let actors: String + + func getInfoArray() -> [(title: String, info: String)] { + return [ + ("감독:", director), + ("제작년도:", productYear), + ("개봉일:", openDate), + ("상영시간:", showTime), + ("관람등급:", watchGrade), + ("제작국가:", nation), + ("장르:", genres), + ("배우:", actors) + ] + } +} diff --git a/BoxOffice/Domain/Entities/MoviePosterEntity.swift b/BoxOffice/Domain/Entities/MoviePosterEntity.swift new file mode 100644 index 00000000..82b31520 --- /dev/null +++ b/BoxOffice/Domain/Entities/MoviePosterEntity.swift @@ -0,0 +1,12 @@ +// +// MoviePosterEntity.swift +// BoxOffice +// +// Created by nayeon on 3/19/24. +// + +import UIKit + +struct MoviePosterEntity { + let image: UIImage +} diff --git a/BoxOffice/Domain/Interfaces/Repositories/MoviePosterRepository.swift b/BoxOffice/Domain/Interfaces/Repositories/MoviePosterRepository.swift new file mode 100644 index 00000000..f7e2bf36 --- /dev/null +++ b/BoxOffice/Domain/Interfaces/Repositories/MoviePosterRepository.swift @@ -0,0 +1,13 @@ +// +// MoviePosterRepository.swift +// BoxOffice +// +// Created by nayeon on 3/19/24. +// + +import Foundation + +protocol MoviePosterRepository { + func fetchMoviePoster(query: String, completion: @escaping + (NetworkResult) -> Void) +} diff --git a/BoxOffice/Domain/Interfaces/Repositories/MovieRepository.swift b/BoxOffice/Domain/Interfaces/Repositories/MovieRepository.swift new file mode 100644 index 00000000..4c03f9d8 --- /dev/null +++ b/BoxOffice/Domain/Interfaces/Repositories/MovieRepository.swift @@ -0,0 +1,13 @@ +// +// MovieRepository.swift +// BoxOffice +// +// Created by nayeon on 3/13/24. +// + +import Foundation + +protocol MovieRepository { + func fetchMovieDetail(movieCode: String, completion: @escaping + (NetworkResult) -> Void) +} diff --git a/BoxOffice/Domain/UseCases/MoviePosterUseCase.swift b/BoxOffice/Domain/UseCases/MoviePosterUseCase.swift new file mode 100644 index 00000000..5358f431 --- /dev/null +++ b/BoxOffice/Domain/UseCases/MoviePosterUseCase.swift @@ -0,0 +1,27 @@ +// +// MoviePosterUseCase.swift +// BoxOffice +// +// Created by nayeon on 3/19/24. +// + +import Foundation + +protocol MoviePosterUseCase { + func fetchMoviePoster(query: String, completion: @escaping (NetworkResult) -> Void) +} + +final class DefaultMoviePoserUseCase: MoviePosterUseCase { + + private let moviePosterRepository: MoviePosterRepository + + init(movieRepository: MoviePosterRepository) { + self.moviePosterRepository = movieRepository + } + + func fetchMoviePoster(query: String, completion: @escaping (NetworkResult) -> Void) { + moviePosterRepository.fetchMoviePoster(query: query) { result in + completion(result) + } + } +} diff --git a/BoxOffice/Domain/UseCases/MovieUseCase.swift b/BoxOffice/Domain/UseCases/MovieUseCase.swift new file mode 100644 index 00000000..a44c71ab --- /dev/null +++ b/BoxOffice/Domain/UseCases/MovieUseCase.swift @@ -0,0 +1,27 @@ +// +// MovieUseCase.swift +// BoxOffice +// +// Created by nayeon on 3/13/24. +// + +import Foundation + +protocol MovieUseCase { + func fetchMovieDetail(movieCode: String, completion: @escaping (NetworkResult) -> Void) +} + +final class DefaultMovieUseCase: MovieUseCase { + + private let movieRepository: MovieRepository + + init(movieRepository: MovieRepository) { + self.movieRepository = movieRepository + } + + func fetchMovieDetail(movieCode: String, completion: @escaping (NetworkResult) -> Void) { + movieRepository.fetchMovieDetail(movieCode: movieCode) { result in + completion(result) + } + } +} diff --git a/BoxOffice/Infrastructure/Network/Foundation/BaseAPIService.swift b/BoxOffice/Infrastructure/Network/Foundation/BaseAPIService.swift index f5e3e376..ebc8c462 100644 --- a/BoxOffice/Infrastructure/Network/Foundation/BaseAPIService.swift +++ b/BoxOffice/Infrastructure/Network/Foundation/BaseAPIService.swift @@ -21,7 +21,7 @@ class BaseAPIService { let decoder = JSONDecoder() guard let decodedData = try? decoder.decode(T.self, from: data) else { - return .pathError + return .parsingError } switch statusCode { diff --git a/BoxOffice/Infrastructure/Network/Foundation/NetworkConstants.swift b/BoxOffice/Infrastructure/Network/Foundation/NetworkConstants.swift index 468eb9ae..8c10d20e 100644 --- a/BoxOffice/Infrastructure/Network/Foundation/NetworkConstants.swift +++ b/BoxOffice/Infrastructure/Network/Foundation/NetworkConstants.swift @@ -9,6 +9,7 @@ import Foundation enum HeaderType { case basic + case custom([String: String]) } enum HTTPHeaderField: String { diff --git a/BoxOffice/Infrastructure/Network/Foundation/NetworkEnvironment.swift b/BoxOffice/Infrastructure/Network/Foundation/NetworkEnvironment.swift index 6871317d..3f92de5d 100644 --- a/BoxOffice/Infrastructure/Network/Foundation/NetworkEnvironment.swift +++ b/BoxOffice/Infrastructure/Network/Foundation/NetworkEnvironment.swift @@ -9,4 +9,5 @@ import Foundation enum NetworkEnvironment { static let baseURL = "http://www.kobis.or.kr/kobisopenapi/webservice/rest" + static let kakaoURL = "https://dapi.kakao.com/v2" } diff --git a/BoxOffice/Infrastructure/Network/Foundation/NetworkProvider.swift b/BoxOffice/Infrastructure/Network/Foundation/NetworkProvider.swift index 79766dcf..2b025329 100644 --- a/BoxOffice/Infrastructure/Network/Foundation/NetworkProvider.swift +++ b/BoxOffice/Infrastructure/Network/Foundation/NetworkProvider.swift @@ -21,8 +21,9 @@ final class NetworkProvider: Requestable { (200..<500).contains(httpResponse.statusCode), let data = data { completion(.success(NetworkResponse(data: data, response: httpResponse))) + } else { + completion(.failure(NetworkError.networkFailed)) } - completion(.failure(NetworkError.networkFailed)) } task.resume() } diff --git a/BoxOffice/Infrastructure/Network/Foundation/NetworkResult.swift b/BoxOffice/Infrastructure/Network/Foundation/NetworkResult.swift index f792b031..6651bd76 100644 --- a/BoxOffice/Infrastructure/Network/Foundation/NetworkResult.swift +++ b/BoxOffice/Infrastructure/Network/Foundation/NetworkResult.swift @@ -9,6 +9,7 @@ enum NetworkResult { case success(T) case requestError case pathError + case parsingError case serverError case networkFail } diff --git a/BoxOffice/Infrastructure/Network/Foundation/TargetType.swift b/BoxOffice/Infrastructure/Network/Foundation/TargetType.swift index bfda606c..44036018 100644 --- a/BoxOffice/Infrastructure/Network/Foundation/TargetType.swift +++ b/BoxOffice/Infrastructure/Network/Foundation/TargetType.swift @@ -17,10 +17,6 @@ protocol TargetType { extension TargetType { - var baseURL: String { - return NetworkEnvironment.baseURL - } - private func asURL(_ url: String) throws -> URL { guard let url = URL(string: url) else { @@ -37,6 +33,10 @@ extension TargetType { case .basic: request.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue) + case .custom(let headers): + headers.forEach { key, value in + request.setValue(value, forHTTPHeaderField: key) + } } return request diff --git a/BoxOffice/Presentation/BoxOfficeCollectionViewCell.swift b/BoxOffice/Presentation/BoxOfficeScene/BoxOfficeCollectionViewCell.swift similarity index 98% rename from BoxOffice/Presentation/BoxOfficeCollectionViewCell.swift rename to BoxOffice/Presentation/BoxOfficeScene/BoxOfficeCollectionViewCell.swift index 815fec84..2c0cf836 100644 --- a/BoxOffice/Presentation/BoxOfficeCollectionViewCell.swift +++ b/BoxOffice/Presentation/BoxOfficeScene/BoxOfficeCollectionViewCell.swift @@ -8,8 +8,6 @@ import UIKit final class BoxOfficeCollectionViewCell: UICollectionViewCell { - - static let identifier = "BoxOfficeCollectionViewCell" private let rankLabel: UILabel = { let label = UILabel() diff --git a/BoxOffice/Presentation/BoxOfficeViewController.swift b/BoxOffice/Presentation/BoxOfficeScene/BoxOfficeViewController.swift similarity index 78% rename from BoxOffice/Presentation/BoxOfficeViewController.swift rename to BoxOffice/Presentation/BoxOfficeScene/BoxOfficeViewController.swift index 4afbc96a..4dc36992 100644 --- a/BoxOffice/Presentation/BoxOfficeViewController.swift +++ b/BoxOffice/Presentation/BoxOfficeScene/BoxOfficeViewController.swift @@ -7,7 +7,7 @@ import UIKit -class BoxOfficeViewController: UIViewController { +final class BoxOfficeViewController: UIViewController { private let viewModel: BoxOfficeViewModel private var refreshAction: Observable = Observable(()) @@ -72,6 +72,7 @@ class BoxOfficeViewController: UIViewController { self.navigationItem.title = formattedDateString } + // TODO: 위치 옮기기 private func formatDateString(_ dateString: String) -> String { let yearIndex = dateString.index(dateString.startIndex, offsetBy: 0) let monthIndex = dateString.index(dateString.startIndex, offsetBy: 4) @@ -88,7 +89,8 @@ class BoxOfficeViewController: UIViewController { collectionView.backgroundColor = .white collectionView.delegate = self collectionView.dataSource = self - collectionView.register(BoxOfficeCollectionViewCell.self, forCellWithReuseIdentifier: "BoxOfficeCollectionViewCell") + collectionView.register(BoxOfficeCollectionViewCell.self, + forCellWithReuseIdentifier: String(describing: BoxOfficeCollectionViewCell.self)) } private func setupRefreshControl() { @@ -127,7 +129,8 @@ extension BoxOfficeViewController: UICollectionViewDataSource { } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: BoxOfficeCollectionViewCell.identifier, for: indexPath) as? BoxOfficeCollectionViewCell else { + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: BoxOfficeCollectionViewCell.self), for: indexPath) as? BoxOfficeCollectionViewCell + else { return UICollectionViewCell() } cell.configure(with: boxOfficeList[indexPath.row]) @@ -142,3 +145,22 @@ extension BoxOfficeViewController: UICollectionViewDelegateFlowLayout { return CGSize(width: collectionView.bounds.width, height: 100) } } + +extension BoxOfficeViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let selectedMovieCode = Observable(boxOfficeList[indexPath.item].movieCode) + let selectedMovieName = Observable(boxOfficeList[indexPath.item].movieName) + + let movieDetailVC = MovieDetailViewController( + viewModel: MovieDetailViewModel( + movieUseCase: DefaultMovieUseCase( + movieRepository: DefaultMovieRepository( + apiService: MovieAPIService(provider: NetworkProvider()) + ) + ), moviePosterUseCase: DefaultMoviePoserUseCase(movieRepository: DefaultMoviePosterRepository(apiService: MoviePosterAPIService(provider: NetworkProvider()))) + ), + movieCode: selectedMovieCode, movieName: selectedMovieName + ) + navigationController?.pushViewController(movieDetailVC, animated: true) + } +} diff --git a/BoxOffice/Presentation/BoxOfficeViewModel.swift b/BoxOffice/Presentation/BoxOfficeScene/BoxOfficeViewModel.swift similarity index 100% rename from BoxOffice/Presentation/BoxOfficeViewModel.swift rename to BoxOffice/Presentation/BoxOfficeScene/BoxOfficeViewModel.swift diff --git a/BoxOffice/Presentation/MovieDetailScene/MovieDetailViewController.swift b/BoxOffice/Presentation/MovieDetailScene/MovieDetailViewController.swift new file mode 100644 index 00000000..3f9d8c5b --- /dev/null +++ b/BoxOffice/Presentation/MovieDetailScene/MovieDetailViewController.swift @@ -0,0 +1,146 @@ +// +// MovieDetailViewController.swift +// BoxOffice +// +// Created by EUNJU on 3/12/24. +// + +import UIKit + +final class MovieDetailViewController: UIViewController { + + private let viewModel: MovieDetailViewModel + private let movieCode: Observable + private let movieName: Observable + private var movieDetail: MovieEntity? + private var moviePoster: MoviePosterEntity? + private let movieDetailTableView = UITableView() + + init(viewModel: MovieDetailViewModel, + movieCode: Observable, + movieName: Observable) { + self.viewModel = viewModel + self.movieCode = movieCode + self.movieName = movieName + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + configureUI() + setUpLayout() + setUpNavigationBar(movieName: "") + setUpTableView() + registerCell() + bindViewModel() + } + + private func configureUI() { + view.backgroundColor = .white + movieDetailTableView.separatorStyle = .none + } + + private func setUpLayout() { + view.addSubview(movieDetailTableView) + movieDetailTableView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + movieDetailTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + movieDetailTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + movieDetailTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + movieDetailTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + ]) + } + + private func setUpNavigationBar(movieName: String) { + self.navigationItem.title = movieName + } + + private func setUpTableView() { + movieDetailTableView.delegate = self + movieDetailTableView.dataSource = self + } + + private func registerCell() { + movieDetailTableView.register(MovieInfoTableViewCell.self, + forCellReuseIdentifier: String(describing: MovieInfoTableViewCell.self)) + movieDetailTableView.register(MovieImageTableViewCell.self, forCellReuseIdentifier: String(describing: MovieImageTableViewCell.self)) + } + + private func bindViewModel() { + let input = MovieDetailViewModel.Input( + viewDidLoad: Observable(()), + refreshAction: Observable(()), + movieCode: movieCode, + movieName: movieName + ) + let output = viewModel.transform(input: input) + + output.movieDetail.subscribe { [weak self] movieEntity in + guard let movieEntity = movieEntity else { return } + self?.movieDetail = movieEntity + self?.setUpNavigationBar(movieName: movieEntity.movieName) + self?.movieDetailTableView.reloadData() + } + + output.moviePoster.subscribe { [weak self] moviePoster in + guard let moviePoster = moviePoster else { return } + self?.moviePoster = moviePoster + self?.movieDetailTableView.reloadData() + } + + output.networkError.subscribe { [weak self] result in + if result { + self?.presentAlert(title: "네트워크 오류로 인해\n데이터를 불러올 수 없습니다.\n다시 시도해 주세요.") + } + } + } +} + +// MARK: - UITableViewDelegate +extension MovieDetailViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } +} + +// MARK: - UITableViewDataSource +extension MovieDetailViewController: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: return 1 + case 1: return 8 + default: return 0 + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let imageCell = tableView.dequeueReusableCell(withIdentifier: String(describing: MovieImageTableViewCell.self), for: indexPath) as? MovieImageTableViewCell, + let infoCell = tableView.dequeueReusableCell(withIdentifier: String(describing: MovieInfoTableViewCell.self), for: indexPath) as? MovieInfoTableViewCell + else { + return UITableViewCell() + } + + switch indexPath.section { + case 0: + imageCell.setUpData(with: moviePoster) + return imageCell + case 1: + let info = movieDetail?.getInfoArray()[indexPath.row] + infoCell.setUpData(with: info) + return infoCell + default: + return UITableViewCell() + } + } +} diff --git a/BoxOffice/Presentation/MovieDetailScene/MovieDetailViewModel.swift b/BoxOffice/Presentation/MovieDetailScene/MovieDetailViewModel.swift new file mode 100644 index 00000000..5c5e33d8 --- /dev/null +++ b/BoxOffice/Presentation/MovieDetailScene/MovieDetailViewModel.swift @@ -0,0 +1,77 @@ +// +// MovieDetailViewModel.swift +// BoxOffice +// +// Created by nayeon on 3/13/24. +// + +import Foundation + +final class MovieDetailViewModel: ViewModelType { + + private let movieUseCase: MovieUseCase + private let moviePosterUseCase: MoviePosterUseCase + private var movieDetail: Observable = Observable(nil) + private var networkError: Observable = Observable(false) + private var moviePoster: Observable = Observable(nil) + + init(movieUseCase: MovieUseCase, moviePosterUseCase: MoviePosterUseCase) { + self.movieUseCase = movieUseCase + self.moviePosterUseCase = moviePosterUseCase + } + + struct Input { + let viewDidLoad: Observable + let refreshAction: Observable + let movieCode: Observable + let movieName: Observable + } + + struct Output { + let movieDetail: Observable + let networkError: Observable + let moviePoster: Observable + } + + func transform(input: Input) -> Output { + input.viewDidLoad.subscribe { [weak self] in + self?.fetchMovieDetail(movieCode: input.movieCode.value, movieName: input.movieName.value) + } + + input.refreshAction.subscribe { [weak self] in + self?.fetchMovieDetail(movieCode: input.movieCode.value, movieName: input.movieName.value) + } + + input.movieCode.subscribe { [weak self] newMovieCode in + self?.fetchMovieDetail(movieCode: newMovieCode, movieName: input.movieName.value) + } + + return Output(movieDetail: movieDetail, networkError: networkError, moviePoster: moviePoster) + } + + private func fetchMovieDetail(movieCode: String, movieName: String) { + movieUseCase.fetchMovieDetail(movieCode: movieCode) { [weak self] result in + switch result { + case .success(let data): + DispatchQueue.main.async { + self?.movieDetail.value = data + } + default: + DispatchQueue.main.async { + self?.networkError.value = true + } + } + } + + moviePosterUseCase.fetchMoviePoster(query: movieName) { [weak self] result in + switch result { + case .success(let data): + DispatchQueue.main.async { + self?.moviePoster.value = data + } + default: + break + } + } + } +} diff --git a/BoxOffice/Presentation/MovieDetailScene/MovieImageTableViewCell.swift b/BoxOffice/Presentation/MovieDetailScene/MovieImageTableViewCell.swift new file mode 100644 index 00000000..8e4ab546 --- /dev/null +++ b/BoxOffice/Presentation/MovieDetailScene/MovieImageTableViewCell.swift @@ -0,0 +1,48 @@ +// +// MovieImageTableViewCell.swift +// BoxOffice +// +// Created by nayeon on 3/19/24. +// + +import UIKit + +final class MovieImageTableViewCell: UITableViewCell { + + private let posterImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFill + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + configureUI() + setUpLayout() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configureUI() { + selectionStyle = .none + } + + private func setUpLayout() { + contentView.addSubview(posterImageView) + NSLayoutConstraint.activate([ + posterImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + posterImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + posterImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + posterImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } + + func setUpData(with moviePoster: MoviePosterEntity?) { + guard let moviePoster = moviePoster else { return } + posterImageView.image = moviePoster.image + } +} diff --git a/BoxOffice/Presentation/MovieDetailScene/MovieInfoTableViewCell.swift b/BoxOffice/Presentation/MovieDetailScene/MovieInfoTableViewCell.swift new file mode 100644 index 00000000..7837ea5b --- /dev/null +++ b/BoxOffice/Presentation/MovieDetailScene/MovieInfoTableViewCell.swift @@ -0,0 +1,72 @@ +// +// MovieInfoTableViewCell.swift +// BoxOffice +// +// Created by EUNJU on 3/12/24. +// + +import UIKit + +final class MovieInfoTableViewCell: UITableViewCell { + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 20, weight: .semibold) + label.textAlignment = .center + return label + }() + + private let infoLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 20, weight: .regular) + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + return label + }() + + private lazy var movieInfoStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [titleLabel, infoLabel]) + stackView.distribution = .fill + stackView.alignment = .center + stackView.axis = .horizontal + stackView.spacing = 10 + return stackView + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + configureUI() + setUpLayout() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private func configureUI() { + selectionStyle = .none + } + + private func setUpLayout() { + contentView.addSubview(movieInfoStackView) + [titleLabel, infoLabel, movieInfoStackView].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + } + titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + infoLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + NSLayoutConstraint.activate([ + movieInfoStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), + movieInfoStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), + movieInfoStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), + movieInfoStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5) + ]) + } + + func setUpData(with info: (title: String, info: String)?) { + guard let info = info else { return } + titleLabel.text = info.title + infoLabel.text = info.info + } +} diff --git a/BoxOffice/Resources/Configuration.xcconfig b/BoxOffice/Resources/Configuration.xcconfig index 4a0c8f50..5d5b3258 100644 --- a/BoxOffice/Resources/Configuration.xcconfig +++ b/BoxOffice/Resources/Configuration.xcconfig @@ -9,3 +9,4 @@ // https://help.apple.com/xcode/#/dev745c5c974 API_KEY = 2fa5866e3c329a53afad886b2f90601c +KAKAO_KEY = be99aa7fba9ce3b9cc3a5533ac74e3bf diff --git a/BoxOffice/Resources/Info.plist b/BoxOffice/Resources/Info.plist index 7d7ddb1c..5d2b0e9e 100644 --- a/BoxOffice/Resources/Info.plist +++ b/BoxOffice/Resources/Info.plist @@ -4,6 +4,13 @@ API_KEY $(API_KEY) + KAKAO_KEY + $(KAKAO_KEY) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -21,10 +28,5 @@ - NSAppTransportSecurity - - NSAllowsArbitraryLoads - -