diff --git a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift
index cbf692c6..84ee98d8 100644
--- a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift
+++ b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/InfoPlist.swift
@@ -13,6 +13,8 @@ public enum IdleInfoPlist {
"CFBundleDisplayName": "케어밋",
+ "CFBundleShortVersionString" : "1.0.0",
+
"NSAppTransportSecurity" : [
"NSAllowsArbitraryLoads" : true
],
diff --git a/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/TargetScripts.swift b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/TargetScripts.swift
new file mode 100644
index 00000000..b77038b3
--- /dev/null
+++ b/project/Plugins/ConfigurationPlugin/ProjectDescriptionHelpers/TargetScripts.swift
@@ -0,0 +1,29 @@
+//
+// TargetScripts.swift
+// ConfigurationPlugin
+//
+// Created by choijunios on 9/14/24.
+//
+
+import Foundation
+import ProjectDescription
+
+// MARK: Firebase crashlytics
+
+public extension TargetScript {
+
+ static let crashlyticsScript: TargetScript = .post(
+ script: """
+ ROOT_DIR=\(ProcessInfo.processInfo.environment["TUIST_ROOT_DIR"] ?? "")
+ "${ROOT_DIR}/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run"
+ """,
+ name: "Firebase Crashlytics",
+ inputPaths: [
+ "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}",
+ "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist",
+ "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist",
+ "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)",
+ ],
+ basedOnDependencyAnalysis: false
+ )
+}
diff --git a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift
index e4c67a36..3e33d3e8 100644
--- a/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift
+++ b/project/Plugins/DependencyPlugin/ProjectDescriptionHelpers/Dependency.swift
@@ -44,6 +44,9 @@ public extension ModuleDependency {
public static let FSCalendar: TargetDependency = .external(name: "FSCalendar")
public static let NaverMapSDKForSPM: TargetDependency = .external(name: "Junios.NMapSDKForSPM")
public static let KingFisher: TargetDependency = .external(name: "Kingfisher")
+ public static let FirebaseRemoteConfig: TargetDependency = .external(name: "FirebaseRemoteConfig")
+ public static let FirebaseCrashlytics: TargetDependency = .external(name: "FirebaseCrashlytics")
+ public static let FirebaseAnalytics: TargetDependency = .external(name: "FirebaseAnalytics")
}
}
diff --git a/project/Projects/App/Project.swift b/project/Projects/App/Project.swift
index 2f9f9405..5dfd820c 100644
--- a/project/Projects/App/Project.swift
+++ b/project/Projects/App/Project.swift
@@ -27,6 +27,9 @@ let project = Project(
infoPlist: IdleInfoPlist.appDefault,
sources: ["Sources/**"],
resources: ["Resources/**"],
+ scripts: [
+ .crashlyticsScript
+ ],
dependencies: [
// Presentation
@@ -39,7 +42,7 @@ let project = Project(
D.Data.ConcreteRepository,
// ThirdParty
- D.ThirdParty.Swinject
+ D.ThirdParty.Swinject,
],
settings: .settings(
configurations: IdleConfiguration.appConfigurations
diff --git a/project/Projects/App/Resources/GoogleService-Info.plist b/project/Projects/App/Resources/GoogleService-Info.plist
new file mode 100644
index 00000000..e902d244
--- /dev/null
+++ b/project/Projects/App/Resources/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+
+
+
+
+ API_KEY
+ AIzaSyB4eId4P8P3XlpHIVINfVpyB-pMQmqNxQM
+ GCM_SENDER_ID
+ 740246202578
+ PLIST_VERSION
+ 1
+ BUNDLE_ID
+ com.idle.caremeet
+ PROJECT_ID
+ swm-3idiots
+ STORAGE_BUCKET
+ swm-3idiots.appspot.com
+ IS_ADS_ENABLED
+
+ IS_ANALYTICS_ENABLED
+
+ IS_APPINVITE_ENABLED
+
+ IS_GCM_ENABLED
+
+ IS_SIGNIN_ENABLED
+
+ GOOGLE_APP_ID
+ 1:740246202578:ios:d5e59f4f2116f92342250b
+
+
\ No newline at end of file
diff --git a/project/Projects/App/Sources/AppDelegate.swift b/project/Projects/App/Sources/AppDelegate.swift
index 2a996576..69eba4b1 100644
--- a/project/Projects/App/Sources/AppDelegate.swift
+++ b/project/Projects/App/Sources/AppDelegate.swift
@@ -9,18 +9,20 @@ import UIKit
import AppTrackingTransparency
import AdSupport
import PresentationCore
+import FirebaseCore
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
-
-
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { [weak self] in
self?.requestTrackingAuthorization()
})
+ // FireBase setting
+ FirebaseApp.configure()
+
return true
}
diff --git a/project/Projects/App/Sources/DI/Assembly/DataAssembly.swift b/project/Projects/App/Sources/DI/Assembly/DataAssembly.swift
index be84b0d0..7a337df4 100644
--- a/project/Projects/App/Sources/DI/Assembly/DataAssembly.swift
+++ b/project/Projects/App/Sources/DI/Assembly/DataAssembly.swift
@@ -46,5 +46,10 @@ public struct DataAssembly: Assembly {
container.register(RecruitmentPostRepository.self) { _ in
return DefaultRecruitmentPostRepository()
}
+
+ // MARK: RemoteConfig
+ container.register(RemoteConfigRepository.self) { _ in
+ return DefaultRemoteConfigRepository()
+ }
}
}
diff --git a/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift b/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift
index 159f158f..87e41458 100644
--- a/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift
+++ b/project/Projects/App/Sources/RootCoordinator/RootCoordinator.swift
@@ -38,7 +38,8 @@ class RootCoordinator {
authUseCase: injector.resolve(AuthUseCase.self),
workerProfileUseCase: injector.resolve(WorkerProfileUseCase.self),
centerProfileUseCase: injector.resolve(CenterProfileUseCase.self),
- userInfoLocalRepository: injector.resolve(UserInfoLocalRepository.self)
+ userInfoLocalRepository: injector.resolve(UserInfoLocalRepository.self),
+ remoteConfigRepository: injector.resolve(RemoteConfigRepository.self)
)
vc.bind(viewModel: vm)
diff --git a/project/Projects/Data/ConcreteRepository/RemoteConfig/asd.swift b/project/Projects/Data/ConcreteRepository/RemoteConfig/asd.swift
new file mode 100644
index 00000000..18804d06
--- /dev/null
+++ b/project/Projects/Data/ConcreteRepository/RemoteConfig/asd.swift
@@ -0,0 +1,58 @@
+//
+// asd.swift
+// ConcreteRepository
+//
+// Created by choijunios on 9/13/24.
+//
+
+import Foundation
+import RepositoryInterface
+
+import RxSwift
+import FirebaseRemoteConfig
+import Entity
+
+public class DefaultRemoteConfigRepository: RemoteConfigRepository {
+
+ private let remoteConfig = RemoteConfig.remoteConfig()
+ private let settings = RemoteConfigSettings()
+
+ public init() {
+ remoteConfig.configSettings = settings
+ }
+
+ public func fetchRemoteConfig() -> RxSwift.Single> {
+
+ Single.create { [weak self] single in
+
+ self?.remoteConfig.fetch { status, error in
+
+ if status == .success {
+ self?.remoteConfig.activate()
+ single(.success(.success(true)))
+ } else {
+ single(.success(.success(false)))
+ }
+ }
+
+ return Disposables.create { }
+ }
+ }
+
+ public func getForceUpdateInfo() -> ForceUpdateInfo? {
+ let jsonData = remoteConfig["forceUpdate_iOS"].jsonValue
+
+ if let jsonData {
+
+ do {
+ let data = try JSONSerialization.data(withJSONObject: jsonData)
+ let decoded = try JSONDecoder().decode(ForceUpdateInfo.self, from: data)
+ return decoded
+ } catch {
+
+ return nil
+ }
+ }
+ return nil
+ }
+}
diff --git a/project/Projects/Data/DataSource/API/AuthAPI.swift b/project/Projects/Data/DataSource/API/AuthAPI.swift
index c7b16bc7..e84925b1 100644
--- a/project/Projects/Data/DataSource/API/AuthAPI.swift
+++ b/project/Projects/Data/DataSource/API/AuthAPI.swift
@@ -168,10 +168,6 @@ extension AuthAPI: BaseAPI {
}
}
- public var validationType: ValidationType {
- .successCodes
- }
-
public var task: Task {
switch self {
case .startPhoneNumberAuth:
diff --git a/project/Projects/Data/DataSource/Service/BaseNetworkService.swift b/project/Projects/Data/DataSource/Service/BaseNetworkService.swift
index 048ee8fa..2472155c 100644
--- a/project/Projects/Data/DataSource/Service/BaseNetworkService.swift
+++ b/project/Projects/Data/DataSource/Service/BaseNetworkService.swift
@@ -186,14 +186,51 @@ public extension BaseNetworkService {
provider.rx
.request(api)
.catch { error in
- if let moyaError = error as? MoyaError {
+
+ let moyaError = error as! MoyaError
+
+ // 재시도 실패 or 근본적인 에러(Ex 타임아웃, 네트워크 끊어짐)
+ if case let .underlying(error, response) = moyaError {
- var response: Response?
+ // 리타리어 실패
+ if let afError = error.asAFError {
+
+ if case .requestRetryFailed = afError, let response {
+
+ return .error(
+ HTTPResponseException(response: response)
+ )
+ }
+ }
- if case let .underlying(_, res) = moyaError { response = res }
- if case let .statusCode(res) = moyaError { response = res }
- if let response { return .error(HTTPResponseException(response: response)) }
+ // 근본적인 문제
+ if let urlError = error as? URLError {
+
+ var underlyingError: UnderLyingError!
+
+ switch urlError.code {
+ case .notConnectedToInternet:
+ underlyingError = .internetNotConnected
+ case .networkConnectionLost:
+ underlyingError = .networkConnectionLost
+ case .timedOut:
+ underlyingError = .timeout
+ default:
+ underlyingError = .unHandledError
+ }
+
+ return .error(underlyingError)
+ }
}
+
+ // HTTP통신 에러
+ if case let .statusCode(response) = moyaError {
+ return .error(
+ HTTPResponseException(response: response)
+ )
+ }
+
+ // 엣지 케이스
return .error(error)
}
}
diff --git a/project/Projects/Data/DataSource/Service/asd.swift b/project/Projects/Data/DataSource/Service/CrawlingPostService.swift
similarity index 100%
rename from project/Projects/Data/DataSource/Service/asd.swift
rename to project/Projects/Data/DataSource/Service/CrawlingPostService.swift
diff --git a/project/Projects/Data/Project.swift b/project/Projects/Data/Project.swift
index 3745174c..68b28f0a 100644
--- a/project/Projects/Data/Project.swift
+++ b/project/Projects/Data/Project.swift
@@ -30,7 +30,8 @@ let project = Project(
D.Data.DataSource,
// ThirdParty
- D.ThirdParty.RxSwift
+ D.ThirdParty.RxSwift,
+ D.ThirdParty.FirebaseRemoteConfig
],
settings: .settings(
base: ["ENABLE_TESTABILITY": "YES"]
diff --git a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift
index 58969877..401df140 100644
--- a/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift
+++ b/project/Projects/Domain/ConcreteUseCase/RecruitmentPost/DefualtRecruitmentPostUseCase.swift
@@ -157,7 +157,8 @@ public class DefaultRecruitmentPostUseCase: RecruitmentPostUseCase {
requestCnt: postCount
)
case .thirdParty:
- return .just(.failure(.undefinedError))
+ // Presentation에서 에초에 호출하지 않음
+ return .just(.failure(.undefinedCode))
}
}
diff --git a/project/Projects/Domain/Entity/Config/ForceUpdateInfo.swift b/project/Projects/Domain/Entity/Config/ForceUpdateInfo.swift
new file mode 100644
index 00000000..00007e40
--- /dev/null
+++ b/project/Projects/Domain/Entity/Config/ForceUpdateInfo.swift
@@ -0,0 +1,14 @@
+//
+// ForceUpdateInfo.swift
+// Entity
+//
+// Created by choijunios on 9/13/24.
+//
+
+import Foundation
+
+public struct ForceUpdateInfo: Decodable {
+ public let minVersion: String
+ public let marketUrl: String
+ public let noticeMsg: String
+}
diff --git a/project/Projects/Domain/Entity/Error/DomainError.swift b/project/Projects/Domain/Entity/Error/DomainError.swift
index 210c3aea..55fa1c8f 100644
--- a/project/Projects/Domain/Entity/Error/DomainError.swift
+++ b/project/Projects/Domain/Entity/Error/DomainError.swift
@@ -7,7 +7,7 @@
import Foundation
-public enum DomainError: Error {
+public enum DomainError: Error, Equatable {
// API
case invalidParameter
@@ -58,10 +58,8 @@ public enum DomainError: Error {
// undefinedError
case undefinedCode
- case undefinedError
- // Not Implemented
- case notImplemented
+ case undelyingError(error: UnderLyingError)
public init(code: String) {
switch code {
@@ -123,7 +121,7 @@ public enum DomainError: Error {
self = .geoCodingFailure
default:
- self = .undefinedError
+ self = .undefinedCode
}
}
@@ -199,11 +197,20 @@ public enum DomainError: Error {
case .geoCodingFailure:
return "입력된 주소로 지리 정보를 찾을 수 없습니다. 주소를 다시 확인해주세요."
- case .undefinedCode, .undefinedError:
- return "예기치 않은 오류가 발생했습니다. 잠시 후 다시 시도해주세요."
-
- case .notImplemented:
- return "아직 개발되지 않은 기능입니다."
+ case .undefinedCode:
+ return "처리되지 않은 HTTP코드입니다."
+
+ case .undelyingError(let underlyingError):
+ switch underlyingError {
+ case .internetNotConnected:
+ return "인터넷이 연결되지 않았습니다."
+ case .timeout:
+ return "요청시간을 초과했습니다."
+ case .networkConnectionLost:
+ return "연결상태가 변경됬습니다."
+ case .unHandledError:
+ return "알 수 없는 문제가 발생했습니다."
+ }
}
}
}
diff --git a/project/Projects/Domain/Entity/Error/NetworkError/UnderlyingError.swift b/project/Projects/Domain/Entity/Error/NetworkError/UnderlyingError.swift
new file mode 100644
index 00000000..0b275cbf
--- /dev/null
+++ b/project/Projects/Domain/Entity/Error/NetworkError/UnderlyingError.swift
@@ -0,0 +1,16 @@
+//
+// Entity.swift
+// ConcreteUseCase
+//
+// Created by choijunios on 9/13/24.
+//
+
+import Foundation
+
+public enum UnderLyingError: Error {
+
+ case internetNotConnected
+ case timeout
+ case networkConnectionLost
+ case unHandledError
+}
diff --git a/project/Projects/Domain/Entity/VO/AlertContentVO.swift b/project/Projects/Domain/Entity/VO/AlertContentVO.swift
index ee5d4ee4..902f0b1a 100644
--- a/project/Projects/Domain/Entity/VO/AlertContentVO.swift
+++ b/project/Projects/Domain/Entity/VO/AlertContentVO.swift
@@ -11,11 +11,13 @@ public struct DefaultAlertContentVO {
public let title: String
public let message: String
+ public let dismissButtonLabelText: String
public let onDismiss: (() -> ())?
- public init(title: String, message: String, onDismiss: (() -> ())? = nil) {
+ public init(title: String, message: String, dismissButtonLabelText: String = "닫기", onDismiss: (() -> ())? = nil) {
self.title = title
self.message = message
+ self.dismissButtonLabelText = dismissButtonLabelText
self.onDismiss = onDismiss
}
diff --git a/project/Projects/Domain/RepositoryInterface/RemotConfig/RemoteConfigRepository.swift b/project/Projects/Domain/RepositoryInterface/RemotConfig/RemoteConfigRepository.swift
new file mode 100644
index 00000000..17886bc4
--- /dev/null
+++ b/project/Projects/Domain/RepositoryInterface/RemotConfig/RemoteConfigRepository.swift
@@ -0,0 +1,20 @@
+//
+// RemoteConfigRepository.swift
+// RepositoryInterface
+//
+// Created by choijunios on 9/13/24.
+//
+
+import Foundation
+import Entity
+
+import RxSwift
+
+public protocol RemoteConfigRepository: RepositoryBase {
+
+ /// RemoteConfig를 fetch합니다.
+ func fetchRemoteConfig() -> Single>
+
+ /// 강제업데이트 정보를 가져옵니다.
+ func getForceUpdateInfo() -> ForceUpdateInfo?
+}
diff --git a/project/Projects/Domain/UseCaseInterface/Auth/AuthInputValidationUseCase.swift b/project/Projects/Domain/UseCaseInterface/Auth/AuthInputValidationUseCase.swift
index 2917d6c4..df830a1d 100644
--- a/project/Projects/Domain/UseCaseInterface/Auth/AuthInputValidationUseCase.swift
+++ b/project/Projects/Domain/UseCaseInterface/Auth/AuthInputValidationUseCase.swift
@@ -19,7 +19,7 @@ import Entity
/// - #7. 아이디 중복확인
/// - #8. 패스워드 유효성 확인
-public protocol AuthInputValidationUseCase: UseCaseBase {
+public protocol AuthInputValidationUseCase: BaseUseCase {
// #1.
/// 전화번호 인증 요청
diff --git a/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift b/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift
index 78e31333..4f96b481 100644
--- a/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift
+++ b/project/Projects/Domain/UseCaseInterface/Auth/AuthUseCase.swift
@@ -9,7 +9,7 @@ import Foundation
import RxSwift
import Entity
-public protocol AuthUseCase: UseCaseBase {
+public protocol AuthUseCase: BaseUseCase {
/// 센터 회원가입 실행
diff --git a/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift b/project/Projects/Domain/UseCaseInterface/BaseUseCase.swift
similarity index 76%
rename from project/Projects/Domain/UseCaseInterface/UseCaseBase.swift
rename to project/Projects/Domain/UseCaseInterface/BaseUseCase.swift
index 26ca6512..884fcca0 100644
--- a/project/Projects/Domain/UseCaseInterface/UseCaseBase.swift
+++ b/project/Projects/Domain/UseCaseInterface/BaseUseCase.swift
@@ -9,9 +9,9 @@ import Foundation
import RxSwift
import Entity
-public protocol UseCaseBase: AnyObject { }
+public protocol BaseUseCase: AnyObject { }
-public extension UseCaseBase {
+public extension BaseUseCase {
/// Repository로 부터 전달받은 언어레벨의 에러를 도메인 특화 에러로 변경하고, error를 Result의 Failure로, 성공을 Success로 변경합니다.
func convert(task: Single) -> Single> {
@@ -28,7 +28,8 @@ public extension UseCaseBase {
// MARK: InputValidationError
private func toDomainError(error: Error) -> DomainError {
-
+
+ // 네트워크 에러
if let httpError = error as? HTTPResponseException {
if let code = httpError.rawCode {
@@ -49,6 +50,14 @@ public extension UseCaseBase {
#endif
}
- return DomainError.undefinedError
+ // 네트워크 에러보다 근본적인 에러
+ if let underlyingError = error as? UnderLyingError {
+
+ let domainError: DomainError = .undelyingError(error: underlyingError)
+
+ return domainError
+ }
+
+ return DomainError.undelyingError(error: .unHandledError)
}
}
diff --git a/project/Projects/Domain/UseCaseInterface/CenterCertificate/CenterCertificateUseCase.swift b/project/Projects/Domain/UseCaseInterface/CenterCertificate/CenterCertificateUseCase.swift
index 4bd9c592..a82446e9 100644
--- a/project/Projects/Domain/UseCaseInterface/CenterCertificate/CenterCertificateUseCase.swift
+++ b/project/Projects/Domain/UseCaseInterface/CenterCertificate/CenterCertificateUseCase.swift
@@ -10,7 +10,7 @@ import Entity
import RxSwift
-public protocol CenterCertificateUseCase: UseCaseBase {
+public protocol CenterCertificateUseCase: BaseUseCase {
/// 센터 로그아웃
func signoutCenterAccount() -> Single>
diff --git a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift
index a1689a7a..b4fce273 100644
--- a/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift
+++ b/project/Projects/Domain/UseCaseInterface/RecruitmentPost/RecruitmentPostUseCase.swift
@@ -9,7 +9,7 @@ import Foundation
import Entity
import RxSwift
-public protocol RecruitmentPostUseCase: UseCaseBase {
+public protocol RecruitmentPostUseCase: BaseUseCase {
// MARK: Center
diff --git a/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift b/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift
index f6bb3874..fa6e1e62 100644
--- a/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift
+++ b/project/Projects/Domain/UseCaseInterface/Setting/SettingScreenUseCase .swift
@@ -15,7 +15,7 @@ public enum NotificationApproveAction: Equatable {
case error(message: String)
}
-public protocol SettingScreenUseCase: UseCaseBase {
+public protocol SettingScreenUseCase: BaseUseCase {
/// 현재 알람수신 동의 여부를 확인합니다.
func checkPushNotificationApproved() -> Single
diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift
index 1193d9bc..e958d502 100644
--- a/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift
+++ b/project/Projects/Domain/UseCaseInterface/UserInfo/CenterProfileUseCase.swift
@@ -16,7 +16,7 @@ import Entity
/// 5. 센터 프로필 최초 등록
/// 6. 특정 센터의 프로필 불러오기
-public protocol CenterProfileUseCase: UseCaseBase {
+public protocol CenterProfileUseCase: BaseUseCase {
/// 1. 나의 센터/다른 센터 프로필 정보 조회
/// 6. 특정 센터의 프로필 불러오기
diff --git a/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift b/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift
index f2669f37..9bce9761 100644
--- a/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift
+++ b/project/Projects/Domain/UseCaseInterface/UserInfo/WorkerProfileUseCase.swift
@@ -15,7 +15,7 @@ import Entity
/// 4. 나의(요보) 프로필 정보 업데이트(이미지, pre-signed-url-callback)
/// 5. 특정 요양보호사의 프로필 불러오기
-public protocol WorkerProfileUseCase: UseCaseBase {
+public protocol WorkerProfileUseCase: BaseUseCase {
/// 1. 나의(요보) 프로필 정보 조회
/// 5. 특정 요양보호사의 프로필 불러오기
diff --git a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift
index 574354cf..8c9e1649 100644
--- a/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift
+++ b/project/Projects/Presentation/DSKit/Sources/CommonUI/Alert /IdleBigAlertController.swift
@@ -38,7 +38,8 @@ public class DefaultIdleAlertVM: IdleAlertViewModelable {
description: String,
acceptButtonLabelText: String,
cancelButtonLabelText: String,
- onAccepted: (() -> ())? = nil
+ onAccepted: (() -> ())? = nil,
+ onCanceled: (() -> ())? = nil
) {
self.title = title
self.description = description
@@ -50,7 +51,9 @@ public class DefaultIdleAlertVM: IdleAlertViewModelable {
acceptButtonClicked
.map({ _ in onAccepted?() })
.asObservable(),
- cancelButtonClicked.asObservable()
+ cancelButtonClicked
+ .map({ _ in onCanceled?() })
+ .asObservable()
)
.asDriver(onErrorDriveWith: .never())
}
diff --git a/project/Projects/Presentation/Feature/Base/Project.swift b/project/Projects/Presentation/Feature/Base/Project.swift
index c1d364b0..f0c7e4e2 100644
--- a/project/Projects/Presentation/Feature/Base/Project.swift
+++ b/project/Projects/Presentation/Feature/Base/Project.swift
@@ -39,6 +39,8 @@ let project = Project(
D.ThirdParty.RxSwift,
D.ThirdParty.RxCocoa,
D.ThirdParty.NaverMapSDKForSPM,
+ D.ThirdParty.FirebaseCrashlytics,
+ D.ThirdParty.FirebaseAnalytics,
],
settings: .settings(
configurations: IdleConfiguration.presentationConfigurations
diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift
index a5d44281..bb0b00ac 100644
--- a/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift
+++ b/project/Projects/Presentation/Feature/Base/Sources/View/View/RecruitmentPost/Worker/Detail/ViewModel/NativePostDetailForWorkerVM.swift
@@ -191,9 +191,9 @@ public class NativePostDetailForWorkerVM: BaseViewModel ,NativePostDetailForWork
title: "공고에 지원하시겠어요?",
description: "",
acceptButtonLabelText: "지원하기",
- cancelButtonLabelText: "취소하기") {
+ cancelButtonLabelText: "취소하기", onCanceled: {
applyRequest.accept(())
- }
+ })
}
.asDriver(onErrorDriveWith: .never())
diff --git a/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift
index da7d3c69..03eb8e8b 100644
--- a/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift
+++ b/project/Projects/Presentation/Feature/Base/Sources/View/ViewController/Base/BaseViewController.swift
@@ -91,7 +91,7 @@ public extension BaseViewController {
message: vo.message,
preferredStyle: .alert
)
- let close = UIAlertAction(title: "닫기", style: .default) { [weak self] _ in
+ let close = UIAlertAction(title: vo.dismissButtonLabelText, style: .default) { [weak self] _ in
self?.alert(vo: vo)
// dismiss
diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVC.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVC.swift
index 9f1d62de..98ade75d 100644
--- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVC.swift
+++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVC.swift
@@ -8,11 +8,12 @@
import UIKit
import BaseFeature
import PresentationCore
-import RxCocoa
-import RxSwift
import Entity
import DSKit
+import RxCocoa
+import RxSwift
+
public class InitialScreenVC: BaseViewController {
// Init
@@ -44,8 +45,8 @@ public class InitialScreenVC: BaseViewController {
super.bind(viewModel: viewModel)
- self.rx.viewWillAppear
- .map { _ in () }
+ self.rx.viewDidAppear
+ .map({ _ in })
.bind(to: viewModel.viewWillAppear)
.disposed(by: disposeBag)
}
diff --git a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift
index 3daad6b3..a98b924e 100644
--- a/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift
+++ b/project/Projects/Presentation/Feature/Root/Sources/Screen/Common/InitialScreen/InitialScreenVM.swift
@@ -5,14 +5,20 @@
// Created by choijunios on 8/25/24.
//
-import RxSwift
-import RxCocoa
-import Foundation
+import UIKit
+import Network
+
import PresentationCore
import UseCaseInterface
import RepositoryInterface
import BaseFeature
import Entity
+import DSKit
+
+import RxSwift
+import RxCocoa
+import FirebaseCrashlytics
+
public class InitialScreenVM: BaseViewModel {
@@ -26,13 +32,20 @@ public class InitialScreenVM: BaseViewModel {
let workerProfileUseCase: WorkerProfileUseCase
let centerProfileUseCase: CenterProfileUseCase
let userInfoLocalRepository: UserInfoLocalRepository
+ let remoteConfigRepository: RemoteConfigRepository
+
+ // network monitoring
+ private let networkMonitor: NWPathMonitor = .init()
+ private let networkMonitoringQueue = DispatchQueue.global(qos: .background)
+ private let networtIsAvailablePublisher: PublishSubject = .init()
public init(
coordinator: RootCoorinatable?,
authUseCase: AuthUseCase,
workerProfileUseCase: WorkerProfileUseCase,
centerProfileUseCase: CenterProfileUseCase,
- userInfoLocalRepository: UserInfoLocalRepository
+ userInfoLocalRepository: UserInfoLocalRepository,
+ remoteConfigRepository: RemoteConfigRepository
)
{
self.coordinator = coordinator
@@ -40,22 +53,127 @@ public class InitialScreenVM: BaseViewModel {
self.workerProfileUseCase = workerProfileUseCase
self.centerProfileUseCase = centerProfileUseCase
self.userInfoLocalRepository = userInfoLocalRepository
+ self.remoteConfigRepository = remoteConfigRepository
super.init()
- // MARK: 로그아웃, 회원탈퇴시
- NotificationCenter.default.rx.notification(.popToInitialVC)
- .observe(on: MainScheduler.instance)
+ // MARK: 네트워크 모니터링 시작
+ let networkConnected: ReplaySubject = .create(bufferSize: 1)
+
+ // 최초 1회 네트워크 연결이벤트 전송
+ networtIsAvailablePublisher
+ .filter { $0 }
+ .take(1)
+ .map { _ in }
+ .bind(to: networkConnected)
+ .disposed(by: disposeBag)
+
+ // 네트워크가 연결되지 않은 경우 재시도 하며, 재시도 실패시 같은 플로우 반복
+ networtIsAvailablePublisher.filter { !$0 }
.subscribe(onNext: { [weak self] _ in
- guard let self else { return }
+ let alertVO = DefaultAlertContentVO(
+ title: "인터넷 연결이 불안정해요",
+ message: "Wi-Fi 또는 셀룰러 데이터 연결을 확인한 후 다시 시도해 주세요.",
+ dismissButtonLabelText: "다시 시도하기") { [weak self] in
+
+ DispatchQueue.main.asyncAfter(deadline: .now()+1) { [weak self] in
+ guard let self else { return }
+
+ if self.networkMonitor.currentPath.status == .unsatisfied {
+
+ self.networtIsAvailablePublisher.onNext(false)
+ }
+ }
+ }
- self.coordinator?.popToRoot()
+ // 네트워크 연결되지 않음
+ self?.alert.onNext(alertVO)
})
.disposed(by: disposeBag)
+ startNeworkMonitoring()
+
+
+ // MARK: 강제업데이트 확인
+ // 네트워크 확인 -> 강제업데이트 확인
+ let needsForceUpdate = networkConnected
+ .flatMap { [remoteConfigRepository] _ in
+ remoteConfigRepository.fetchRemoteConfig()
+ }
+ .compactMap { $0.value }
+ .map { [remoteConfigRepository] isConfigFetched in
+
+ if !isConfigFetched {
+ Crashlytics.crashlytics().log("Remote Config fetch실패")
+ }
+
+ guard let config = remoteConfigRepository.getForceUpdateInfo() else {
+ // ‼️ Config로딩 불가시 크래쉬
+ Crashlytics.crashlytics().log("Remote Config획득 실패")
+ fatalError("Remote Config fetching에러")
+ }
+
+ return config
+ }
+ .map { info in
+
+ let minAppVersion = info.minVersion
+
+ let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
+
+ printIfDebug("앱 버전: \(appVersion) 최소앱버전: \(minAppVersion)")
+
+ return minAppVersion > appVersion
+ }
+ .share()
+
+ // 강제업데이트 필요
+ needsForceUpdate
+ .filter { $0 }
+ .subscribe(onNext: {
+ [weak self] _ in
+
+ guard let self else { return }
+
+ // 네트워크 연결되지 않음
+ let object = IdleAlertObject()
+ .setTitle("최신 버전의 앱이 있어요")
+ .setDescription("유저분들의 의견을 반영해 앱을 더 발전시켰어요.\n보다 좋은 서비스를 만나기 위해, 업데이트해주세요.")
+ .setAcceptButtonLabelText("앱 종료")
+ .setCancelButtonLabelText("앱 업데이트")
+
+ object
+ .cancelButtonClicked
+ .subscribe(onNext: { [weak self] in
+ self?.openAppStoreForUpdate()
+ })
+ .disposed(by: disposeBag)
+
+ object
+ .acceptButtonClicked
+ .subscribe(onNext: {
+ exit(0)
+ })
+ .disposed(by: disposeBag)
+
+ alertObject.onNext(object)
+ })
+ .disposed(by: disposeBag)
- viewWillAppear
+ // 강제업데이트 필요하지 않음
+ let forceUpdateChecked = needsForceUpdate.filter { !$0 }
+
+ // MARK: 유저별 플로우 시작
+ // 네트워크 연결확인 -> 강제업데이트 확인 -> 유저별 플로우 시작
+ Observable
+ .combineLatest(
+ // 강제업데이트 확인 완료
+ forceUpdateChecked,
+
+ // viewWillAppear
+ viewWillAppear
+ )
.subscribe(onNext: { [weak self] _ in
guard let self else { exit(0) }
@@ -76,6 +194,18 @@ public class InitialScreenVM: BaseViewModel {
})
.disposed(by: disposeBag)
+
+
+ // MARK: 로그아웃, 회원탈퇴시
+ NotificationCenter.default.rx.notification(.popToInitialVC)
+ .observe(on: MainScheduler.instance)
+ .subscribe(onNext: { [weak self] _ in
+
+ guard let self else { return }
+
+ self.coordinator?.popToRoot()
+ })
+ .disposed(by: disposeBag)
}
func workerInitialFlow() {
@@ -127,6 +257,37 @@ public class InitialScreenVM: BaseViewModel {
.disposed(by: disposeBag)
}
+ deinit {
+ networkMonitor.cancel()
+ }
+
+ func startNeworkMonitoring() {
+
+ networkMonitor.pathUpdateHandler = { [weak self] path in
+
+ DispatchQueue.main.async {
+ self?.checkNetworkStatusAndPublish(status: path.status, delay: 0)
+ }
+ }
+
+ networkMonitor.start(queue: networkMonitoringQueue)
+ }
+
+ func checkNetworkStatusAndPublish(status: NWPath.Status, delay: Int) {
+
+ switch status {
+ case .requiresConnection, .satisfied:
+ // requiresConnection는 일반적으로 즉시 연결이 가능한 상태
+ networtIsAvailablePublisher.onNext(true)
+ networtIsAvailablePublisher.onCompleted()
+ networkMonitor.cancel()
+ return
+ default:
+ networtIsAvailablePublisher.onNext(false)
+ return
+ }
+ }
+
func centerInitialFlow() {
// #1. 센터 상태를 확인함과 동시에 토큰 유효성 확인
@@ -144,7 +305,7 @@ public class InitialScreenVM: BaseViewModel {
guard let self else { return }
switch error {
- case .tokenExpiredException:
+ case .tokenExpiredException, .tokenNotFound:
// 토큰이 만료된 경우
coordinator?.auth()
default:
@@ -227,5 +388,18 @@ public class InitialScreenVM: BaseViewModel {
})
.disposed(by: disposeBag)
}
-
+
+ func checkForceUpdate() {
+
+
+ }
+
+ /// 앱스토에에서 해당앱을 엽니다.
+ func openAppStoreForUpdate() {
+ let url = "itms-apps://itunes.apple.com/app/6670529341";
+ if let url = URL(string: url), UIApplication.shared.canOpenURL(url) {
+ UIApplication.shared.open(url, options: [:])
+ }
+ }
}
+
diff --git a/project/Tuist/Package.swift b/project/Tuist/Package.swift
index c57a0c65..0b515062 100644
--- a/project/Tuist/Package.swift
+++ b/project/Tuist/Package.swift
@@ -41,6 +41,8 @@ let package = Package(
// Naver map
.package(url: "https://github.com/J0onYEong/NaverMapSDKForSPM.git", from: "1.0.0"),
// KingFisher
- .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.12.0")
+ .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.12.0"),
+ // Firebase
+ .package(url: "https://github.com/firebase/firebase-ios-sdk.git", from: "11.2.0"),
]
)