diff --git a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift index d82177a8..49a62959 100644 --- a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -50,7 +50,9 @@ public extension [String: Plist.Value] { "NMFClientId": "$(NAVERMAP_CLIENT_ID)", "TERMS_OF_PRIVACY_URL": "$(TERMS_OF_PRIVACY_URL)", "LOCATION_PRIVACY_URL": "$(LOCATION_PRIVACY_URL)", - "INQURY_URL": "$(INQURY_URL)" + "INQUIRY_URL": "$(INQUIRY_URL)", + "APPSTORE_ID": "$(APPSTORE_ID)", + "DOMAIN_URL": "$(DOMAIN_URL)", ] static let additionalInfoPlist: Self = [ diff --git a/Projects/App/Sources/AppDelegate+Register.swift b/Projects/App/Sources/AppDelegate+Register.swift index a4d9241c..cd3990f8 100644 --- a/Projects/App/Sources/AppDelegate+Register.swift +++ b/Projects/App/Sources/AppDelegate+Register.swift @@ -38,6 +38,9 @@ extension AppDelegate { = DefaultLocalNotificationService() let regularAlarmEditingService: RegularAlarmEditingService = DefaultRegularAlarmEditingService() + // TODO: 추후 의존 주입 형태 변경 +// let versionCheckRepository: VersionCheckRepository +// = DefaultVersionCheckRepository(networkService: networkService) DIContainer.register( type: FavoritesUseCase.self, @@ -92,5 +95,12 @@ extension AppDelegate { type: RegularAlarmEditingService.self, regularAlarmEditingService ) + +// DIContainer.register( +// type: VersionCheckUseCase.self, +// DefaultVersionCheckUseCase( +// versionCheckRepository: versionCheckRepository +// ) +// ) } } diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index 2d12b3d9..6b7f028b 100644 --- a/Projects/App/Sources/SceneDelegate.swift +++ b/Projects/App/Sources/SceneDelegate.swift @@ -8,6 +8,7 @@ import UIKit +import Core import NetworkService import Domain import Data @@ -18,9 +19,20 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var appCoordinator: AppCoordinator? var deeplinkHandler: DeeplinkHandler? + + let disposeBag = DisposeBag() + + // MARK: 추후 구체타입이 아닌 형태로 변경 + private var useCase: VersionCheckUseCase + = DefaultVersionCheckUseCase( + versionCheckRepository: DefaultVersionCheckRepository( + networkService: DefaultNetworkService() + ), + forceUpdateService: DefaultForceUpdateService() + ) func scene( - _ scene: UIScene, + _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { @@ -37,6 +49,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) appCoordinator?.start() window?.makeKeyAndVisible() + // 앱 진입할 때 확인 deeplinkHandler = .init(appCoordinator: appCoordinator) if let url = connectionOptions.urlContexts.first?.url { deeplinkHandler?.handleUrl(url: url) @@ -52,7 +65,9 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneWillResignActive(_ scene: UIScene) { } + /// 앱이 Foreground로 전환될때 실행될 함수 func sceneWillEnterForeground(_ scene: UIScene) { + checkAndUpdateIfNeeded() } func sceneDidEnterBackground(_ scene: UIScene) { @@ -66,5 +81,47 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { deeplinkHandler?.handleUrl(url: url) } } + + private func checkAndUpdateIfNeeded() { + useCase.fetchAppStoreURL() + .subscribe(with: self) { owner, str in + guard let str else { return } + owner.showUpdateAlert(with: str) + } onFailure: { _, error in + print(error) + } + .disposed(by: disposeBag) + } + + private func showUpdateAlert(with urlString: String) { + let alert = UIAlertController( + title: "업데이트 알림", + message: "더 나은 서비스를 위해 업데이트 되었어요 ! 업데이트 해주세요.", + preferredStyle: .alert + ) + + let alertAction = UIAlertAction( + title: "업데이트", + style: .default + ) { [weak self] _ in + guard let self else { return } + + openAppStore(urlString) + } + + alert.addAction(alertAction) + + Task { @MainActor in + window?.rootViewController?.present(alert, animated: true) + } + } + + private func openAppStore(_ str: String) { + guard let url = URL(string: str) else { return } + + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } } diff --git a/Projects/App/Widget/ArrivalInfo/View/ArrivalInfoView.swift b/Projects/App/Widget/ArrivalInfo/View/ArrivalInfoView.swift index 6c04dbb9..2ce1068d 100644 --- a/Projects/App/Widget/ArrivalInfo/View/ArrivalInfoView.swift +++ b/Projects/App/Widget/ArrivalInfo/View/ArrivalInfoView.swift @@ -10,7 +10,7 @@ import SwiftUI import WidgetKit import DesignSystem -@available (iOS 17.0, *) +@available(iOS 17.0, *) struct ArrivalInfoView: View { var entry: ArrivalInfoProvider.Entry @Environment(\.widgetFamily) var widgetFamily diff --git a/Projects/Core/Sources/Extension/String+.swift b/Projects/Core/Sources/Extension/String+.swift index 4be1e8cc..b0fc754e 100644 --- a/Projects/Core/Sources/Extension/String+.swift +++ b/Projects/Core/Sources/Extension/String+.swift @@ -34,11 +34,13 @@ public extension String { return serverKey } - static func getCurrentVersion() -> String { + /// 프로젝트 버전 + static func getCurrentVersion() -> [Int] { guard let dictionary = Bundle.main.infoDictionary, let version = dictionary["CFBundleShortVersionString"] as? String - else { return "" } - return version + else { return [1, 0, 0] } + + return version.split(separator: ".").compactMap { Int($0) } } static func getDeviceIdentifier() -> String { diff --git a/Projects/Data/Sources/DTO/AppInfoDTO.swift b/Projects/Data/Sources/DTO/AppInfoDTO.swift new file mode 100644 index 00000000..f5791ed4 --- /dev/null +++ b/Projects/Data/Sources/DTO/AppInfoDTO.swift @@ -0,0 +1,43 @@ +// +// AppInfoDTO.swift +// Data +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import Foundation + +import Domain + +public struct AppInfoDTO: Decodable { + let resultCount: Int + let results: [AppDetailDTO] +} + +extension AppInfoDTO { + var toDomain: AppVersionInfoResponse? { + guard let versionString = results.map({ $0.version }).first + else { return nil } + + let versionComponents = versionString.split(separator: ".") + .compactMap { Int($0) } + + guard versionComponents.count == 3 + else { return nil } + + return AppVersionInfoResponse( + major: versionComponents[0], + minor: versionComponents[1], + patch: versionComponents[2] + ) + } +} + +extension AppInfoDTO { + struct AppDetailDTO: Decodable { + let trackId: Int + /// App Version + let version: String + } +} diff --git a/Projects/Data/Sources/DTO/RequiredVersionDTO.swift b/Projects/Data/Sources/DTO/RequiredVersionDTO.swift new file mode 100644 index 00000000..d3854177 --- /dev/null +++ b/Projects/Data/Sources/DTO/RequiredVersionDTO.swift @@ -0,0 +1,35 @@ +// +// RequiredVersionDTO.swift +// Data +// +// Created by Jisoo Ham on 3/26/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +import Domain + +public struct RequiredVersionDTO: Decodable { + let version: String + + enum CodingKeys: String, CodingKey { + case version = "ver" + } +} + +extension RequiredVersionDTO { + var toDomain: AppVersionInfoResponse { + let versionComponents = version.split(separator: ".") + .compactMap { Int($0) } + + guard versionComponents.count == 3 + else { return AppVersionInfoResponse(major: 1, minor: 2, patch: 5) } + + return AppVersionInfoResponse( + major: versionComponents[0], + minor: versionComponents[1], + patch: versionComponents[2] + ) + } +} diff --git a/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift new file mode 100644 index 00000000..3575999b --- /dev/null +++ b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift @@ -0,0 +1,96 @@ +// +// DefaultVersionCheckRepository.swift +// Data +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import UIKit + +import Core +import Domain +import NetworkService + +import RxSwift + +public final class DefaultVersionCheckRepository: VersionCheckRepository { + private let networkService: NetworkService + + @UserDefaultsWrapper( + key: "ForceUpdate", + defaultValue: ForceUpdate( + version: AppVersionInfoResponse(major: 1, minor: 2, patch: 4), + date: Date(timeIntervalSince1970: 0) + ) + ) + private var forceUpdateInfo: ForceUpdate + + public init(networkService: NetworkService) { + self.networkService = networkService + } + + /// 서버로 부터 받은 App의 최소 지원 버전 + public func fetchRequiredVersion() + -> Single> { + return networkService.request( + endPoint: MinVersionEndpoint(domain: getDomainURL()), + responseType: RequiredVersionDTO.self + ) + .map { result in + switch result { + case .success(let value): + return .success(value.toDomain) + case .failure(let error): + return .failure(error) + } + } + } + + public func getStoreLink() -> String? { + return OpenStoreEndpoint(appStoreID: getAppStoreID()).toURLString + } + + public func getAppStoreID() -> String { + guard let appId = Bundle.main.object( + forInfoDictionaryKey: "APPSTORE_ID" + ) as? String + else { return "" } + + return appId + } + + public func getUserAppVersion() -> AppVersionInfoResponse { + guard let dictionary = Bundle.main.infoDictionary, + let version = dictionary["CFBundleShortVersionString"] as? String + else { return AppVersionInfoResponse(major: 1, minor: 0, patch: 0) } + + let splitedVersion = version.split(separator: ".") + .compactMap { Int($0) } + + return AppVersionInfoResponse( + major: splitedVersion[0], + minor: splitedVersion[1], + patch: splitedVersion[2] + ) + } + + /// 최소 요구 버전, fetch 받은 날짜를 UserDefaults 저장 + public func saveForceUpdateInfo(_ newValue: ForceUpdate) { + forceUpdateInfo = newValue + } + + /// UserDefaults 저장된 최소 요구 버전, fetch 받은 날짜 + public func getForceUpdateInfo() -> ForceUpdate { + return forceUpdateInfo + } + + private func getDomainURL() -> String { + guard let domainURL = Bundle.main.object( + forInfoDictionaryKey: "DOMAIN_URL" + ) as? String + else { return "" } + + return domainURL + } +} diff --git a/Projects/Data/Sources/Service/ForceUpdateService/DefaultForceUpdateService.swift b/Projects/Data/Sources/Service/ForceUpdateService/DefaultForceUpdateService.swift new file mode 100644 index 00000000..74a2083f --- /dev/null +++ b/Projects/Data/Sources/Service/ForceUpdateService/DefaultForceUpdateService.swift @@ -0,0 +1,23 @@ +// +// DefaultForceUpdateService.swift +// Data +// +// Created by Jisoo Ham on 3/27/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +import Domain + +public final class DefaultForceUpdateService: ForceUpdateService { + + public init() { } + + public func compareVersion( + user: AppVersionInfoResponse, + required: AppVersionInfoResponse + ) -> Bool { + return required > user + } +} diff --git a/Projects/Domain/Sources/Entity/ForceUpdate.swift b/Projects/Domain/Sources/Entity/ForceUpdate.swift new file mode 100644 index 00000000..56c2de5b --- /dev/null +++ b/Projects/Domain/Sources/Entity/ForceUpdate.swift @@ -0,0 +1,22 @@ +// +// ForceUpdate.swift +// Domain +// +// Created by Jisoo Ham on 3/27/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +public struct ForceUpdate: Codable { + let version: AppVersionInfoResponse + let date: Date + + public init( + version: AppVersionInfoResponse, + date: Date + ) { + self.version = version + self.date = date + } +} diff --git a/Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift b/Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift new file mode 100644 index 00000000..ecba196e --- /dev/null +++ b/Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift @@ -0,0 +1,34 @@ +// +// AppVersionInfoResponse.swift +// Domain +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import Foundation + +public struct AppVersionInfoResponse: Codable, Comparable { + let major: Int + let minor: Int + let patch: Int + + public init( + major: Int, + minor: Int, + patch: Int + ) { + self.major = major + self.minor = minor + self.patch = patch + } + + public static func < ( + lhs: AppVersionInfoResponse, + rhs: AppVersionInfoResponse + ) -> Bool { + if lhs.major != rhs.major { return lhs.major < rhs.major } + if lhs.minor != rhs.minor { return lhs.minor < rhs.minor } + return lhs.patch < rhs.patch + } +} diff --git a/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift b/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift new file mode 100644 index 00000000..a1f9cc0b --- /dev/null +++ b/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift @@ -0,0 +1,21 @@ +// +// VersionCheckRepository.swift +// Domain +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import Foundation + +import RxSwift + +public protocol VersionCheckRepository: AnyObject { + func fetchRequiredVersion() + -> Single> + func getStoreLink() -> String? + func getAppStoreID() -> String + func getUserAppVersion() -> AppVersionInfoResponse + func saveForceUpdateInfo(_ info: ForceUpdate) + func getForceUpdateInfo() -> ForceUpdate +} diff --git a/Projects/Domain/Sources/Service/ForceUpdateService/ForceUpdateService.swift b/Projects/Domain/Sources/Service/ForceUpdateService/ForceUpdateService.swift new file mode 100644 index 00000000..066278aa --- /dev/null +++ b/Projects/Domain/Sources/Service/ForceUpdateService/ForceUpdateService.swift @@ -0,0 +1,16 @@ +// +// ForceUpdateService.swift +// Domain +// +// Created by Jisoo Ham on 3/27/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +public protocol ForceUpdateService { + func compareVersion( + user: AppVersionInfoResponse, + required: AppVersionInfoResponse + ) -> Bool +} diff --git a/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift b/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift new file mode 100644 index 00000000..edfa0b56 --- /dev/null +++ b/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift @@ -0,0 +1,91 @@ +// +// DefaultVersionCheckUseCase.swift +// Domain +// +// Created by Jisoo Ham on 3/25/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +import RxSwift + +public final class DefaultVersionCheckUseCase: VersionCheckUseCase { + private let versionCheckRepository: VersionCheckRepository + private let forceUpdateService: ForceUpdateService + + public init( + versionCheckRepository: VersionCheckRepository, + forceUpdateService: ForceUpdateService + ) { + self.versionCheckRepository = versionCheckRepository + self.forceUpdateService = forceUpdateService + } + + public func fetchAppStoreURL() -> Single { + if hasToFetchVersion() { + return fetchAndUpdateVersion() + } else { + let forceUpdateInfo = versionCheckRepository.getForceUpdateInfo() + return .just(getStoreLink(forceUpdateInfo.version)) + } + } +} +extension DefaultVersionCheckUseCase { + /// 호출하는 시점과 UserDefaults에 저장된 Date를 기준으로 4시간이 넘는지를 확인하는 method + private func hasToFetchVersion() -> Bool { + let fourHour: TimeInterval = 4 * 60 * 60 + + return Date().timeIntervalSince( + versionCheckRepository.getForceUpdateInfo().date + ) >= fourHour + } + + private func fetchAndUpdateVersion() -> Single { + return versionCheckRepository.fetchRequiredVersion() + .do(onSuccess: { [weak self] result in + guard let self else { return } + saveForceVersionInfo(result) + }) + .map { [weak self] result in + guard let self else { return nil } + return handleFetchedResult(result) + } + } + + /// 서버 통신의 결과 상태를 기반으로 app store Link 반환 + private func handleFetchedResult( + _ result: Result + ) -> String? { + switch result { + case .success(let version): + return getStoreLink(version) + case .failure: + return nil + } + } + + /// 결과값에 따라 ForceUpdate 타입의 info들을 UserDefaults에 저장 + private func saveForceVersionInfo( + _ result: Result + ) { + switch result { + case .success(let version): + let forceUpdate = ForceUpdate( + version: version, + date: Date() + ) + versionCheckRepository.saveForceUpdateInfo(forceUpdate) + case .failure(let error): + print(error) + } + } + + /// Get app store url after comparing version + private func getStoreLink(_ required: AppVersionInfoResponse) -> String? { + return forceUpdateService.compareVersion( + user: versionCheckRepository.getUserAppVersion(), + required: required + ) ? versionCheckRepository.getStoreLink() : nil + } +} diff --git a/Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift b/Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift new file mode 100644 index 00000000..a78afacb --- /dev/null +++ b/Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift @@ -0,0 +1,15 @@ +// +// VersionCheckUseCase.swift +// Domain +// +// Created by Jisoo Ham on 3/25/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +import RxSwift + +public protocol VersionCheckUseCase { + func fetchAppStoreURL() -> Single +} diff --git a/Projects/NetworkService/Sources/EndPoint/AppStoreEndPoint.swift b/Projects/NetworkService/Sources/EndPoint/AppStoreEndPoint.swift new file mode 100644 index 00000000..35a78f27 --- /dev/null +++ b/Projects/NetworkService/Sources/EndPoint/AppStoreEndPoint.swift @@ -0,0 +1,45 @@ +// +// AppStoreEndPoint.swift +// NetworkService +// +// Created by Jisoo HAM on 7/31/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import Foundation + +public struct AppStoreEndPoint: EndPoint { + private let appStoreID: String + + public var scheme: Scheme { + return .https + } + public var host: String { + return "itunes.apple.com" + } + public var port: String { + "" + } + public var path: String { + return "/lookup" + } + public var query: [String: String] { + return [ + "id": appStoreID, + "country": "kr" + ] + } + public var header: [String: String] { + return [:] + } + public var body: [String: Any] { + return [:] + } + public var method: HTTPMethod { + return .get + } + + public init(appStoreID: String) { + self.appStoreID = appStoreID + } +} diff --git a/Projects/NetworkService/Sources/EndPoint/EndPoint.swift b/Projects/NetworkService/Sources/EndPoint/EndPoint.swift index d268c60a..1f7d1a39 100644 --- a/Projects/NetworkService/Sources/EndPoint/EndPoint.swift +++ b/Projects/NetworkService/Sources/EndPoint/EndPoint.swift @@ -20,13 +20,22 @@ public protocol EndPoint { } public enum Scheme: String { - case http, https + case http, https, itms + + var toString: String { + switch self { + case .itms: + "itms-apps" + default: + self.rawValue + } + } } extension EndPoint { public func toURLRequest() throws -> URLRequest { var urlComponent = URLComponents() - urlComponent.scheme = scheme.rawValue + urlComponent.scheme = scheme.toString urlComponent.host = host urlComponent.port = Int(port) urlComponent.path = path @@ -51,6 +60,22 @@ extension EndPoint { } } return urlRequest + } + + public var toURLString: String? { + var urlComponent = URLComponents() + urlComponent.scheme = scheme.toString + urlComponent.host = host + urlComponent.port = Int(port) + urlComponent.path = path + if !query.isEmpty { + urlComponent.queryItems = query.map { + .init(name: $0.key, value: $0.value) + } + } + let urlStr = urlComponent.url?.absoluteString + .replacingOccurrences(of: "%25", with: "%") + return urlStr } } diff --git a/Projects/NetworkService/Sources/EndPoint/MinVersionEndpoint.swift b/Projects/NetworkService/Sources/EndPoint/MinVersionEndpoint.swift new file mode 100644 index 00000000..f6174e99 --- /dev/null +++ b/Projects/NetworkService/Sources/EndPoint/MinVersionEndpoint.swift @@ -0,0 +1,48 @@ +// +// MinVersionEndpoint.swift +// NetworkService +// +// Created by Jisoo Ham on 3/26/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +public struct MinVersionEndpoint: EndPoint { + private var domain: String + + public var scheme: Scheme { + .https + } + + public var host: String { + return domain + } + + public var port: String { + "" + } + + public var path: String { + return "/minVer" + } + + public var query: [String: String] { + return [:] + } + public var header: [String: String] { + return [:] + } + + public var body: [String: Any] { + return [:] + } + + public var method: HTTPMethod { + return .get + } + + public init(domain: String) { + self.domain = domain + } +} diff --git a/Projects/NetworkService/Sources/EndPoint/OpenStoreEndpoint.swift b/Projects/NetworkService/Sources/EndPoint/OpenStoreEndpoint.swift new file mode 100644 index 00000000..e5682b22 --- /dev/null +++ b/Projects/NetworkService/Sources/EndPoint/OpenStoreEndpoint.swift @@ -0,0 +1,49 @@ +// +// OpenStoreEndpoint.swift +// NetworkService +// +// Created by Jisoo Ham on 3/25/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +public struct OpenStoreEndpoint: EndPoint { + private let appStoreID: String + + public var scheme: Scheme { + return .itms + } + + public var host: String { + "itunes.apple.com" + } + + public var port: String { + "" + } + + public var path: String { + "/app/apple-store/\(appStoreID)" + } + + public var query: [String: String] { + return [:] + } + + public var header: [String: String] { + return [:] + } + + public var body: [String: Any] { + return [:] + } + + public var method: HTTPMethod { + return .get + } + + public init(appStoreID: String) { + self.appStoreID = appStoreID + } +} diff --git a/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift b/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift index afc9bd79..1c7ab385 100644 --- a/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift +++ b/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift @@ -58,4 +58,59 @@ public final class DefaultNetworkService: NetworkService { } } } + + public func request( + endPoint: any EndPoint, + responseType: T.Type + ) -> Single> { + return Single.create { observer -> Disposable in + do { + let urlReqeust = try endPoint.toURLRequest() + URLSession.shared.dataTask( + with: urlReqeust + ) { data, response, error in + if let error { + return observer(.success( + .failure(NetworkError.transportError(error)) + )) + } + + guard let httpURLResponse = response as? HTTPURLResponse + else { + observer(.success( + .failure(NetworkError.invalidResponse) + )) + return + } + + guard 200..<300 ~= httpURLResponse.statusCode + else { + return observer(.success(.failure( + NetworkError.invalidStatusCode( + httpURLResponse.statusCode + ) + ))) + } + + guard let data + else { return observer(.success( + .failure(NetworkError.invalidData) + ))} + + do { + let decoded = try JSONDecoder().decode( + responseType, + from: data + ) + observer(.success(.success(decoded))) // 성공적으로 디코딩한 경우 + } catch { + observer(.success(.failure(NetworkError.parseError))) + } + }.resume() + } catch { + observer(.success(.failure(NetworkError.invalidURL))) + } + return Disposables.create() + } + } } diff --git a/Projects/NetworkService/Sources/NetworkService/NetworkService.swift b/Projects/NetworkService/Sources/NetworkService/NetworkService.swift index dff82053..98e4ce0a 100644 --- a/Projects/NetworkService/Sources/NetworkService/NetworkService.swift +++ b/Projects/NetworkService/Sources/NetworkService/NetworkService.swift @@ -12,4 +12,8 @@ import RxSwift public protocol NetworkService { func request(endPoint: EndPoint) -> Observable + func request( + endPoint: EndPoint, + responseType: T.Type + ) -> Single> }