From abc58dd075d555e7bae51df8cf8d861316383b1c Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Sun, 26 May 2024 14:01:44 +0900 Subject: [PATCH 01/32] =?UTF-8?q?[Add]=20#311=20App=20store=EC=97=90=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EB=90=9C=20app=20id=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectDescriptionHelpers/InfoPlist.swift | 3 ++- Projects/Core/Sources/Extension/String+.swift | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift index d82177a8..df70436e 100644 --- a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -50,7 +50,8 @@ 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)" + "INQURY_URL": "$(INQURY_URL)", + "APPLE_ID": "$(APPLE_ID)", ] static let additionalInfoPlist: Self = [ diff --git a/Projects/Core/Sources/Extension/String+.swift b/Projects/Core/Sources/Extension/String+.swift index 4be1e8cc..5da76d19 100644 --- a/Projects/Core/Sources/Extension/String+.swift +++ b/Projects/Core/Sources/Extension/String+.swift @@ -34,10 +34,11 @@ public extension String { return serverKey } + /// 프로젝트 버전 static func getCurrentVersion() -> String { guard let dictionary = Bundle.main.infoDictionary, let version = dictionary["CFBundleShortVersionString"] as? String - else { return "" } + else { return "1" } return version } From 976c89eb39ff11a9c449c491a08c4893322167b0 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Sun, 26 May 2024 14:04:35 +0900 Subject: [PATCH 02/32] =?UTF-8?q?[Add]=20#311=20=EC=95=B1=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EC=96=B4=EC=97=90=20=EB=93=B1=EB=A1=9D=EB=90=9C=20?= =?UTF-8?q?=EC=95=B1=EC=9D=98=20=EC=B5=9C=EC=8B=A0=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=ED=95=A8?= =?UTF-8?q?=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/AppStoreCheck.swift | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 Projects/App/Sources/AppStoreCheck.swift diff --git a/Projects/App/Sources/AppStoreCheck.swift b/Projects/App/Sources/AppStoreCheck.swift new file mode 100644 index 00000000..e45bbcdd --- /dev/null +++ b/Projects/App/Sources/AppStoreCheck.swift @@ -0,0 +1,51 @@ +// +// AppStoreCheck.swift +// App +// +// Created by Jisoo Ham on 5/26/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import UIKit + +import Core + +public final class AppStoreCheck { + + /// 프로젝트 버전 + let appVersion = String.getCurrentVersion() + + /// 앱스토어에 등록된 앱의 ID + static let appleID = Bundle.main.object(forInfoDictionaryKey: "APPLE_ID") as? String + + /// 앱스토어 연결 링크 + static let appStoreURLString + = "itms-apps://itunes.apple.com/app/apple-store/" + + /// 앱스토어에 등록된 최신 버전 가져오는 함수 + static public func latestVersion(completion: @escaping (String?) -> Void) { + guard let appleID = AppStoreCheck.appleID, + let url = URL( + string: "https://itunes.apple.com/lookup?id=\(appleID)&country=kr" + ) + else { return } + + let task = URLSession.shared.dataTask(with: url) { data, response, err in + guard let data, + let json = try? JSONSerialization.jsonObject( + with: data, + options: .allowFragments + ) as? [String: Any], + let results = json["results"] as? [[String: Any]], + let appStoreVersion = results[0]["version"] as? String + else { + completion(nil) + return + } + + completion(appStoreVersion) + } + + task.resume() + } +} From a21519760763b71cb29b22b7f8cf56c892e47497 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Sun, 26 May 2024 14:05:40 +0900 Subject: [PATCH 03/32] =?UTF-8?q?[Add]=20#311=20=EC=95=B1=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EC=96=B4=EC=97=90=20=EC=97=B0=EA=B2=B0=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=ED=95=A9=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/AppStoreCheck.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Projects/App/Sources/AppStoreCheck.swift b/Projects/App/Sources/AppStoreCheck.swift index e45bbcdd..d82907dc 100644 --- a/Projects/App/Sources/AppStoreCheck.swift +++ b/Projects/App/Sources/AppStoreCheck.swift @@ -48,4 +48,15 @@ public final class AppStoreCheck { task.resume() } + + /// URL을 통해 앱스토어 오픈 + static public func openAppStore() { + guard let appleID, + let url = URL(string: AppStoreCheck.appStoreURLString + appleID) + else { return } + + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } } From f5dd3c8833dc7490c82e16c0ba26ff3f2429ff09 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Sun, 26 May 2024 14:50:21 +0900 Subject: [PATCH 04/32] =?UTF-8?q?[Feat]=20#311=20=EC=95=B1=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EC=96=B4=20=EB=B2=84=EC=A0=84=EA=B3=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EB=B2=84=EC=A0=84=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=ED=95=B4=EC=84=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20alert=20=EC=A3=BC=EB=8A=94=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/SceneDelegate.swift | 48 +++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index 2d12b3d9..89020a60 100644 --- a/Projects/App/Sources/SceneDelegate.swift +++ b/Projects/App/Sources/SceneDelegate.swift @@ -20,7 +20,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var deeplinkHandler: DeeplinkHandler? func scene( - _ scene: UIScene, + _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { @@ -37,6 +37,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) appCoordinator?.start() window?.makeKeyAndVisible() + self.checkAndUpdateIfNeeded() deeplinkHandler = .init(appCoordinator: appCoordinator) if let url = connectionOptions.urlContexts.first?.url { deeplinkHandler?.handleUrl(url: url) @@ -52,7 +53,9 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { func sceneWillResignActive(_ scene: UIScene) { } + /// 앱이 Foreground로 전환될때 실행될 함수 func sceneWillEnterForeground(_ scene: UIScene) { + self.checkAndUpdateIfNeeded() } func sceneDidEnterBackground(_ scene: UIScene) { @@ -66,5 +69,48 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { deeplinkHandler?.handleUrl(url: url) } } + + private func checkAndUpdateIfNeeded() { + AppStoreCheck.latestVersion { marketingVersion in + DispatchQueue.main.async { + guard let marketingVersion else { return } + + /// 현재 기기 버전 + let currentProjectVersion = String.getCurrentVersion() + + /// .을 기준으로 나눔 + let splitMarketingVersion = marketingVersion.split(separator: ".").map { $0 } + let splitCurrentProjectVersion = currentProjectVersion.split(separator: ".").map { $0 } + + if splitCurrentProjectVersion.count > 0 && splitMarketingVersion.count > 0 { + // Major 버전만을 비교 + if splitCurrentProjectVersion[0] < splitMarketingVersion[0] { + self.showUpdateAlert(version: marketingVersion) + } else { + print("현재 최신 버전입니다.") + } + + } + } + } + } + + private func showUpdateAlert(version: String) { + let alert = UIAlertController( + title: "업데이트 알림", + message: "더 나은 서비스를 위해 업데이트 되었어요 ! 업데이트 해주세요.", + preferredStyle: .alert + ) + + let alertAction = UIAlertAction( + title: "업데이트", + style: .default) { _ in + AppStoreCheck.openAppStore() + } + + alert.addAction(alertAction) + window?.rootViewController?.present(alert, animated: true) + + } } From 5cefc757f2927ae3d6bc59f2da614e65d836f036 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Sun, 26 May 2024 16:37:59 +0900 Subject: [PATCH 05/32] =?UTF-8?q?[Chore]=20#311=20completion=20block=20->?= =?UTF-8?q?=20async=20await=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/AppStoreCheck.swift | 59 +++++++++++++++++------- Projects/App/Sources/SceneDelegate.swift | 48 ++++++++++--------- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/Projects/App/Sources/AppStoreCheck.swift b/Projects/App/Sources/AppStoreCheck.swift index d82907dc..566ec7ee 100644 --- a/Projects/App/Sources/AppStoreCheck.swift +++ b/Projects/App/Sources/AppStoreCheck.swift @@ -23,30 +23,57 @@ public final class AppStoreCheck { = "itms-apps://itunes.apple.com/app/apple-store/" /// 앱스토어에 등록된 최신 버전 가져오는 함수 - static public func latestVersion(completion: @escaping (String?) -> Void) { +// static public func latestVersion(completion: @escaping (String?) -> Void) { +// guard let appleID = AppStoreCheck.appleID, +// let url = URL( +// string: "https://itunes.apple.com/lookup?id=\(appleID)&country=kr" +// ) +// else { return } +// +// let task = URLSession.shared.dataTask(with: url) { data, response, err in +// guard let data, +// let json = try? JSONSerialization.jsonObject( +// with: data, +// options: .allowFragments +// ) as? [String: Any], +// let results = json["results"] as? [[String: Any]], +// let appStoreVersion = results[0]["version"] as? String +// else { +// completion(nil) +// return +// } +// +// completion(appStoreVersion) +// } +// +// task.resume() +// } + + static public func latestVersion() async -> String? { guard let appleID = AppStoreCheck.appleID, - let url = URL( - string: "https://itunes.apple.com/lookup?id=\(appleID)&country=kr" - ) - else { return } + let url + = URL(string: "https://itunes.apple.com/lookup?id=\(appleID)&country=kr") + else { return nil } - let task = URLSession.shared.dataTask(with: url) { data, response, err in - guard let data, - let json = try? JSONSerialization.jsonObject( - with: data, - options: .allowFragments - ) as? [String: Any], + do { + let (data, _) = try await URLSession.shared.data( + for: URLRequest(url: url) + ) + + guard let json = try? JSONSerialization.jsonObject( + with: data, + options: .allowFragments + ) as? [String: Any], let results = json["results"] as? [[String: Any]], let appStoreVersion = results[0]["version"] as? String else { - completion(nil) - return + return nil } - completion(appStoreVersion) + return appStoreVersion + } catch { + return nil } - - task.resume() } /// URL을 통해 앱스토어 오픈 diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index 89020a60..bed7671e 100644 --- a/Projects/App/Sources/SceneDelegate.swift +++ b/Projects/App/Sources/SceneDelegate.swift @@ -37,7 +37,10 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) appCoordinator?.start() window?.makeKeyAndVisible() - self.checkAndUpdateIfNeeded() + + Task { + await self.checkAndUpdateIfNeeded() + } deeplinkHandler = .init(appCoordinator: appCoordinator) if let url = connectionOptions.urlContexts.first?.url { deeplinkHandler?.handleUrl(url: url) @@ -55,7 +58,9 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { /// 앱이 Foreground로 전환될때 실행될 함수 func sceneWillEnterForeground(_ scene: UIScene) { - self.checkAndUpdateIfNeeded() + Task { + await self.checkAndUpdateIfNeeded() + } } func sceneDidEnterBackground(_ scene: UIScene) { @@ -70,28 +75,25 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - private func checkAndUpdateIfNeeded() { - AppStoreCheck.latestVersion { marketingVersion in - DispatchQueue.main.async { - guard let marketingVersion else { return } - - /// 현재 기기 버전 - let currentProjectVersion = String.getCurrentVersion() - - /// .을 기준으로 나눔 - let splitMarketingVersion = marketingVersion.split(separator: ".").map { $0 } - let splitCurrentProjectVersion = currentProjectVersion.split(separator: ".").map { $0 } - - if splitCurrentProjectVersion.count > 0 && splitMarketingVersion.count > 0 { - // Major 버전만을 비교 - if splitCurrentProjectVersion[0] < splitMarketingVersion[0] { - self.showUpdateAlert(version: marketingVersion) - } else { - print("현재 최신 버전입니다.") - } - - } + private func checkAndUpdateIfNeeded() async { + guard let marketingVersion = await AppStoreCheck.latestVersion() + else { return } + + /// 현재 기기 버전 + let currentProjectVersion = String.getCurrentVersion() + + /// .을 기준으로 나눔 + let splitMarketingVersion = marketingVersion.split(separator: ".").map { $0 } + let splitCurrentProjectVersion = currentProjectVersion.split(separator: ".").map { $0 } + + if splitCurrentProjectVersion.count > 0 && splitMarketingVersion.count > 0 { + // Major 버전만을 비교 + if splitCurrentProjectVersion[0] < splitMarketingVersion[0] { + self.showUpdateAlert(version: marketingVersion) + } else { + print("현재 최신 버전입니다.") } + } } From b8e435339306a462056281ec948bdbe2b701e11a Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Sun, 26 May 2024 16:40:50 +0900 Subject: [PATCH 06/32] =?UTF-8?q?[Remove]=20#311=20legacy=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/AppStoreCheck.swift | 25 ------------------------ 1 file changed, 25 deletions(-) diff --git a/Projects/App/Sources/AppStoreCheck.swift b/Projects/App/Sources/AppStoreCheck.swift index 566ec7ee..4cf28d10 100644 --- a/Projects/App/Sources/AppStoreCheck.swift +++ b/Projects/App/Sources/AppStoreCheck.swift @@ -23,31 +23,6 @@ public final class AppStoreCheck { = "itms-apps://itunes.apple.com/app/apple-store/" /// 앱스토어에 등록된 최신 버전 가져오는 함수 -// static public func latestVersion(completion: @escaping (String?) -> Void) { -// guard let appleID = AppStoreCheck.appleID, -// let url = URL( -// string: "https://itunes.apple.com/lookup?id=\(appleID)&country=kr" -// ) -// else { return } -// -// let task = URLSession.shared.dataTask(with: url) { data, response, err in -// guard let data, -// let json = try? JSONSerialization.jsonObject( -// with: data, -// options: .allowFragments -// ) as? [String: Any], -// let results = json["results"] as? [[String: Any]], -// let appStoreVersion = results[0]["version"] as? String -// else { -// completion(nil) -// return -// } -// -// completion(appStoreVersion) -// } -// -// task.resume() -// } static public func latestVersion() async -> String? { guard let appleID = AppStoreCheck.appleID, From f1f9aada2fd5f48f7497952d24518b90e22aff90 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 28 May 2024 09:11:08 +0900 Subject: [PATCH 07/32] =?UTF-8?q?[Chore]=20#311=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EB=A6=B0=ED=8A=B8?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95,=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectDescriptionHelpers/InfoPlist.swift | 2 +- Projects/App/Sources/AppStoreCheck.swift | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift index df70436e..b74a57b9 100644 --- a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -51,7 +51,7 @@ public extension [String: Plist.Value] { "TERMS_OF_PRIVACY_URL": "$(TERMS_OF_PRIVACY_URL)", "LOCATION_PRIVACY_URL": "$(LOCATION_PRIVACY_URL)", "INQURY_URL": "$(INQURY_URL)", - "APPLE_ID": "$(APPLE_ID)", + "APPSTORE_ID": "$(APPSTORE_ID)", ] static let additionalInfoPlist: Self = [ diff --git a/Projects/App/Sources/AppStoreCheck.swift b/Projects/App/Sources/AppStoreCheck.swift index 4cf28d10..047c397d 100644 --- a/Projects/App/Sources/AppStoreCheck.swift +++ b/Projects/App/Sources/AppStoreCheck.swift @@ -11,12 +11,8 @@ import UIKit import Core public final class AppStoreCheck { - - /// 프로젝트 버전 - let appVersion = String.getCurrentVersion() - /// 앱스토어에 등록된 앱의 ID - static let appleID = Bundle.main.object(forInfoDictionaryKey: "APPLE_ID") as? String + static let appstoreID = Bundle.main.object(forInfoDictionaryKey: "APPSTORE_ID") as? String /// 앱스토어 연결 링크 static let appStoreURLString @@ -25,9 +21,9 @@ public final class AppStoreCheck { /// 앱스토어에 등록된 최신 버전 가져오는 함수 static public func latestVersion() async -> String? { - guard let appleID = AppStoreCheck.appleID, - let url - = URL(string: "https://itunes.apple.com/lookup?id=\(appleID)&country=kr") + guard let appstoreID = AppStoreCheck.appstoreID, + let url + = URL(string: "https://itunes.apple.com/lookup?id=\(appstoreID)&country=kr") else { return nil } do { @@ -53,8 +49,8 @@ public final class AppStoreCheck { /// URL을 통해 앱스토어 오픈 static public func openAppStore() { - guard let appleID, - let url = URL(string: AppStoreCheck.appStoreURLString + appleID) + guard let appstoreID, + let url = URL(string: AppStoreCheck.appStoreURLString + appstoreID) else { return } if UIApplication.shared.canOpenURL(url) { From edffe09c967112fc3b59d14b794f047235af34b7 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Mon, 24 Jun 2024 15:53:36 +0900 Subject: [PATCH 08/32] =?UTF-8?q?[Chore]=20Single=20=ED=98=95=ED=83=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 한 번 구독되고 값이 방출되면 stream이 마무리되는 single 선택 - error 케이스 생성 - private init() 을 통해 객체 생성 방지 --- Projects/App/Sources/AppStoreCheck.swift | 73 +++++++++++++++--------- Projects/App/Sources/SceneDelegate.swift | 59 +++++++++---------- 2 files changed, 77 insertions(+), 55 deletions(-) diff --git a/Projects/App/Sources/AppStoreCheck.swift b/Projects/App/Sources/AppStoreCheck.swift index 047c397d..40bf2666 100644 --- a/Projects/App/Sources/AppStoreCheck.swift +++ b/Projects/App/Sources/AppStoreCheck.swift @@ -10,47 +10,68 @@ import UIKit import Core +import RxSwift + +enum AppStoreError: Error { + case invalidURL + case noData + case parsingError + case networkError(Error) +} + public final class AppStoreCheck { /// 앱스토어에 등록된 앱의 ID - static let appstoreID = Bundle.main.object(forInfoDictionaryKey: "APPSTORE_ID") as? String + static let appstoreID = Bundle.main + .object(forInfoDictionaryKey: "APPSTORE_ID") as? String /// 앱스토어 연결 링크 static let appStoreURLString = "itms-apps://itunes.apple.com/app/apple-store/" - /// 앱스토어에 등록된 최신 버전 가져오는 함수 + private init() { } - static public func latestVersion() async -> String? { - guard let appstoreID = AppStoreCheck.appstoreID, - let url - = URL(string: "https://itunes.apple.com/lookup?id=\(appstoreID)&country=kr") - else { return nil } - - do { - let (data, _) = try await URLSession.shared.data( - for: URLRequest(url: url) - ) - - guard let json = try? JSONSerialization.jsonObject( - with: data, - options: .allowFragments - ) as? [String: Any], - let results = json["results"] as? [[String: Any]], - let appStoreVersion = results[0]["version"] as? String - else { - return nil + /// 앱스토어에 등록된 최신 버전 가져오는 함수 + static public func latestVersion() -> Single { + return Single.create { single in + Task { + do { + guard let appstoreID = AppStoreCheck.appstoreID else { + throw AppStoreError.invalidURL + } + let urlString = "https://itunes.apple.com/lookup?id=\(appstoreID)&country=kr" + guard let url = URL(string: urlString) else { + throw AppStoreError.invalidURL + } + + let (data, _) = try await URLSession + .shared.data(for: URLRequest(url: url)) + let json = try JSONSerialization.jsonObject( + with: data, + options: .allowFragments + ) as? [String: Any] + + guard let results = json?["results"] as? [[String: Any]], + let appStoreVersion = results.first?["version"] + as? String else { + throw AppStoreError.parsingError + } + single(.success(appStoreVersion)) + } catch let error as AppStoreError { + single(.failure(error)) + } catch { + single(.failure(AppStoreError.networkError(error))) + } } - - return appStoreVersion - } catch { - return nil + return Disposables.create() } } /// URL을 통해 앱스토어 오픈 static public func openAppStore() { guard let appstoreID, - let url = URL(string: AppStoreCheck.appStoreURLString + appstoreID) + let url = URL( + string: AppStoreCheck.appStoreURLString + appstoreID + ) else { return } if UIApplication.shared.canOpenURL(url) { diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index bed7671e..8502dd57 100644 --- a/Projects/App/Sources/SceneDelegate.swift +++ b/Projects/App/Sources/SceneDelegate.swift @@ -18,6 +18,8 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var appCoordinator: AppCoordinator? var deeplinkHandler: DeeplinkHandler? + + let disposeBag = DisposeBag() func scene( _ scene: UIScene, @@ -37,10 +39,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) appCoordinator?.start() window?.makeKeyAndVisible() - - Task { - await self.checkAndUpdateIfNeeded() - } + checkAndUpdateIfNeeded() deeplinkHandler = .init(appCoordinator: appCoordinator) if let url = connectionOptions.urlContexts.first?.url { deeplinkHandler?.handleUrl(url: url) @@ -58,9 +57,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { /// 앱이 Foreground로 전환될때 실행될 함수 func sceneWillEnterForeground(_ scene: UIScene) { - Task { - await self.checkAndUpdateIfNeeded() - } + checkAndUpdateIfNeeded() } func sceneDidEnterBackground(_ scene: UIScene) { @@ -75,26 +72,29 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - private func checkAndUpdateIfNeeded() async { - guard let marketingVersion = await AppStoreCheck.latestVersion() - else { return } - - /// 현재 기기 버전 - let currentProjectVersion = String.getCurrentVersion() - - /// .을 기준으로 나눔 - let splitMarketingVersion = marketingVersion.split(separator: ".").map { $0 } - let splitCurrentProjectVersion = currentProjectVersion.split(separator: ".").map { $0 } - - if splitCurrentProjectVersion.count > 0 && splitMarketingVersion.count > 0 { - // Major 버전만을 비교 - if splitCurrentProjectVersion[0] < splitMarketingVersion[0] { - self.showUpdateAlert(version: marketingVersion) - } else { - print("현재 최신 버전입니다.") - } - - } + private func checkAndUpdateIfNeeded() { + AppStoreCheck.latestVersion() + .observe(on: MainScheduler.instance) + .subscribe(onSuccess: { version in + let splitMarketingVersion = version.split(separator: ".") + .map { $0 } + let splitCurrentVersion = String.getCurrentVersion() + .split(separator: ".") + .map { $0 } + + if splitCurrentVersion.count > 0 && + splitMarketingVersion.count > 0 { + // Major 버전만을 비교 + if splitCurrentVersion[0] < splitMarketingVersion[0] { + self.showUpdateAlert(version: version) + } else { + print("현재 최신 버전입니다.") + } + } + }, onFailure: { err in + print(err, #function) + }) + .disposed(by: disposeBag) } private func showUpdateAlert(version: String) { @@ -111,8 +111,9 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { } alert.addAction(alertAction) - window?.rootViewController?.present(alert, animated: true) - + DispatchQueue.main.async { + self.window?.rootViewController?.present(alert, animated: true) + } } } From f0ba96cddbdbde04ff69eaf20757ff75b29d4228 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Thu, 1 Aug 2024 00:01:43 +0900 Subject: [PATCH 09/32] =?UTF-8?q?[Feat]=20AppStore=EC=97=90=20=EC=98=AC?= =?UTF-8?q?=EB=9D=BC=EA=B0=84=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=B0=9B?= =?UTF-8?q?=EB=8A=94=20Endpoint=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 린트에 맞춰 수정 - 싱글톤 형태로 변경 --- Projects/App/Sources/AppStoreCheck.swift | 55 +++++++++++-------- Projects/App/Sources/SceneDelegate.swift | 9 +-- .../Sources/EndPoint/AppStoreEndPoint.swift | 45 +++++++++++++++ 3 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 Projects/NetworkService/Sources/EndPoint/AppStoreEndPoint.swift diff --git a/Projects/App/Sources/AppStoreCheck.swift b/Projects/App/Sources/AppStoreCheck.swift index 40bf2666..d7e9048d 100644 --- a/Projects/App/Sources/AppStoreCheck.swift +++ b/Projects/App/Sources/AppStoreCheck.swift @@ -9,6 +9,8 @@ import UIKit import Core +import Domain +import NetworkService import RxSwift @@ -19,43 +21,51 @@ enum AppStoreError: Error { case networkError(Error) } -public final class AppStoreCheck { - /// 앱스토어에 등록된 앱의 ID - static let appstoreID = Bundle.main - .object(forInfoDictionaryKey: "APPSTORE_ID") as? String +public final class DefaultAppStoreCheck { + static let shared = DefaultAppStoreCheck() - /// 앱스토어 연결 링크 - static let appStoreURLString + private let appstoreID: String? + + public let appStoreURLString = "itms-apps://itunes.apple.com/app/apple-store/" - private init() { } + private init() { + appstoreID = Bundle.main.object(forInfoDictionaryKey: "APPSTORE_ID") + as? String + } - /// 앱스토어에 등록된 최신 버전 가져오는 함수 - static public func latestVersion() -> Single { - return Single.create { single in + public func latestVersion() -> Single { + return Single.create { [weak self] single in + guard let self = self else { + single(.failure(AppStoreError.invalidURL)) + return Disposables.create() + } Task { do { - guard let appstoreID = AppStoreCheck.appstoreID else { - throw AppStoreError.invalidURL - } - let urlString = "https://itunes.apple.com/lookup?id=\(appstoreID)&country=kr" - guard let url = URL(string: urlString) else { + guard let appstoreID = self.appstoreID, + let urlRequest = AppStoreEndPoint( + appStoreID: appstoreID).toURLRequest + else { throw AppStoreError.invalidURL } - + let (data, _) = try await URLSession - .shared.data(for: URLRequest(url: url)) + .shared.data(for: urlRequest) + let json = try JSONSerialization.jsonObject( with: data, options: .allowFragments ) as? [String: Any] - + guard let results = json?["results"] as? [[String: Any]], - let appStoreVersion = results.first?["version"] - as? String else { + let appStoreVersion = results.first?["version"] + as? String + else { throw AppStoreError.parsingError } + single(.success(appStoreVersion)) + } catch let error as AppStoreError { single(.failure(error)) } catch { @@ -65,12 +75,11 @@ public final class AppStoreCheck { return Disposables.create() } } - /// URL을 통해 앱스토어 오픈 - static public func openAppStore() { + public func openAppStore() { guard let appstoreID, let url = URL( - string: AppStoreCheck.appStoreURLString + appstoreID + string: appStoreURLString + appstoreID ) else { return } diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index 8502dd57..8d4d67e2 100644 --- a/Projects/App/Sources/SceneDelegate.swift +++ b/Projects/App/Sources/SceneDelegate.swift @@ -73,7 +73,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func checkAndUpdateIfNeeded() { - AppStoreCheck.latestVersion() + DefaultAppStoreCheck.shared.latestVersion() .observe(on: MainScheduler.instance) .subscribe(onSuccess: { version in let splitMarketingVersion = version.split(separator: ".") @@ -106,9 +106,10 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { let alertAction = UIAlertAction( title: "업데이트", - style: .default) { _ in - AppStoreCheck.openAppStore() - } + style: .default + ) { _ in + DefaultAppStoreCheck.shared.openAppStore() + } alert.addAction(alertAction) DispatchQueue.main.async { 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 + } +} From 3637db523ae9d995f870091e8f4519277750c879 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:38:39 +0900 Subject: [PATCH 10/32] =?UTF-8?q?[Add]=20#311=20AppInfoResponse,=20DTO=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Data/Sources/DTO/AppInfoDTO.swift | 80 +++++++++++++++++++ .../Entity/Response/AppInfoResponse.swift | 31 +++++++ 2 files changed, 111 insertions(+) create mode 100644 Projects/Data/Sources/DTO/AppInfoDTO.swift create mode 100644 Projects/Domain/Sources/Entity/Response/AppInfoResponse.swift diff --git a/Projects/Data/Sources/DTO/AppInfoDTO.swift b/Projects/Data/Sources/DTO/AppInfoDTO.swift new file mode 100644 index 00000000..123fb836 --- /dev/null +++ b/Projects/Data/Sources/DTO/AppInfoDTO.swift @@ -0,0 +1,80 @@ +// +// 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: AppInfoResponse { + let results = results.map { + AppDetailResponse( + releaseNotes: $0.releaseNotes, + releaseDate: $0.releaseDate, + version: $0.version + ) + } + return AppInfoResponse.init( + resultCount: resultCount, + results: results + ) + } +} + +extension AppInfoDTO { + struct AppDetailDTO: Decodable { + let advisories: [String] + let appletvScreenshotUrls: [String] + let artistId: Int + let artistName: String + let artistViewUrl: String + let artworkUrl100: String + let artworkUrl512: String + let artworkUrl60: String + let averageUserRating: Double + let averageUserRatingForCurrentVersion: Double + let bundleId: String + let contentAdvisoryRating: String + let currency: String + let currentVersionReleaseDate: String + let description: String + let features: [String] + let fileSizeBytes: Int + let formattedPrice: String + let genreIds: [String] + let genres: [String] + let ipadScreenshotUrls: [String] + let isGameCenterEnabled: Bool + let isVppDeviceBasedLicensingEnabled: Bool + let kind: String + let languageCodesISO2A: [String] + let minimumOsVersion: String + let price: Double + let primaryGenreId: Int + let primaryGenreName: String + let releaseDate: String + let releaseNotes: String + let screenshotUrls: [String] + let sellerName: String + let supportedDevices: [String] + let trackCensoredName: String + let trackContentRating: String + let trackId: Int + let trackName: String + let trackViewUrl: String + let userRatingCount: Int + let userRatingCountForCurrentVersion: Int + let version: String + let wrapperType: String + } +} diff --git a/Projects/Domain/Sources/Entity/Response/AppInfoResponse.swift b/Projects/Domain/Sources/Entity/Response/AppInfoResponse.swift new file mode 100644 index 00000000..aaf6a84a --- /dev/null +++ b/Projects/Domain/Sources/Entity/Response/AppInfoResponse.swift @@ -0,0 +1,31 @@ +// +// AppInfoResponse.swift +// Domain +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import Foundation + +public struct AppInfoResponse { + let resultCount: Int + let results: [AppDetailResponse] + + public init(resultCount: Int, results: [AppDetailResponse]) { + self.resultCount = resultCount + self.results = results + } +} + +public struct AppDetailResponse: Decodable { + let releaseNotes: String + let releaseDate: String + let version: String + + public init(releaseNotes: String, releaseDate: String, version: String) { + self.releaseNotes = releaseNotes + self.releaseDate = releaseDate + self.version = version + } +} From b4070743c693b6f5ce459f725920440d42545d8a Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Thu, 1 Aug 2024 22:39:30 +0900 Subject: [PATCH 11/32] =?UTF-8?q?[Add]=20#311=20AppStoreRepository=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultAppStoreRepository.swift | 77 +++++++++++++++++++ .../AppstoreRepository.swift | 15 ++++ 2 files changed, 92 insertions(+) create mode 100644 Projects/Data/Sources/Repository/DefaultAppStoreRepository.swift create mode 100644 Projects/Domain/Sources/RepositoryInterface/AppstoreRepository.swift diff --git a/Projects/Data/Sources/Repository/DefaultAppStoreRepository.swift b/Projects/Data/Sources/Repository/DefaultAppStoreRepository.swift new file mode 100644 index 00000000..2c624a1c --- /dev/null +++ b/Projects/Data/Sources/Repository/DefaultAppStoreRepository.swift @@ -0,0 +1,77 @@ +// +// DefaultAppStoreRepository.swift +// Data +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import UIKit + +import Domain +import NetworkService + +import RxSwift + +enum AppStoreError: Error, LocalizedError { + case invalidAppStoreId + case invalidURL + case noData + case parsingError + case networkError(Error) + + var errorDescription: String? { + switch self { + case .invalidAppStoreId: + "AppStore Id 잘못됨" + case .invalidURL: + "AppStore URL 잘못됨" + case .noData: + "Data 잘못됨" + case .parsingError: + "데이터 parsing 잘못됨" + case .networkError(let error): + "\(error) 네트워크 에러" + } + } +} + +public final class DefaultAppStoreRepository: AppstoreRepository { + private let networkService: NetworkService + private let appStoreId = Bundle.main + .object(forInfoDictionaryKey: "APPSTORE_ID") as? String + + public let appStoreURLString + = "itms-apps://itunes.apple.com/app/apple-store/" + + public init(networkService: NetworkService) { + self.networkService = networkService + } + + public func latestVersion() -> Observable { + guard let appStoreId else { + return Observable.error(AppStoreError.invalidAppStoreId) + } + let data = networkService.request( + endPoint: AppStoreEndPoint(appStoreID: appStoreId) + ) + .decode( + type: AppInfoDTO.self, + decoder: JSONDecoder() + ) + .compactMap { $0.toDomain } + return data + } + + public func openAppStore() { + guard let appStoreId, + let url = URL( + string: appStoreURLString + appStoreId + ) + else { return } + + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } +} diff --git a/Projects/Domain/Sources/RepositoryInterface/AppstoreRepository.swift b/Projects/Domain/Sources/RepositoryInterface/AppstoreRepository.swift new file mode 100644 index 00000000..f4348fc1 --- /dev/null +++ b/Projects/Domain/Sources/RepositoryInterface/AppstoreRepository.swift @@ -0,0 +1,15 @@ +// +// AppstoreRepository.swift +// Domain +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import Foundation + +import RxSwift + +public protocol AppstoreRepository { + func latestVersion() -> Observable +} From c09701d1d5c53c91df2d02330fc8433ab9756ca0 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:41:35 +0900 Subject: [PATCH 12/32] =?UTF-8?q?[Remove]=20#311=20=EB=A0=88=EA=B1=B0?= =?UTF-8?q?=EC=8B=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/AppStoreCheck.swift | 90 ------------------- .../DefaultAppStoreRepository.swift | 77 ---------------- .../Entity/Response/AppInfoResponse.swift | 31 ------- .../AppstoreRepository.swift | 15 ---- 4 files changed, 213 deletions(-) delete mode 100644 Projects/App/Sources/AppStoreCheck.swift delete mode 100644 Projects/Data/Sources/Repository/DefaultAppStoreRepository.swift delete mode 100644 Projects/Domain/Sources/Entity/Response/AppInfoResponse.swift delete mode 100644 Projects/Domain/Sources/RepositoryInterface/AppstoreRepository.swift diff --git a/Projects/App/Sources/AppStoreCheck.swift b/Projects/App/Sources/AppStoreCheck.swift deleted file mode 100644 index d7e9048d..00000000 --- a/Projects/App/Sources/AppStoreCheck.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// AppStoreCheck.swift -// App -// -// Created by Jisoo Ham on 5/26/24. -// Copyright © 2024 Pepsi-Club. All rights reserved. -// - -import UIKit - -import Core -import Domain -import NetworkService - -import RxSwift - -enum AppStoreError: Error { - case invalidURL - case noData - case parsingError - case networkError(Error) -} - -public final class DefaultAppStoreCheck { - static let shared = DefaultAppStoreCheck() - - private let appstoreID: String? - - public let appStoreURLString - = "itms-apps://itunes.apple.com/app/apple-store/" - - private init() { - appstoreID = Bundle.main.object(forInfoDictionaryKey: "APPSTORE_ID") - as? String - } - - public func latestVersion() -> Single { - return Single.create { [weak self] single in - guard let self = self else { - single(.failure(AppStoreError.invalidURL)) - return Disposables.create() - } - Task { - do { - guard let appstoreID = self.appstoreID, - let urlRequest = AppStoreEndPoint( - appStoreID: appstoreID).toURLRequest - else { - throw AppStoreError.invalidURL - } - - let (data, _) = try await URLSession - .shared.data(for: urlRequest) - - let json = try JSONSerialization.jsonObject( - with: data, - options: .allowFragments - ) as? [String: Any] - - guard let results = json?["results"] as? [[String: Any]], - let appStoreVersion = results.first?["version"] - as? String - else { - throw AppStoreError.parsingError - } - - single(.success(appStoreVersion)) - - } catch let error as AppStoreError { - single(.failure(error)) - } catch { - single(.failure(AppStoreError.networkError(error))) - } - } - return Disposables.create() - } - } - /// URL을 통해 앱스토어 오픈 - public func openAppStore() { - guard let appstoreID, - let url = URL( - string: appStoreURLString + appstoreID - ) - else { return } - - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - } - } -} diff --git a/Projects/Data/Sources/Repository/DefaultAppStoreRepository.swift b/Projects/Data/Sources/Repository/DefaultAppStoreRepository.swift deleted file mode 100644 index 2c624a1c..00000000 --- a/Projects/Data/Sources/Repository/DefaultAppStoreRepository.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// DefaultAppStoreRepository.swift -// Data -// -// Created by Jisoo HAM on 8/1/24. -// Copyright © 2024 Pepsi-Club. All rights reserved. -// - -import UIKit - -import Domain -import NetworkService - -import RxSwift - -enum AppStoreError: Error, LocalizedError { - case invalidAppStoreId - case invalidURL - case noData - case parsingError - case networkError(Error) - - var errorDescription: String? { - switch self { - case .invalidAppStoreId: - "AppStore Id 잘못됨" - case .invalidURL: - "AppStore URL 잘못됨" - case .noData: - "Data 잘못됨" - case .parsingError: - "데이터 parsing 잘못됨" - case .networkError(let error): - "\(error) 네트워크 에러" - } - } -} - -public final class DefaultAppStoreRepository: AppstoreRepository { - private let networkService: NetworkService - private let appStoreId = Bundle.main - .object(forInfoDictionaryKey: "APPSTORE_ID") as? String - - public let appStoreURLString - = "itms-apps://itunes.apple.com/app/apple-store/" - - public init(networkService: NetworkService) { - self.networkService = networkService - } - - public func latestVersion() -> Observable { - guard let appStoreId else { - return Observable.error(AppStoreError.invalidAppStoreId) - } - let data = networkService.request( - endPoint: AppStoreEndPoint(appStoreID: appStoreId) - ) - .decode( - type: AppInfoDTO.self, - decoder: JSONDecoder() - ) - .compactMap { $0.toDomain } - return data - } - - public func openAppStore() { - guard let appStoreId, - let url = URL( - string: appStoreURLString + appStoreId - ) - else { return } - - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - } - } -} diff --git a/Projects/Domain/Sources/Entity/Response/AppInfoResponse.swift b/Projects/Domain/Sources/Entity/Response/AppInfoResponse.swift deleted file mode 100644 index aaf6a84a..00000000 --- a/Projects/Domain/Sources/Entity/Response/AppInfoResponse.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// AppInfoResponse.swift -// Domain -// -// Created by Jisoo HAM on 8/1/24. -// Copyright © 2024 Pepsi-Club. All rights reserved. -// - -import Foundation - -public struct AppInfoResponse { - let resultCount: Int - let results: [AppDetailResponse] - - public init(resultCount: Int, results: [AppDetailResponse]) { - self.resultCount = resultCount - self.results = results - } -} - -public struct AppDetailResponse: Decodable { - let releaseNotes: String - let releaseDate: String - let version: String - - public init(releaseNotes: String, releaseDate: String, version: String) { - self.releaseNotes = releaseNotes - self.releaseDate = releaseDate - self.version = version - } -} diff --git a/Projects/Domain/Sources/RepositoryInterface/AppstoreRepository.swift b/Projects/Domain/Sources/RepositoryInterface/AppstoreRepository.swift deleted file mode 100644 index f4348fc1..00000000 --- a/Projects/Domain/Sources/RepositoryInterface/AppstoreRepository.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// AppstoreRepository.swift -// Domain -// -// Created by Jisoo HAM on 8/1/24. -// Copyright © 2024 Pepsi-Club. All rights reserved. -// - -import Foundation - -import RxSwift - -public protocol AppstoreRepository { - func latestVersion() -> Observable -} From 86d92478b1da454e4d8ca0f3e27b83b27ae2f5e5 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:43:40 +0900 Subject: [PATCH 13/32] =?UTF-8?q?[Refactor]=20#311=20Endpoint=20Scheme=20?= =?UTF-8?q?=ED=98=95=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=95=B1?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EC=96=B4=20=EB=A7=81=ED=81=AC=EB=A5=BC=20End?= =?UTF-8?q?point=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/EndPoint/EndPoint.swift | 30 +++++++++++- .../Sources/EndPoint/OpenStoreEndpoint.swift | 49 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 Projects/NetworkService/Sources/EndPoint/OpenStoreEndpoint.swift diff --git a/Projects/NetworkService/Sources/EndPoint/EndPoint.swift b/Projects/NetworkService/Sources/EndPoint/EndPoint.swift index 526add4b..70de72e6 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 var toURLRequest: URLRequest? { var urlComponent = URLComponents() - urlComponent.scheme = scheme.rawValue + urlComponent.scheme = scheme.toString urlComponent.host = host urlComponent.port = Int(port) urlComponent.path = path @@ -54,4 +63,21 @@ 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/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 + } +} From 01501d098d9134b8d2a3ebb2adfaf75130f046be Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:44:21 +0900 Subject: [PATCH 14/32] =?UTF-8?q?[Add]=20#311=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=ED=86=B5=EC=8B=A0=20Single=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=20=ED=98=95=ED=83=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultNetworkService.swift | 51 +++++++++++++++++++ .../NetworkService/NetworkService.swift | 4 ++ 2 files changed, 55 insertions(+) diff --git a/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift b/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift index 87b1d2c1..c7e9ef8a 100644 --- a/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift +++ b/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift @@ -78,4 +78,55 @@ public final class DefaultNetworkService: NetworkService { return Disposables.create() } } + + public func request( + endPoint: any EndPoint, + responseType: T.Type + ) -> Single> { + return Single.create { observer -> Disposable in + guard let urlRequest = endPoint.toURLRequest + else { + observer(.success(.failure(NetworkError.invalidURL))) + return Disposables.create() + } + + URLSession.shared.dataTask( + with: urlRequest + ) { data, response, error in + if let error { + return observer(.success( + .failure(NetworkError.transportError(error)) + )) + } + + guard let httpURLResponse = response as? HTTPURLResponse + else { 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() + + 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> } From ac159a215586d064cb344e5014798659b25a28f5 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:45:37 +0900 Subject: [PATCH 15/32] =?UTF-8?q?[Add]=20#311=20itunes=20search=20api=20DT?= =?UTF-8?q?O=20=EB=B0=8F=20Response=20=EB=AA=A8=EB=8D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Data/Sources/DTO/AppInfoDTO.swift | 67 +++++-------------- .../Response/AppVersionInfoResponse.swift | 25 +++++++ 2 files changed, 40 insertions(+), 52 deletions(-) create mode 100644 Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift diff --git a/Projects/Data/Sources/DTO/AppInfoDTO.swift b/Projects/Data/Sources/DTO/AppInfoDTO.swift index 123fb836..f5791ed4 100644 --- a/Projects/Data/Sources/DTO/AppInfoDTO.swift +++ b/Projects/Data/Sources/DTO/AppInfoDTO.swift @@ -16,65 +16,28 @@ public struct AppInfoDTO: Decodable { } extension AppInfoDTO { - var toDomain: AppInfoResponse { - let results = results.map { - AppDetailResponse( - releaseNotes: $0.releaseNotes, - releaseDate: $0.releaseDate, - version: $0.version - ) - } - return AppInfoResponse.init( - resultCount: resultCount, - results: results + 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 advisories: [String] - let appletvScreenshotUrls: [String] - let artistId: Int - let artistName: String - let artistViewUrl: String - let artworkUrl100: String - let artworkUrl512: String - let artworkUrl60: String - let averageUserRating: Double - let averageUserRatingForCurrentVersion: Double - let bundleId: String - let contentAdvisoryRating: String - let currency: String - let currentVersionReleaseDate: String - let description: String - let features: [String] - let fileSizeBytes: Int - let formattedPrice: String - let genreIds: [String] - let genres: [String] - let ipadScreenshotUrls: [String] - let isGameCenterEnabled: Bool - let isVppDeviceBasedLicensingEnabled: Bool - let kind: String - let languageCodesISO2A: [String] - let minimumOsVersion: String - let price: Double - let primaryGenreId: Int - let primaryGenreName: String - let releaseDate: String - let releaseNotes: String - let screenshotUrls: [String] - let sellerName: String - let supportedDevices: [String] - let trackCensoredName: String - let trackContentRating: String let trackId: Int - let trackName: String - let trackViewUrl: String - let userRatingCount: Int - let userRatingCountForCurrentVersion: Int + /// App Version let version: String - let wrapperType: String } } diff --git a/Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift b/Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift new file mode 100644 index 00000000..1c693d7c --- /dev/null +++ b/Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift @@ -0,0 +1,25 @@ +// +// AppVersionInfoResponse.swift +// Domain +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import Foundation + +public struct AppVersionInfoResponse { + 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 + } +} From a06125aae47439bccd57e54c7b9b422ea5be10e2 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:46:29 +0900 Subject: [PATCH 16/32] =?UTF-8?q?[Refactor]=20#311=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=95=B1=20=EB=B2=84=EC=A0=84=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=20=ED=98=95=ED=83=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Core/Sources/Extension/String+.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Projects/Core/Sources/Extension/String+.swift b/Projects/Core/Sources/Extension/String+.swift index 5da76d19..b0fc754e 100644 --- a/Projects/Core/Sources/Extension/String+.swift +++ b/Projects/Core/Sources/Extension/String+.swift @@ -35,11 +35,12 @@ public extension String { } /// 프로젝트 버전 - static func getCurrentVersion() -> String { + static func getCurrentVersion() -> [Int] { guard let dictionary = Bundle.main.infoDictionary, let version = dictionary["CFBundleShortVersionString"] as? String - else { return "1" } - return version + else { return [1, 0, 0] } + + return version.split(separator: ".").compactMap { Int($0) } } static func getDeviceIdentifier() -> String { From 9df3a73a675514907981ef9b61fa89b06fbadb4d Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:46:55 +0900 Subject: [PATCH 17/32] =?UTF-8?q?[Fix]=20#311=20Lint=20=ED=98=95=ED=83=9C?= =?UTF-8?q?=20=EB=A7=9E=EC=B6=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Widget/ArrivalInfo/View/ArrivalInfoView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 63fd2dbb8d9e4af541ff8ba5017a16828becbb3c Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:47:17 +0900 Subject: [PATCH 18/32] =?UTF-8?q?[Add]=20#311=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=EC=9D=84=20=EC=9C=84=ED=95=9C=20Repository?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultVersionCheckRepository.swift | 43 +++++++++++++++++++ .../VersionCheckRepository.swift | 17 ++++++++ 2 files changed, 60 insertions(+) create mode 100644 Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift create mode 100644 Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift diff --git a/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift new file mode 100644 index 00000000..3d251d83 --- /dev/null +++ b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift @@ -0,0 +1,43 @@ +// +// DefaultVersionCheckRepository.swift +// Data +// +// Created by Jisoo HAM on 8/1/24. +// Copyright © 2024 Pepsi-Club. All rights reserved. +// + +import UIKit + +import Domain +import NetworkService + +import RxSwift + +public final class DefaultVersionCheckRepository: VersionCheckRepository { + private let networkService: NetworkService + private let disposeBag: DisposeBag = DisposeBag() + + public init(networkService: NetworkService) { + self.networkService = networkService + } + + public func getAppVersion(appId: String) + -> Single> { + return networkService.request( + endPoint: AppStoreEndPoint(appStoreID: appId), + responseType: AppInfoDTO.self + ) + .map { result in + switch result { + case .success(let value): + return .success(value.toDomain) + case .failure(let error): + return .failure(error) + } + } + } + + public func getStoreLink(appId: String) -> String? { + return OpenStoreEndpoint(appStoreID: appId).toURLString + } +} diff --git a/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift b/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift new file mode 100644 index 00000000..07f8ad4f --- /dev/null +++ b/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift @@ -0,0 +1,17 @@ +// +// 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 getAppVersion(appId: String) + -> Single> + func getStoreLink(appId: String) -> String? +} From bed33bebadc47bce40219ef5898762cb4a8562e6 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:47:44 +0900 Subject: [PATCH 19/32] =?UTF-8?q?[Add]=20#311=20Version=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=EC=9D=84=20=EC=9C=84=ED=95=9C=20UseCase=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UseCase/DefaultVersionCheckUseCase.swift | 66 +++++++++++++++++++ .../Protocol/VersionCheckUseCase.swift | 15 +++++ 2 files changed, 81 insertions(+) create mode 100644 Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift create mode 100644 Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift diff --git a/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift b/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift new file mode 100644 index 00000000..2c5f8031 --- /dev/null +++ b/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift @@ -0,0 +1,66 @@ +// +// 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 disposeBag = DisposeBag() + + public init(versionCheckRepository: VersionCheckRepository) { + self.versionCheckRepository = versionCheckRepository + } + + public func fetchAppStoreURL(appId: String) -> Single { + return versionCheckRepository.getAppVersion(appId: appId) + .map { [weak self] result in + guard let self else { return nil} + switch result { + case .success(let version): + return fetchURLString( + needsToUpdate(version), + appId: appId + ) + case .failure(let error): + print(error, #function) + return nil + } + } + } + + /// 업데이트가 필요하다면 urlString return + private func fetchURLString( + _ isNeeded: Bool, + appId: String + ) -> String? { + if isNeeded { + return versionCheckRepository.getStoreLink(appId: appId) + } else { + return nil + } + } + + /// 앱스토어의 버전과 과 유저의 앱 버전의 major만을 비교하여 Bool 값을 return + private func needsToUpdate(_ version: AppVersionInfoResponse?) -> Bool { + guard let version else { return false } + print(version.major, getUserVersion().major) + return version.major > getUserVersion().major ? true : false + } + + /// User가 사용하는 현재 앱 버전을 확인하기 위한 method + private func getUserVersion() -> AppVersionInfoResponse { + let splitCurrentVersion = String.getCurrentVersion() + return AppVersionInfoResponse( + major: splitCurrentVersion[0], + minor: splitCurrentVersion[1], + patch: splitCurrentVersion[2] + ) + } +} diff --git a/Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift b/Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift new file mode 100644 index 00000000..6ab9423d --- /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(appId: String) -> Single +} From a5df382bdd77fd336c4f69c07bfdcaa4ccf23c79 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:48:13 +0900 Subject: [PATCH 20/32] =?UTF-8?q?[Add]=20#311=20DIContainer=EC=97=90=20Use?= =?UTF-8?q?Case=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/AppDelegate+Register.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Projects/App/Sources/AppDelegate+Register.swift b/Projects/App/Sources/AppDelegate+Register.swift index a4d9241c..ecc57c79 100644 --- a/Projects/App/Sources/AppDelegate+Register.swift +++ b/Projects/App/Sources/AppDelegate+Register.swift @@ -38,6 +38,8 @@ extension AppDelegate { = DefaultLocalNotificationService() let regularAlarmEditingService: RegularAlarmEditingService = DefaultRegularAlarmEditingService() + let versionCheckRepository: VersionCheckRepository + = DefaultVersionCheckRepository(networkService: networkService) DIContainer.register( type: FavoritesUseCase.self, @@ -92,5 +94,12 @@ extension AppDelegate { type: RegularAlarmEditingService.self, regularAlarmEditingService ) + + DIContainer.register( + type: VersionCheckUseCase.self, + DefaultVersionCheckUseCase( + versionCheckRepository: versionCheckRepository + ) + ) } } From fdc2b472023d20a626ab337958a9d5036c8edfa3 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 19:49:43 +0900 Subject: [PATCH 21/32] =?UTF-8?q?[Refcator]=20#311=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EB=B2=84=EC=A0=84=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/App/Sources/SceneDelegate.swift | 59 ++++++++++++++---------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index 8d4d67e2..05b1aa0c 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 @@ -20,6 +21,8 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { var deeplinkHandler: DeeplinkHandler? let disposeBag = DisposeBag() + + @Injected(VersionCheckUseCase.self) var useCase: VersionCheckUseCase func scene( _ scene: UIScene, @@ -39,6 +42,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) appCoordinator?.start() window?.makeKeyAndVisible() + // 앱 진입할 때 확인 checkAndUpdateIfNeeded() deeplinkHandler = .init(appCoordinator: appCoordinator) if let url = connectionOptions.urlContexts.first?.url { @@ -73,31 +77,25 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func checkAndUpdateIfNeeded() { - DefaultAppStoreCheck.shared.latestVersion() - .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { version in - let splitMarketingVersion = version.split(separator: ".") - .map { $0 } - let splitCurrentVersion = String.getCurrentVersion() - .split(separator: ".") - .map { $0 } - - if splitCurrentVersion.count > 0 && - splitMarketingVersion.count > 0 { - // Major 버전만을 비교 - if splitCurrentVersion[0] < splitMarketingVersion[0] { - self.showUpdateAlert(version: version) - } else { - print("현재 최신 버전입니다.") - } - } - }, onFailure: { err in - print(err, #function) - }) + guard let appId = Bundle.main.object( + forInfoDictionaryKey: "APPSTORE_ID" + ) as? String + else { return } + + useCase.fetchAppStoreURL(appId: appId) + .subscribe { [weak self] str in + guard let self, + let urlString = str + else { return } + self.showUpdateAlert(with: urlString) + } onFailure: { error in + print(error) + } .disposed(by: disposeBag) + } - private func showUpdateAlert(version: String) { + private func showUpdateAlert(with urlString: String) { let alert = UIAlertController( title: "업데이트 알림", message: "더 나은 서비스를 위해 업데이트 되었어요 ! 업데이트 해주세요.", @@ -107,14 +105,25 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { let alertAction = UIAlertAction( title: "업데이트", style: .default - ) { _ in - DefaultAppStoreCheck.shared.openAppStore() + ) { [weak self] _ in + guard let self else { return } + + openAppStore(urlString) } alert.addAction(alertAction) - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self else { return } self.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) + } + } } From 70427762e93ee29b1d6178ec1bd2836610aadd56 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:24:31 +0900 Subject: [PATCH 22/32] =?UTF-8?q?[Refactor]=20#311=20dev=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=B0=20Network=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/EndPoint/EndPoint.swift | 1 - .../DefaultNetworkService.swift | 84 ++++++++++--------- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/Projects/NetworkService/Sources/EndPoint/EndPoint.swift b/Projects/NetworkService/Sources/EndPoint/EndPoint.swift index 2390845c..1f7d1a39 100644 --- a/Projects/NetworkService/Sources/EndPoint/EndPoint.swift +++ b/Projects/NetworkService/Sources/EndPoint/EndPoint.swift @@ -60,7 +60,6 @@ extension EndPoint { } } return urlRequest - } public var toURLString: String? { diff --git a/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift b/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift index 74b0fc67..1c7ab385 100644 --- a/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift +++ b/Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift @@ -64,48 +64,52 @@ public final class DefaultNetworkService: NetworkService { responseType: T.Type ) -> Single> { return Single.create { observer -> Disposable in - guard let urlRequest = endPoint.toURLRequest - else { + 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() } - - URLSession.shared.dataTask( - with: urlRequest - ) { data, response, error in - if let error { - return observer(.success( - .failure(NetworkError.transportError(error)) - )) - } - - guard let httpURLResponse = response as? HTTPURLResponse - else { 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() - return Disposables.create() } } From e5aa978294ef23ed78e74dda9b037ecb9d523541 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:34:20 +0900 Subject: [PATCH 23/32] =?UTF-8?q?[Refactor]=20#311=20=EC=98=A4=ED=83=88?= =?UTF-8?q?=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift index b74a57b9..63b2149e 100644 --- a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -50,7 +50,7 @@ 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)", ] From 4f287873f1b5bb60d0c33ed2ec68333d9c4dac41 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:20:13 +0900 Subject: [PATCH 24/32] =?UTF-8?q?[Add]=20#311=20=EC=B5=9C=EC=86=8C?= =?UTF-8?q?=EB=B2=84=EC=A0=84=EC=97=90=20=EB=8C=80=ED=95=9C=20Endpoint=20?= =?UTF-8?q?=EB=B0=8F=20DTO=20=EB=AA=A8=EB=8D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Projects/Data/Sources/DTO/MinVersionDTO.swift | 35 ++++++++++++++ .../Sources/EndPoint/MinVersionEndpoint.swift | 48 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 Projects/Data/Sources/DTO/MinVersionDTO.swift create mode 100644 Projects/NetworkService/Sources/EndPoint/MinVersionEndpoint.swift diff --git a/Projects/Data/Sources/DTO/MinVersionDTO.swift b/Projects/Data/Sources/DTO/MinVersionDTO.swift new file mode 100644 index 00000000..6e501db6 --- /dev/null +++ b/Projects/Data/Sources/DTO/MinVersionDTO.swift @@ -0,0 +1,35 @@ +// +// MinVersionDTO.swift +// Data +// +// Created by Jisoo Ham on 3/26/25. +// Copyright © 2025 Pepsi-Club. All rights reserved. +// + +import Foundation + +import Domain + +public struct MinVersionDTO: Decodable { + let version: String + + enum CodingKeys: String, CodingKey { + case version = "ver" + } +} + +extension MinVersionDTO { + var toDomain: AppVersionInfoResponse? { + let versionComponents = version.split(separator: ".") + .compactMap { Int($0) } + + guard versionComponents.count == 3 + else { return nil } + + return AppVersionInfoResponse( + major: versionComponents[0], + minor: versionComponents[1], + patch: versionComponents[2] + ) + } +} 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 + } +} From 399636013929125d42426476887e0207605841bc Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:21:15 +0900 Subject: [PATCH 25/32] =?UTF-8?q?[Refactor]=20#311=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EB=90=9C=20Endpoint=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Repository/DefaultVersionCheckRepository.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift index 3d251d83..04462998 100644 --- a/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift +++ b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift @@ -24,8 +24,8 @@ public final class DefaultVersionCheckRepository: VersionCheckRepository { public func getAppVersion(appId: String) -> Single> { return networkService.request( - endPoint: AppStoreEndPoint(appStoreID: appId), - responseType: AppInfoDTO.self + endPoint: MinVersionEndpoint(domain: getDomainURL()), + responseType: MinVersionDTO.self ) .map { result in switch result { @@ -40,4 +40,13 @@ public final class DefaultVersionCheckRepository: VersionCheckRepository { public func getStoreLink(appId: String) -> String? { return OpenStoreEndpoint(appStoreID: appId).toURLString } + + private func getDomainURL() -> String { + guard let domainURL = Bundle.main.object( + forInfoDictionaryKey: "DOMAIN_URL" + ) as? String + else { fatalError("Can't Find Domain URL") } + + return domainURL + } } From 2e11d378493788f342c854a75c8347a010356108 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:21:37 +0900 Subject: [PATCH 26/32] =?UTF-8?q?[Add]=20#311=20plist=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift index 63b2149e..49a62959 100644 --- a/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift +++ b/Plugins/EnvironmentPlugin/ProjectDescriptionHelpers/InfoPlist.swift @@ -52,6 +52,7 @@ public extension [String: Plist.Value] { "LOCATION_PRIVACY_URL": "$(LOCATION_PRIVACY_URL)", "INQUIRY_URL": "$(INQUIRY_URL)", "APPSTORE_ID": "$(APPSTORE_ID)", + "DOMAIN_URL": "$(DOMAIN_URL)", ] static let additionalInfoPlist: Self = [ From 65358b3613158a4ad111b9a1498597d880cb0f7e Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Fri, 28 Mar 2025 02:09:31 +0900 Subject: [PATCH 27/32] =?UTF-8?q?[Refactor]=20#311=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=20=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 추후 변경 예정 --- .../App/Sources/AppDelegate+Register.swift | 17 +++++----- Projects/App/Sources/SceneDelegate.swift | 34 +++++++++---------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Projects/App/Sources/AppDelegate+Register.swift b/Projects/App/Sources/AppDelegate+Register.swift index ecc57c79..cd3990f8 100644 --- a/Projects/App/Sources/AppDelegate+Register.swift +++ b/Projects/App/Sources/AppDelegate+Register.swift @@ -38,8 +38,9 @@ extension AppDelegate { = DefaultLocalNotificationService() let regularAlarmEditingService: RegularAlarmEditingService = DefaultRegularAlarmEditingService() - let versionCheckRepository: VersionCheckRepository - = DefaultVersionCheckRepository(networkService: networkService) + // TODO: 추후 의존 주입 형태 변경 +// let versionCheckRepository: VersionCheckRepository +// = DefaultVersionCheckRepository(networkService: networkService) DIContainer.register( type: FavoritesUseCase.self, @@ -95,11 +96,11 @@ extension AppDelegate { regularAlarmEditingService ) - DIContainer.register( - type: VersionCheckUseCase.self, - DefaultVersionCheckUseCase( - versionCheckRepository: versionCheckRepository - ) - ) +// DIContainer.register( +// type: VersionCheckUseCase.self, +// DefaultVersionCheckUseCase( +// versionCheckRepository: versionCheckRepository +// ) +// ) } } diff --git a/Projects/App/Sources/SceneDelegate.swift b/Projects/App/Sources/SceneDelegate.swift index 05b1aa0c..6b7f028b 100644 --- a/Projects/App/Sources/SceneDelegate.swift +++ b/Projects/App/Sources/SceneDelegate.swift @@ -22,7 +22,14 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { let disposeBag = DisposeBag() - @Injected(VersionCheckUseCase.self) var useCase: VersionCheckUseCase + // MARK: 추후 구체타입이 아닌 형태로 변경 + private var useCase: VersionCheckUseCase + = DefaultVersionCheckUseCase( + versionCheckRepository: DefaultVersionCheckRepository( + networkService: DefaultNetworkService() + ), + forceUpdateService: DefaultForceUpdateService() + ) func scene( _ scene: UIScene, @@ -43,7 +50,6 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { appCoordinator?.start() window?.makeKeyAndVisible() // 앱 진입할 때 확인 - checkAndUpdateIfNeeded() deeplinkHandler = .init(appCoordinator: appCoordinator) if let url = connectionOptions.urlContexts.first?.url { deeplinkHandler?.handleUrl(url: url) @@ -77,22 +83,14 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { } private func checkAndUpdateIfNeeded() { - guard let appId = Bundle.main.object( - forInfoDictionaryKey: "APPSTORE_ID" - ) as? String - else { return } - - useCase.fetchAppStoreURL(appId: appId) - .subscribe { [weak self] str in - guard let self, - let urlString = str - else { return } - self.showUpdateAlert(with: urlString) - } onFailure: { error in + 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) { @@ -112,9 +110,9 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate { } alert.addAction(alertAction) - DispatchQueue.main.async { [weak self] in - guard let self else { return } - self.window?.rootViewController?.present(alert, animated: true) + + Task { @MainActor in + window?.rootViewController?.present(alert, animated: true) } } From 40f3e02aa74eb4e7fe9d236b097ee745b84a8217 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Fri, 28 Mar 2025 02:11:53 +0900 Subject: [PATCH 28/32] =?UTF-8?q?[Refactor]=20#311=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=A6=AC=ED=84=B4=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD,?= =?UTF-8?q?=20Comparable=20=EC=B1=84=ED=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 강제 업데이트를 위한 정보를 담은 모델 생성 - major, minor, patch 버전 비교를 위한 Comparable 프로토콜 채택 - DTO -> Response 변환 과정 중 리턴 형태 수정 --- Projects/Data/Sources/DTO/MinVersionDTO.swift | 4 ++-- .../Domain/Sources/Entity/ForceUpdate.swift | 22 +++++++++++++++++++ .../Response/AppVersionInfoResponse.swift | 11 +++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 Projects/Domain/Sources/Entity/ForceUpdate.swift diff --git a/Projects/Data/Sources/DTO/MinVersionDTO.swift b/Projects/Data/Sources/DTO/MinVersionDTO.swift index 6e501db6..5d1e8d4c 100644 --- a/Projects/Data/Sources/DTO/MinVersionDTO.swift +++ b/Projects/Data/Sources/DTO/MinVersionDTO.swift @@ -19,12 +19,12 @@ public struct MinVersionDTO: Decodable { } extension MinVersionDTO { - var toDomain: AppVersionInfoResponse? { + var toDomain: AppVersionInfoResponse { let versionComponents = version.split(separator: ".") .compactMap { Int($0) } guard versionComponents.count == 3 - else { return nil } + else { return AppVersionInfoResponse(major: 1, minor: 2, patch: 5) } return AppVersionInfoResponse( major: versionComponents[0], 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 index 1c693d7c..ecba196e 100644 --- a/Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift +++ b/Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift @@ -8,7 +8,7 @@ import Foundation -public struct AppVersionInfoResponse { +public struct AppVersionInfoResponse: Codable, Comparable { let major: Int let minor: Int let patch: Int @@ -22,4 +22,13 @@ public struct AppVersionInfoResponse { 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 + } } From 28ac0e599c67955919cdd235b8a720ad21b206e0 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Fri, 28 Mar 2025 02:19:37 +0900 Subject: [PATCH 29/32] =?UTF-8?q?[Refactor]=20#311=20=EB=B2=84=EC=A0=84=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=86=8C?= =?UTF-8?q?=EC=8A=A4=EB=93=A4=EC=9D=84=20=EA=B4=80=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20Repository=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DefaultVersionCheckRepository.swift | 56 +++++++++++++++++-- .../VersionCheckRepository.swift | 10 +++- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift index 04462998..32165b1f 100644 --- a/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift +++ b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift @@ -8,6 +8,7 @@ import UIKit +import Core import Domain import NetworkService @@ -15,14 +16,23 @@ import RxSwift public final class DefaultVersionCheckRepository: VersionCheckRepository { private let networkService: NetworkService - private let disposeBag: DisposeBag = DisposeBag() + + @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 } - public func getAppVersion(appId: String) - -> Single> { + /// 서버로 부터 받은 App의 최소 지원 버전 + public func fetchRequiredVersion() + -> Single> { return networkService.request( endPoint: MinVersionEndpoint(domain: getDomainURL()), responseType: MinVersionDTO.self @@ -37,15 +47,49 @@ public final class DefaultVersionCheckRepository: VersionCheckRepository { } } - public func getStoreLink(appId: String) -> String? { - return OpenStoreEndpoint(appStoreID: appId).toURLString + 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 { fatalError("Can't Find Domain URL") } + else { return "" } return domainURL } diff --git a/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift b/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift index 07f8ad4f..a1f9cc0b 100644 --- a/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift +++ b/Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift @@ -11,7 +11,11 @@ import Foundation import RxSwift public protocol VersionCheckRepository: AnyObject { - func getAppVersion(appId: String) - -> Single> - func getStoreLink(appId: String) -> String? + func fetchRequiredVersion() + -> Single> + func getStoreLink() -> String? + func getAppStoreID() -> String + func getUserAppVersion() -> AppVersionInfoResponse + func saveForceUpdateInfo(_ info: ForceUpdate) + func getForceUpdateInfo() -> ForceUpdate } From ac363ddcc91066e784c66e68e504ab67a7ff9797 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Fri, 28 Mar 2025 02:20:57 +0900 Subject: [PATCH 30/32] =?UTF-8?q?[Add]=20#311=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=9D=84=20=EB=B9=84=EA=B5=90=ED=95=98=EB=8A=94=20Service=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 추후 앱스토어 버전을 비교하는 형태 추가 예정 --- .../DefaultForceUpdateService.swift | 23 +++++++++++++++++++ .../ForceUpdateService.swift | 16 +++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 Projects/Data/Sources/Service/ForceUpdateService/DefaultForceUpdateService.swift create mode 100644 Projects/Domain/Sources/Service/ForceUpdateService/ForceUpdateService.swift 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/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 +} From 9e2a2048c0e4f71b839e6ead0a05931de325a1aa Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Fri, 28 Mar 2025 02:21:59 +0900 Subject: [PATCH 31/32] =?UTF-8?q?[Refactor]=20#311=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=EC=9D=84=20=EB=B9=84=EA=B5=90=ED=95=98=EB=8A=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UseCase/DefaultVersionCheckUseCase.swift | 95 ++++++++++++------- .../Protocol/VersionCheckUseCase.swift | 2 +- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift b/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift index 2c5f8031..edfa0b56 100644 --- a/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift +++ b/Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift @@ -12,55 +12,80 @@ import RxSwift public final class DefaultVersionCheckUseCase: VersionCheckUseCase { private let versionCheckRepository: VersionCheckRepository - private let disposeBag = DisposeBag() + private let forceUpdateService: ForceUpdateService - public init(versionCheckRepository: VersionCheckRepository) { + public init( + versionCheckRepository: VersionCheckRepository, + forceUpdateService: ForceUpdateService + ) { self.versionCheckRepository = versionCheckRepository + self.forceUpdateService = forceUpdateService } - public func fetchAppStoreURL(appId: String) -> Single { - return versionCheckRepository.getAppVersion(appId: appId) + 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} - switch result { - case .success(let version): - return fetchURLString( - needsToUpdate(version), - appId: appId - ) - case .failure(let error): - print(error, #function) - return nil - } + guard let self else { return nil } + return handleFetchedResult(result) } } - /// 업데이트가 필요하다면 urlString return - private func fetchURLString( - _ isNeeded: Bool, - appId: String + /// 서버 통신의 결과 상태를 기반으로 app store Link 반환 + private func handleFetchedResult( + _ result: Result ) -> String? { - if isNeeded { - return versionCheckRepository.getStoreLink(appId: appId) - } else { + switch result { + case .success(let version): + return getStoreLink(version) + case .failure: return nil } } - /// 앱스토어의 버전과 과 유저의 앱 버전의 major만을 비교하여 Bool 값을 return - private func needsToUpdate(_ version: AppVersionInfoResponse?) -> Bool { - guard let version else { return false } - print(version.major, getUserVersion().major) - return version.major > getUserVersion().major ? true : false + /// 결과값에 따라 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) + } } - /// User가 사용하는 현재 앱 버전을 확인하기 위한 method - private func getUserVersion() -> AppVersionInfoResponse { - let splitCurrentVersion = String.getCurrentVersion() - return AppVersionInfoResponse( - major: splitCurrentVersion[0], - minor: splitCurrentVersion[1], - patch: splitCurrentVersion[2] - ) + /// 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 index 6ab9423d..a78afacb 100644 --- a/Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift +++ b/Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift @@ -11,5 +11,5 @@ import Foundation import RxSwift public protocol VersionCheckUseCase { - func fetchAppStoreURL(appId: String) -> Single + func fetchAppStoreURL() -> Single } From 9cf728c8ef4f413bd8081812ba46002d627f78a2 Mon Sep 17 00:00:00 2001 From: Jisoo Ham <133845468+isakatty@users.noreply.github.com> Date: Fri, 28 Mar 2025 02:26:12 +0900 Subject: [PATCH 32/32] =?UTF-8?q?[Refactor]=20#311=20=EC=B5=9C=EC=86=8C=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20=EB=B2=84=EC=A0=84=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/{MinVersionDTO.swift => RequiredVersionDTO.swift} | 6 +++--- .../Sources/Repository/DefaultVersionCheckRepository.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename Projects/Data/Sources/DTO/{MinVersionDTO.swift => RequiredVersionDTO.swift} (87%) diff --git a/Projects/Data/Sources/DTO/MinVersionDTO.swift b/Projects/Data/Sources/DTO/RequiredVersionDTO.swift similarity index 87% rename from Projects/Data/Sources/DTO/MinVersionDTO.swift rename to Projects/Data/Sources/DTO/RequiredVersionDTO.swift index 5d1e8d4c..d3854177 100644 --- a/Projects/Data/Sources/DTO/MinVersionDTO.swift +++ b/Projects/Data/Sources/DTO/RequiredVersionDTO.swift @@ -1,5 +1,5 @@ // -// MinVersionDTO.swift +// RequiredVersionDTO.swift // Data // // Created by Jisoo Ham on 3/26/25. @@ -10,7 +10,7 @@ import Foundation import Domain -public struct MinVersionDTO: Decodable { +public struct RequiredVersionDTO: Decodable { let version: String enum CodingKeys: String, CodingKey { @@ -18,7 +18,7 @@ public struct MinVersionDTO: Decodable { } } -extension MinVersionDTO { +extension RequiredVersionDTO { var toDomain: AppVersionInfoResponse { let versionComponents = version.split(separator: ".") .compactMap { Int($0) } diff --git a/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift index 32165b1f..3575999b 100644 --- a/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift +++ b/Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift @@ -35,7 +35,7 @@ public final class DefaultVersionCheckRepository: VersionCheckRepository { -> Single> { return networkService.request( endPoint: MinVersionEndpoint(domain: getDomainURL()), - responseType: MinVersionDTO.self + responseType: RequiredVersionDTO.self ) .map { result in switch result {