Skip to content
Merged
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 DefaultForceUpdateService: ForceUpdateInterface {
let remoteConfig = RemoteConfig.remoteConfig()

init() {
let settings = RemoteConfigSettings()
settings.minimumFetchInterval = 0
remoteConfig.configSettings = settings
}
Comment on lines +11 to +17

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init될 때 RemoteConfig.remoteConfig()가 2번 생성 될 것 같네용

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firebase가 싱글톤으로 관리한다고 해서 중복생성되지는 않는다고 합니다!! 이름만 다시 변경해서 수정했습니다 !!!!!


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: DefaultForceUpdateService())
}
}
}
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)
}
}
}

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:
ByeBooLogger.debug("강제업데이트 필요 없음")
}
}
.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)
}
}
Comment on lines +121 to 129

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 요거 viewDidLoad가 아니라 appDidBecome에 Notification 추가해서 처리해준 이유가 있나요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앱스토어를 다녀와서 진입했지만 아직 업데이트를 하지 않은 상태를 처리하기 위해서입니다 !!


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

Expand All @@ -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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JW 요기서 바로 autoLogin하면 안되나요 ??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영 완료했습니당

autoLogin()
}
}
}
}

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