Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions ByeBoo-iOS/ByeBoo-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
183EE0E12ED1D10600154F40 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 183EE0E02ED1D10600154F40 /* FirebaseMessaging */; };
186847F02ED5567D002827DA /* FirebaseCore in Frameworks */ = {isa = PBXBuildFile; productRef = 186847EF2ED5567D002827DA /* FirebaseCore */; };
18A8172D2E154F3D00F68F9F /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 18A8172C2E154F3D00F68F9F /* Alamofire */; };
514D021B2FF6ADDD0041A4E1 /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 514D021A2FF6ADDD0041A4E1 /* FirebaseRemoteConfig */; };
515C127F2E159D45001BA2E0 /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 515C127E2E159D45001BA2E0 /* Then */; };
515C12822E159D77001BA2E0 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 515C12812E159D77001BA2E0 /* SnapKit */; };
51691DED2E5CB059008CCEC6 /* KakaoSDKAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 51691DEC2E5CB059008CCEC6 /* KakaoSDKAuth */; };
Expand Down Expand Up @@ -77,6 +78,7 @@
CBB032A72E7925FC00C0855E /* Mixpanel in Frameworks */,
51691DED2E5CB059008CCEC6 /* KakaoSDKAuth in Frameworks */,
CB71A8A32F3B6F9400176B64 /* FirebaseCrashlytics in Frameworks */,
514D021B2FF6ADDD0041A4E1 /* FirebaseRemoteConfig in Frameworks */,
183EE0E12ED1D10600154F40 /* FirebaseMessaging in Frameworks */,
183EE0DB2ED1CB0E00154F40 /* FirebaseAnalytics in Frameworks */,
);
Expand Down Expand Up @@ -151,6 +153,7 @@
183EE0E02ED1D10600154F40 /* FirebaseMessaging */,
186847EF2ED5567D002827DA /* FirebaseCore */,
CB71A8A22F3B6F9400176B64 /* FirebaseCrashlytics */,
514D021A2FF6ADDD0041A4E1 /* FirebaseRemoteConfig */,
);
productName = "ByeBoo-iOS";
productReference = CB41808B2E0925D2001E7BCB /* ByeBoo-iOS.app */;
Expand Down Expand Up @@ -671,6 +674,11 @@
package = 18A8172B2E154F3D00F68F9F /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
514D021A2FF6ADDD0041A4E1 /* FirebaseRemoteConfig */ = {
isa = XCSwiftPackageProductDependency;
package = 183EE0D92ED1CB0E00154F40 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */;
productName = FirebaseRemoteConfig;
};
515C127E2E159D45001BA2E0 /* Then */ = {
isa = XCSwiftPackageProductDependency;
package = 515C127D2E159D45001BA2E0 /* XCRemoteSwiftPackageReference "Then" */;
Expand Down
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 DefaultForceUpdateRepository: ForceUpdateInterface {
let remoteConfig = RemoteConfig.remoteConfig()

init() {
let settings = RemoteConfigSettings()
settings.minimumFetchInterval = 0
RemoteConfig.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
Expand Up @@ -255,5 +255,9 @@ struct DomainDependencyAssembler: DependencyAssembler {
DIContainer.shared.register(type: ReadAllNotificationsUseCase.self) { _ in
return DefaultReadAllNotificationsUseCase(repository: notificationRepository)
}

DIContainer.shared.register(type: CheckForceUpdateUseCase.self) { _ in
return DefaultCheckForceUpdateUsecase(repository: DefaultForceUpdateRepository())
}
}
}
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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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)
}
}
}

11 changes: 11 additions & 0 deletions ByeBoo-iOS/ByeBoo-iOS/Presentation/Enum/AppConstants.swift
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
Expand Up @@ -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
)
Comment thread
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 {
Expand All @@ -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:
self.viewModel.action(.tryAutoLogin)
}
}
.store(in: &cancellables)
}

private func bindAutoLogin() {
viewModel.output.autoLoginPublisher
.receive(on: DispatchQueue.main)
.sink { result in
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.swift

Repository: 36-APPJAM-HEARTZ/BYEBOO-iOS

Length of output: 5627


배경 탭으로 닫히지 않게 막아야 합니다
ForceUpdateModalView는 닫기 버튼이 없더라도, CustomModalController가 모달 바깥 탭에서 dismiss(animated: false)를 호출합니다. 강제 업데이트는 업데이트 버튼 외에는 종료되지 않도록 배경 탭 dismiss를 비활성화해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@ByeBoo-iOS/ByeBoo-iOS/Presentation/Feature/Login/ViewController/SplashViewController.swift`
around lines 105 - 119, `presentForceUpdateModal()` currently presents
`ForceUpdateModalView` through `ModalBuilder`, but the underlying
`CustomModalController` still dismisses on background taps. Update the modal
presentation path so the force-update flow cannot be closed by tapping outside
the modal, keeping only the update action from `openAppstore()` available; use
the `ModalBuilder`/`CustomModalController` configuration or presentation flags
associated with `ForceUpdateModalView` to disable outside-tap dismiss.

@objc
private func appDidBecomeActive() {
guard !isFirstLaunch else {
isFirstLaunch = false
return
}
viewModel.action(.viewDidLoad)
}
}

extension SplashViewController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,67 @@ 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
}
}


extension SplashViewModel {
enum Input {
case viewDidLoad
case tryAutoLogin
}

struct Output {
let autoLoginPublisher: AnyPublisher<Result<Void, ByeBooError>, Never>
let forceUpdatePublisher: AnyPublisher<Bool, Never>
}

func action(_ trigger: Input) {
switch trigger {
case .viewDidLoad:
checkForceUpdate()
case .tryAutoLogin:
autoLogin()
}
}

}

extension SplashViewModel {
private func checkForceUpdate() {
Task {
do {
if await checkForceUpdateUseCase.execute() {
forceUpdateSubject.send(true)
ByeBooLogger.debug("강제 업데이트 필요")
} else {
forceUpdateSubject.send(false)
}
}
}
}

private func autoLogin() {
ByeBooLogger.debug("자동로그인 실행")
Task {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,15 @@ struct PresentationDependencyAssembler: DependencyAssembler {
}

DIContainer.shared.register(type: SplashViewModel.self) { container in
guard let autoLoginUseCase = container.resolve(
type: AutoLoginUseCase.self
)
else {
guard let autoLoginUseCase = container.resolve(type: AutoLoginUseCase.self),
let checkForceUpdateUseCase = container.resolve(type: CheckForceUpdateUseCase.self) else {
ByeBooLogger.error(ByeBooError.DIFailedError)
return
}
return SplashViewModel(autoLoginUseCase: autoLoginUseCase)
return SplashViewModel(
autoLoginUseCase: autoLoginUseCase,
checkForceUpdateUseCase: checkForceUpdateUseCase
)
}

DIContainer.shared.register(type: FinishJourneyViewModel.self) { container in
Expand Down
Loading