Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5b43749
[Refact] NetworkService async 메서드 추가
gnksbm Jun 24, 2025
186cc8c
[Refact] CoreData 모듈 Swift Concurrency 객체 추가
gnksbm Jun 24, 2025
1cee3cd
[Fix] Swift Concurrency로 전환 중(임시 커밋)
gnksbm Jun 24, 2025
0fee4a0
Merge branch 'dev' into fix/#330
gnksbm Jun 25, 2025
5fd12fa
fix: LegacyCoreDataService 폴더 이동
gnksbm Jun 26, 2025
b6d111b
fix: fetchRequiredVersion async 메서드 추가
gnksbm Jun 26, 2025
58133f9
fix: CoreData read 정렬 로직 제거
gnksbm Jun 29, 2025
2595f73
Merge branch 'dev' into fix/#330
gnksbm Jun 29, 2025
2e7c060
fix: Splash를 통해 DI 수행 및 버전체크하도록, 버전체크 UseCase 리팩토링
gnksbm Jul 5, 2025
498bf82
fix: 버전체크 에러 핸들링 로직 수정
gnksbm Jul 7, 2025
97bc5e2
feat: InfoPlistWrapper 추가
gnksbm Jul 7, 2025
4507880
fix: 린트 룰 변경 및 경고 수정
gnksbm Jul 7, 2025
a95a161
fix: 강업처리 에러시 로그 추가
gnksbm Jul 7, 2025
3401116
fix: deprecated 어트리뷰트 추가
gnksbm Jul 7, 2025
13d5fb4
fix: Dictionary의 중복값 관련 런타임 크래시 수정
gnksbm Jul 7, 2025
60364fc
fix: deprecated 추가, 코드정리
gnksbm Jul 7, 2025
41590cf
fix: AppVersionInfoResponse 크래시 방지를 위한 조건문 추가
gnksbm Jul 12, 2025
b724255
fix: CoreData 모델 매핑시 런타임 크래시 발생하지 않도록 수정
gnksbm Jul 28, 2025
c806a5e
fix: AppVersionCheckUseCase 네이밍 개선
gnksbm Jul 28, 2025
85f0e0f
fix: 불필요한 Rx 의존성 제거
gnksbm Jul 28, 2025
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
5 changes: 4 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ identifier_name:
- vc
- vm
- o
- v
- id
allowed_symbols: "_" # 언더바 허용
function_body_length:
warning: 150
error: 300
Expand All @@ -30,7 +33,7 @@ file_length:
warning: 1000
error: 2000
line_length:
warning: 90
warning: 120
error: 400
disabled_rules: # 제외하고 싶은 룰
- trailing_whitespace
Expand Down
45 changes: 0 additions & 45 deletions Projects/App/Sources/AppDelegate+Register.swift

This file was deleted.

1 change: 0 additions & 1 deletion Projects/App/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
setupAppearance()
registerDependencies()
configureNotification(application: application)
configureFirebase(application: application)

Expand Down
34 changes: 27 additions & 7 deletions Projects/App/Sources/Coordinator/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,41 @@
import UIKit

import FeatureDependency
import MainFeature
import BusStopFeature
import Domain

protocol AppCoordinatorDependency: AnyObject, SplashViewModelDependency {
var sceneWillEnterForeground: AsyncStream<UIScene> { get }
var appVersion: AppVersionInfoResponse { get }
var appStoreID: String { get }
var domainURL: String { get }
}

