-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#470 강제 업데이트 구현 #471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "feat/#470-\uAC15\uC81C-\uC5C5\uB370\uC774\uD2B8-\uAD6C\uD604"
Feat/#470 강제 업데이트 구현 #471
Changes from all commits
f5e6bdd
858b273
fdf49ae
cbf93c6
ab077e2
032a201
716c996
bfe9b52
999c2ee
c7a6a8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| // | ||
| // RemoteConfigeManager.swift | ||
| // ByeBoo-iOS | ||
| // | ||
| // Created by 이나연 on 7/2/26. | ||
| // | ||
|
|
||
| import FirebaseRemoteConfig | ||
|
|
||
| final class DefaultForceUpdateService: ForceUpdateInterface { | ||
| let remoteConfig = RemoteConfig.remoteConfig() | ||
|
|
||
| init() { | ||
| let settings = RemoteConfigSettings() | ||
| settings.minimumFetchInterval = 0 | ||
| remoteConfig.configSettings = settings | ||
| } | ||
|
|
||
| func checkForUpdate() async -> Bool { | ||
| do { | ||
| try await remoteConfig.fetch() | ||
| try await remoteConfig.activate() | ||
|
|
||
| let minimumVersion = remoteConfig["min_version_code"].stringValue | ||
|
|
||
| return isUpdateRequired(minimumVersion: minimumVersion) | ||
| } catch { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| private var currentAppVersion: String { | ||
| Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" | ||
| } | ||
|
|
||
| private func isUpdateRequired(minimumVersion: String) -> Bool { | ||
| ByeBooLogger.debug("현재버전: \(currentAppVersion), 최소버전: \(minimumVersion)") | ||
| return currentAppVersion.compare(minimumVersion, options: .numeric) == .orderedAscending | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| // | ||
| // ForeceUpdateInterface.swift | ||
| // ByeBoo-iOS | ||
| // | ||
| // Created by 이나연 on 7/5/26. | ||
| // | ||
|
|
||
| protocol ForceUpdateInterface { | ||
| func checkForUpdate() async -> Bool | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // | ||
| // CheckForceUpdateUseCase.swift | ||
| // ByeBoo-iOS | ||
| // | ||
| // Created by 이나연 on 7/4/26. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| protocol CheckForceUpdateUseCase { | ||
| func execute() async -> Bool | ||
| } | ||
|
|
||
| struct DefaultCheckForceUpdateUseCase: CheckForceUpdateUseCase { | ||
|
|
||
| private let repository: ForceUpdateInterface | ||
|
|
||
| init(repository: ForceUpdateInterface) { | ||
| self.repository = repository | ||
| } | ||
|
|
||
| func execute() async -> Bool { | ||
| return await repository.checkForUpdate() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| // | ||
| // ForceUpdateModalView.swift | ||
| // ByeBoo-iOS | ||
| // | ||
| // Created by 이나연 on 7/4/26. | ||
| // | ||
|
|
||
| import UIKit | ||
|
|
||
| import SnapKit | ||
| import Then | ||
|
|
||
| final class ForceUpdateModalView: BaseView, ModalProtocol { | ||
| let actionButton: ByeBooButton = ByeBooButton(titleText: "업데이트 하러 가기", type: .enabled) | ||
| let dismissButton: ByeBooButton? = nil | ||
| let modalType: ConfirmModalType? = nil | ||
|
|
||
| private let titleLabel = UILabel() | ||
| private let descriptionLabel = UILabel() | ||
|
|
||
| override func setUI() { | ||
| addSubviews(titleLabel, descriptionLabel, actionButton) | ||
| } | ||
|
|
||
| override func setStyle() { | ||
| backgroundColor = .grayscale900 | ||
| layer.cornerRadius = 12 | ||
|
|
||
| titleLabel.applyByeBooFont( | ||
| style: .sub3M18, | ||
| text: "새로운 업데이트가 있어요", | ||
| color: .grayscale50 | ||
| ) | ||
|
|
||
| descriptionLabel.applyByeBooFont( | ||
| style: .body3R16, | ||
| text: "더 나은 바이부를 만나보세요", | ||
| color: .grayscale400 | ||
| ) | ||
| } | ||
|
|
||
| override func setLayout() { | ||
| titleLabel.snp.makeConstraints { | ||
| $0.top.equalToSuperview().inset(24.adjustedH) | ||
| $0.centerX.equalToSuperview() | ||
| } | ||
| descriptionLabel.snp.makeConstraints { | ||
| $0.top.equalTo(titleLabel.snp.bottom).offset(16.adjustedH) | ||
| $0.centerX.equalToSuperview() | ||
| } | ||
| actionButton.snp.makeConstraints { | ||
| $0.top.equalTo(descriptionLabel.snp.bottom).offset(16.adjustedH) | ||
| $0.centerX.equalToSuperview() | ||
| $0.horizontalEdges.equalToSuperview().inset(24.adjustedW) | ||
| $0.bottom.equalToSuperview().inset(24.adjustedH) | ||
| $0.height.equalTo(53.adjustedH) | ||
| } | ||
| } | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // | ||
| // AppConstants.swift | ||
| // ByeBoo-iOS | ||
| // | ||
| // Created by 이나연 on 7/4/26. | ||
| // | ||
|
|
||
| enum AppConstants { | ||
| static let appStoreID = "6748205348" | ||
| static let appStoreURL = "itms-apps://itunes.apple.com/app/id\(appStoreID)" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,26 +9,33 @@ import Combine | |
| import UIKit | ||
|
|
||
| final class SplashViewController: BaseViewController { | ||
|
|
||
| private let rootView = SplashView() | ||
| private let viewModel: SplashViewModel | ||
| private var cancellables = Set<AnyCancellable>() | ||
|
|
||
| private var isFirstLaunch = true | ||
|
|
||
| init(viewModel: SplashViewModel) { | ||
| self.viewModel = viewModel | ||
| super.init(nibName: nil, bundle: nil) | ||
| } | ||
|
|
||
| required init?(coder: NSCoder) { | ||
| fatalError("init(coder:) has not been implemented") | ||
| } | ||
|
|
||
| override func viewDidLoad() { | ||
| view = rootView | ||
| viewModel.action(.viewDidLoad) | ||
| bind() | ||
| setAddTarget() | ||
|
|
||
| NotificationCenter.default.addObserver( | ||
| self, | ||
| selector: #selector(appDidBecomeActive), | ||
| name: UIApplication.didBecomeActiveNotification, | ||
| object: nil | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in | ||
| guard let self = self else { return } | ||
| if self.cancellables.isEmpty { | ||
|
|
@@ -39,8 +46,27 @@ final class SplashViewController: BaseViewController { | |
| } | ||
|
|
||
| extension SplashViewController { | ||
|
|
||
| private func bind() { | ||
| bindCheckForceUpdate() | ||
| bindAutoLogin() | ||
| } | ||
|
|
||
| private func bindCheckForceUpdate() { | ||
| viewModel.output.forceUpdatePublisher | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { [weak self] result in | ||
| guard let self else { return } | ||
| switch result { | ||
| case true: | ||
| self.presentForceUpdateModal() | ||
| case false: | ||
| ByeBooLogger.debug("강제업데이트 필요 없음") | ||
| } | ||
| } | ||
| .store(in: &cancellables) | ||
| } | ||
|
|
||
| private func bindAutoLogin() { | ||
| viewModel.output.autoLoginPublisher | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { result in | ||
|
|
@@ -77,6 +103,29 @@ extension SplashViewController { | |
| } | ||
| .store(in: &cancellables) | ||
| } | ||
|
|
||
| private func presentForceUpdateModal() { | ||
| let modal = ModalBuilder( | ||
| modalView: ForceUpdateModalView(), | ||
| action: openAppstore, | ||
| rootViewController: self | ||
| ) | ||
| modal.present() | ||
| } | ||
|
|
||
| private func openAppstore() { | ||
| guard let url = URL(string: AppConstants.appStoreURL) else { return } | ||
| UIApplication.shared.open(url) | ||
| } | ||
|
|
||
|
Comment on lines
+106
to
+120
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# ModalBuilder/ModalProtocol의 배경 탭/스와이프 dismiss 처리 여부 확인
rg -n -B3 -A20 'class ModalBuilder' ByeBoo-iOS/ByeBoo-iOS --type=swift
rg -n 'dismissButton|modalType|tapGesture|backgroundView' ByeBoo-iOS/ByeBoo-iOS --type=swift -g '*Modal*'Repository: 36-APPJAM-HEARTZ/BYEBOO-iOS Length of output: 4217 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== ModalProtocol ==\n'
sed -n '1,120p' ByeBoo-iOS/ByeBoo-iOS/Presentation/Protocol/ModalProtocol.swift
printf '\n== CustomModalController ==\n'
sed -n '1,180p' ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/CustomModalController.swift
printf '\n== ForceUpdateModalView ==\n'
sed -n '1,120p' ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/ForceUpdateModalView.swift
printf '\n== ModalBuilder ==\n'
sed -n '1,160p' ByeBoo-iOS/ByeBoo-iOS/Presentation/Common/Modal/ModalBuilder.swiftRepository: 36-APPJAM-HEARTZ/BYEBOO-iOS Length of output: 5627 배경 탭으로 닫히지 않게 막아야 합니다 🤖 Prompt for AI Agents |
||
| @objc | ||
| private func appDidBecomeActive() { | ||
| guard !isFirstLaunch else { | ||
| isFirstLaunch = false | ||
| return | ||
| } | ||
| viewModel.action(.viewDidLoad) | ||
| } | ||
| } | ||
|
Comment on lines
+121
to
129
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오 요거 viewDidLoad가 아니라 appDidBecome에 Notification 추가해서 처리해준 이유가 있나요?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 앱스토어를 다녀와서 진입했지만 아직 업데이트를 하지 않은 상태를 처리하기 위해서입니다 !! |
||
|
|
||
| extension SplashViewController { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,22 +9,27 @@ import Combine | |
| import Foundation | ||
|
|
||
| final class SplashViewModel { | ||
|
|
||
| private var autoLoginSubject: PassthroughSubject<Result<Void, ByeBooError>, Never> = .init() | ||
|
|
||
| private var forceUpdateSubject: PassthroughSubject<Bool, Never> = .init() | ||
|
|
||
| var output: Output { | ||
| Output( | ||
| autoLoginPublisher: autoLoginSubject.eraseToAnyPublisher() | ||
| autoLoginPublisher: autoLoginSubject.eraseToAnyPublisher(), | ||
| forceUpdatePublisher: forceUpdateSubject.eraseToAnyPublisher() | ||
| ) | ||
| } | ||
|
|
||
| private var cancellables = Set<AnyCancellable>() | ||
| private let autoLoginUseCase: AutoLoginUseCase | ||
|
|
||
| private let checkForceUpdateUseCase: CheckForceUpdateUseCase | ||
|
|
||
| init( | ||
| autoLoginUseCase: AutoLoginUseCase | ||
| autoLoginUseCase: AutoLoginUseCase, | ||
| checkForceUpdateUseCase: CheckForceUpdateUseCase | ||
| ) { | ||
| self.autoLoginUseCase = autoLoginUseCase | ||
| self.checkForceUpdateUseCase = checkForceUpdateUseCase | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -36,18 +41,33 @@ extension SplashViewModel { | |
|
|
||
| struct Output { | ||
| let autoLoginPublisher: AnyPublisher<Result<Void, ByeBooError>, Never> | ||
| let forceUpdatePublisher: AnyPublisher<Bool, Never> | ||
| } | ||
|
|
||
| func action(_ trigger: Input) { | ||
| switch trigger { | ||
| case .viewDidLoad: | ||
| autoLogin() | ||
| checkForceUpdate() | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| extension SplashViewModel { | ||
| private func checkForceUpdate() { | ||
| Task { | ||
| do { | ||
| if await checkForceUpdateUseCase.execute() { | ||
| forceUpdateSubject.send(true) | ||
| ByeBooLogger.debug("강제 업데이트 필요") | ||
| } else { | ||
| forceUpdateSubject.send(false) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JW 요기서 바로 autoLogin하면 안되나요 ??
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 반영 완료했습니당 |
||
| autoLogin() | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func autoLogin() { | ||
| ByeBooLogger.debug("자동로그인 실행") | ||
| Task { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
init될 때 RemoteConfig.remoteConfig()가 2번 생성 될 것 같네용
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firebase가 싱글톤으로 관리한다고 해서 중복생성되지는 않는다고 합니다!! 이름만 다시 변경해서 수정했습니다 !!!!!