final class AppCoordinator: Coordinator {
var parent: Coordinator?
var childs: [Coordinator] = []
var navigationController: UINavigationController
public var coordinatorType: CoordinatorType = .app
private let coordinatorProvider = DefaultCoordinatorProvider()
private let dependency: AppCoordinatorDependency

init(navigationController: UINavigationController) {
init(
navigationController: UINavigationController,
dependency: AppCoordinatorDependency
) {
self.navigationController = navigationController
self.dependency = dependency
}

func start() {
let tabBarCoordinator = TabBarCoordinator(
navigationController: navigationController,
coordinatorProvider: coordinatorProvider
let splashCoordinator = SplashCoordinatorImpl(
parent: self,
navigationController: navigationController,
coordinatorProvider: coordinatorProvider,
viewModelDependency: dependency
)
childs.append(tabBarCoordinator)
tabBarCoordinator.start()
childs.append(splashCoordinator)
splashCoordinator.start()
}

func startBusStopFlow(busStopId: String) {
Expand All @@ -43,4 +57,10 @@ final class AppCoordinator: Coordinator {
childs.append(busStopCoordinator)
busStopCoordinator.start()
}

func openURL(_ url: URL) {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
}
54 changes: 54 additions & 0 deletions Projects/App/Sources/Coordinator/Splash/SplashCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// SplashCoordinator.swift
// App
//
// Created by gnksbm on 6/30/25.
// Copyright © 2025 Pepsi-Club. All rights reserved.
//

import UIKit

import MainFeature
import FeatureDependency

protocol SplashCoordinator: Coordinator {
func startTabFlow()
func openURL(_ url: URL)
}

final class SplashCoordinatorImpl: SplashCoordinator {
Copy link
Contributor

Choose a reason for hiding this comment

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

구조체 이름 앞에 Default를 안쓰고 뒤에 Impl를 붙이는 걸로 컨벤션이 바뀌는 걸까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

구현체라는 표현을 목적으로 네이밍을 수정해봤는데 어떤지 의견 부탁드려요! @isakatty

var parent: Coordinator?
var childs: [Coordinator] = []
var navigationController: UINavigationController
public var coordinatorType: CoordinatorType = .splash
private let coordinatorProvider: CoordinatorProvider
private let viewModelDependency: SplashViewModelDependency

init(
parent: Coordinator,
navigationController: UINavigationController,
coordinatorProvider: CoordinatorProvider,
viewModelDependency: SplashViewModelDependency
) {
self.parent = parent
self.navigationController = navigationController
self.coordinatorProvider = coordinatorProvider
self.viewModelDependency = viewModelDependency
}

func start() {
let splashViewController = SplashViewController(
viewModel: SplashViewModel(coordinator: self, dependency: viewModelDependency)
)
navigationController.setViewControllers([splashViewController], animated: false)
}

func startTabFlow() {
let tabBarCoordinator = TabBarCoordinator(
navigationController: navigationController,
coordinatorProvider: coordinatorProvider
)
childs.append(tabBarCoordinator)
tabBarCoordinator.start()
}
}
61 changes: 61 additions & 0 deletions Projects/App/Sources/Coordinator/Splash/SplashViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// SplashViewController.swift
// App
//
// Created by gnksbm on 6/30/25.
// Copyright © 2025 Pepsi-Club. All rights reserved.
//

import UIKit
import DesignSystem

import RxSwift
import RxCocoa

final class SplashViewController: UIViewController {
private let viewModel: SplashViewModel

private let disposeBag: DisposeBag = .init()

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() {
super.viewDidLoad()

view.backgroundColor = DesignSystemAsset.changeBlue.color

let output = viewModel.transform(
input: .init(
viewDidLoad: .just(())
)
)

output.alert
.observe(on: MainScheduler.instance)
.bind(with: self) { owner, alert in
let alertController = UIAlertController(
title: alert.title,
message: alert.message,
preferredStyle: .alert
)
alert.actions.forEach { alertAction in
let alertAction = UIAlertAction(
title: alertAction.title,
style: .default
) { _ in
alertAction.handler()
}
alertController.addAction(alertAction)
}
owner.present(alertController, animated: true)
}
.disposed(by: disposeBag)
}
}
123 changes: 123 additions & 0 deletions Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// SplashViewModel.swift
// App
//
// Created by gnksbm on 6/30/25.
// Copyright © 2025 Pepsi-Club. All rights reserved.
//

import FeatureDependency
import NetworkService
import CoreDataService
import Data
import Domain
import Core
import FirebaseModule

import RxSwift
import RxRelay

protocol SplashViewModelDependency {
var appVersion: AppVersionInfoResponse { get }
var appStoreID: String { get }
var domainURL: String { get }
}

final class SplashViewModel: ViewModel {
private weak var coordinator: SplashCoordinator?
@Injected private var versionCheckUseCase: AppVersionCheckUseCase
Copy link
Contributor

Choose a reason for hiding this comment

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

왜 SplashViewModel에서 @injected 를 사용하는게 안 좋은 형태라고 생각하셨는지 궁금합니다.

@Injected private var firebaseLogger: FirebaseLogger
private let dependency: SplashViewModelDependency

init(
coordinator: SplashCoordinator,
dependency: SplashViewModelDependency
) {
self.coordinator = coordinator
self.dependency = dependency
}

func transform(input: Input) -> Output {
let alertRelay = PublishRelay<Alert>()
Task {
try await input.viewDidLoad.value
Copy link
Contributor

Choose a reason for hiding this comment

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

transform 함수 자체가 viewDidLoad될 때 호출하게 되는데, input으로 시퀀스를 받아서 한번 더 호출하는 이유가 있을까요?

await registerDependency()
do {
let forceUpdate = try await versionCheckUseCase.checkForceUpdateNeeded()
switch forceUpdate {
case .notNeeded:
await MainActor.run {
coordinator?.startTabFlow()
}
case .needed(let appStoreURL):
let alert = Alert(
title: "업데이트 알림",
message: "더 나은 서비스를 위해 업데이트 되었어요 ! 업데이트 해주세요."
) {
AlertAction(title: "업데이트") { [weak self] in
self?.coordinator?.openURL(appStoreURL)
}
}
alertRelay.accept(alert)
}
} catch {
firebaseLogger.log(name: "강제 업데이트 실패: \(error.localizedDescription)")
await MainActor.run {
coordinator?.startTabFlow()
}
}
}
return .init(alert: alertRelay.asObservable())
}

private func registerDependency() async {
let coreDataContainer = await CoreDataContainerBuilder().buildContainer()
Comment on lines +73 to +74
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Core Data 초기화 에러 처리 필요

CoreDataContainerBuilder가 에러를 throw하도록 수정되면 여기서도 처리가 필요합니다.

private func registerDependency() async {
-    let coreDataContainer = await CoreDataContainerBuilder().buildContainer()
+    do {
+        let coreDataContainer = try await CoreDataContainerBuilder().buildContainer()
+        // 나머지 의존성 등록...
+    } catch {
+        DIContainer.firebaseLogger?.log("Core Data 초기화 실패: \(error)")
+        // 치명적 에러이므로 사용자에게 알림 필요
+        throw error
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func registerDependency() async {
let coreDataContainer = await CoreDataContainerBuilder().buildContainer()
private func registerDependency() async {
do {
let coreDataContainer = try await CoreDataContainerBuilder().buildContainer()
// 나머지 의존성 등록...
} catch {
DIContainer.firebaseLogger?.log("Core Data 초기화 실패: \(error)")
// 치명적 에러이므로 사용자에게 알림 필요
throw error
}
🤖 Prompt for AI Agents
In Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift around lines 72
to 73, the call to CoreDataContainerBuilder().buildContainer() needs error
handling because buildContainer() may throw an error. Update the
registerDependency() function to use try/await and handle the potential error
with do-catch, ensuring any thrown errors are caught and managed appropriately.

let firebaseLogger = FirebaseLoggerImpl()

DIContainer.setLogger(firebaseLogger)

DIContainer.register(type: CoreDataStorage.self, CoreDataStorageImpl(container: coreDataContainer))
DIContainer.register(type: CoreDataService.self, DefaultCoreDataService())
DIContainer.register(type: NetworkService.self, DefaultNetworkService())
DIContainer.register(type: LocationService.self, DefaultLocationService())

DIContainer.register(type: AsyncFavoritesRepository.self, AsyncFavoritesRepositoryImpl())
DIContainer.register(type: FavoritesRepository.self, DefaultFavoritesRepository())
DIContainer.register(type: BusStopArrivalInfoRepository.self, DefaultBusStopArrivalInfoRepository())
DIContainer.register(type: StationListRepository.self, DefaultStationListRepository())
DIContainer.register(type: RegularAlarmRepository.self, DefaultRegularAlarmRepository())
DIContainer.register(type: LocalNotificationService.self, DefaultLocalNotificationService())
DIContainer.register(type: RegularAlarmEditingService.self, DefaultRegularAlarmEditingService())
DIContainer.register(
type: VersionCheckRepository.self,
VersionCheckRepositoryImpl(
appStoreID: dependency.appStoreID,
domainURL: dependency.domainURL
)
)

DIContainer.register(type: FavoritesUseCase.self, DefaultFavoritesUseCase())
DIContainer.register(type: RegularAlarmUseCase.self, DefaultRegularAlarmUseCase())
DIContainer.register(type: AddRegularAlarmUseCase.self, DefaultAddRegularAlarmUseCase())
DIContainer.register(type: SearchUseCase.self, DefaultSearchUseCase())
DIContainer.register(type: BusStopUseCase.self, DefaultBusStopUseCase())
DIContainer.register(type: NearMapUseCase.self, DefaultNearMapUseCase())
DIContainer.register(type: FirebaseLogger.self, firebaseLogger)
DIContainer.register(
type: AppVersionCheckUseCase.self,
VersionCheckUseCaseImpl(
currentVersion: dependency.appVersion
)
)
}
}

extension SplashViewModel {
struct Input {
let viewDidLoad: Single<Void>
}

struct Output {
let alert: Observable<Alert>
}
}
Loading
Loading