Conversation
Walkthrough이번 변경에서는 NetworkService와 CoreDataService 모듈을 RxSwift 기반에서 Swift Concurrency(async/await) 기반으로 전환하였습니다. 이에 따라 관련 프로토콜, 구현체, 레포지토리, 유즈케이스, DTO, DI, 앱 진입 플로우, 버전 체크, Alert 구조 등이 대대적으로 개편되었습니다. RxSwift 기반 메서드는 deprecated 처리되었고, Info.plist 접근은 프로퍼티 래퍼로 대체되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant App as AppDelegate
participant Scene as SceneDelegate
participant SplashCoord as SplashCoordinatorImpl
participant SplashVM as SplashViewModel
participant DI as DIContainer
participant VersionCheck as VersionCheckUseCaseImpl
participant User as 사용자
App->>Scene: 앱 실행 (scene(_:willConnectTo:))
Scene->>SplashCoord: AppCoordinator 생성 및 SplashCoordinator 시작
SplashCoord->>SplashVM: SplashViewModel 생성 및 transform(input:)
SplashVM->>DI: registerDependency() 비동기 등록
SplashVM->>VersionCheck: checkForceUpdateNeeded() 호출
alt 업데이트 필요 없음
SplashVM->>SplashCoord: startTabFlow() 호출
else 강제 업데이트 필요
SplashVM->>User: Alert 출력 (업데이트 안내)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested reviewers
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 10
🔭 Outside diff range comments (1)
.swiftlint.yml (1)
14-23: identifier_name 규칙을 동시에 ‘설정’하고 ‘비활성화’하고 있습니다14–23행에서 세부 설정을 해두고 40행에서
disabled_rules에 다시 추가해 버리면, SwiftLint가 해당 설정을 완전히 무시하므로 설정 작성이 무의미해집니다.
둘 중 하나만 남기는 편이 명확합니다.-identifier_name: - min_length: - warning: 2 - error: 3 - excluded: # 제외할 문자열 목록 - - f - - vc - - vm - - o또는 40행의
- identifier_name항목을 삭제해 규칙을 활성화하세요. 팀 컨벤션에 맞는 방향으로 정리 부탁드립니다.Also applies to: 40-40
🧹 Nitpick comments (8)
.swiftlint.yml (1)
32-34: line_length 경고 임계값 120 → 120자 조정 확인 필요라인 길이를 120자로 완화하면 가독성·리뷰 품질이 떨어질 수 있습니다. 실제로 120자를 초과하는 코드가 자주 발생하는지, formatter가 자동 줄바꿈을 지원하는지 확인 후 결정하는 것이 좋습니다.
Projects/Core/Sources/Extension/TimeInterval+.swift (1)
12-12: 매개변수명을 더 명확하게 개선하는 것을 고려해보세요.현재
value매개변수명보다는hours가 더 명확할 것 같습니다.- static func hour(_ value: Int) -> Self { + static func hour(_ hours: Int) -> Self {Projects/Core/Sources/TypeBuilder.swift (1)
11-16: 더 완전한 결과 빌더를 위한 추가 메서드들을 고려해보세요.현재
buildBlock메서드만 구현되어 있지만, 더 유연한 사용을 위해 다른 빌드 메서드들도 추가할 수 있습니다.@resultBuilder public enum TypeBuilder<T> { public static func buildBlock(_ components: T...) -> [T] { components } + + public static func buildOptional(_ component: [T]?) -> [T] { + component ?? [] + } + + public static func buildEither(first component: [T]) -> [T] { + component + } + + public static func buildEither(second component: [T]) -> [T] { + component + } }Projects/NetworkService/Sources/NetworkService/NetworkService.swift (1)
14-16: 메서드 순서를 일관성 있게 정리하는 것을 고려해보세요.비동기 메서드를 다른 메서드들과 분리하여 프로토콜 가독성을 높일 수 있습니다.
public protocol NetworkService { + // MARK: - Async Methods func request(endPoint: EndPoint) async throws -> Data + // MARK: - Reactive Methods func request(endPoint: EndPoint) -> Observable<Data> func request<T: Decodable>( endPoint: EndPoint, responseType: T.Type ) -> Single<Result<T, Error>> }Projects/MainFeature/Sources/TabBarCoordinator.swift (1)
38-47: 비동기 패턴 개선을 고려해보세요.UI 업데이트를 메인 스레드에서 실행하는 것은 좋지만, fire-and-forget 태스크 패턴은 오류 처리가 어려울 수 있습니다.
더 안전한 접근 방식을 고려해보세요:
- private func setupTabBarController() { - Task { @MainActor in + @MainActor + private func setupTabBarController() { + Task { let tabBarController = TabBarViewController() navigationController.setViewControllers( [tabBarController], animated: true ) let viewControllers = MainTab.allCases.map { makeNavigationController(tabKind: $0) } tabBarController.viewControllers = viewControllers } }또는 호출하는 곳에서 async 컨텍스트를 관리하도록 메서드를 async로 만드는 것을 고려해보세요.
Projects/CoreDataService/Sources/CoreDataContainerBuilder.swift (1)
48-55: CloudKit 상태 확인 실패 시 로깅 추가 권장에러를 조용히 무시하면 CloudKit 관련 문제를 디버깅하기 어렵습니다.
에러 발생 시 로깅을 추가하는 것을 고려해보세요:
static var shouldUseCloudKit: Bool { get async { - let status = try? await CKContainer.default().accountStatus() - return status == .available + do { + let status = try await CKContainer.default().accountStatus() + return status == .available + } catch { + DIContainer.firebaseLogger?.log("CloudKit 계정 상태 확인 실패: \(error)") + return false + } } }Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (1)
13-13: 사용하지 않는 RxSwift import 제거async/await로 전환되어 더 이상 RxSwift를 사용하지 않습니다.
-import RxSwiftProjects/CoreDataService/Sources/CoreDataStorageImpl.swift (1)
100-111: 주석 처리된 API 설정의 용도를 확인해주세요.이 주석은 향후 버전 관리 기능을 위한 것으로 보입니다. 구현 계획이 있다면 TODO 주석으로 변경하거나, 불필요하다면 제거를 고려해주세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (41)
.swiftlint.yml(1 hunks)Projects/App/Sources/AppDelegate+Register.swift(0 hunks)Projects/App/Sources/AppDelegate.swift(0 hunks)Projects/App/Sources/Coordinator/AppCoordinator.swift(2 hunks)Projects/App/Sources/Coordinator/Splash/SplashCoordinator.swift(1 hunks)Projects/App/Sources/Coordinator/Splash/SplashViewController.swift(1 hunks)Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift(1 hunks)Projects/App/Sources/SceneDelegate.swift(3 hunks)Projects/Core/Sources/Extension/String+.swift(1 hunks)Projects/Core/Sources/Extension/TimeInterval+.swift(1 hunks)Projects/Core/Sources/PropertyWrapper/InfoPlistWrapper.swift(1 hunks)Projects/Core/Sources/PropertyWrapper/UserDefaultsWrapper.swift(1 hunks)Projects/Core/Sources/TypeBuilder.swift(1 hunks)Projects/CoreDataService/Sources/CoreDataContainerBuilder.swift(1 hunks)Projects/CoreDataService/Sources/CoreDataModel.swift(1 hunks)Projects/CoreDataService/Sources/CoreDataStorage.swift(1 hunks)Projects/CoreDataService/Sources/CoreDataStorageImpl.swift(1 hunks)Projects/Data/Sources/DTO/BusStopArrivalInfoDTO.swift(1 hunks)Projects/Data/Sources/DTO/CoreDataModelObject/FavoritesBusResponseMO+CoreDataClass.swift(2 hunks)Projects/Data/Sources/Repository/DefaultBusStopArrivalInfoRepository.swift(1 hunks)Projects/Data/Sources/Repository/DefaultFavoritesRepository.swift(1 hunks)Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift(1 hunks)Projects/Domain/Sources/Entity/Alert.swift(1 hunks)Projects/Domain/Sources/Entity/ForceUpdate.swift(1 hunks)Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift(3 hunks)Projects/Domain/Sources/Entity/Response/FavoritesBusResponse.swift(1 hunks)Projects/Domain/Sources/Entity/VersionCheckInfo.swift(1 hunks)Projects/Domain/Sources/RepositoryInterface/BusStopArrivalInfoRepository.swift(1 hunks)Projects/Domain/Sources/RepositoryInterface/FavoritesRepository.swift(1 hunks)Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift(1 hunks)Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift(1 hunks)Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift(1 hunks)Projects/Feature/SettingsFeature/Sources/View/SettingButtonView.swift(3 hunks)Projects/Feature/SettingsFeature/Sources/ViewModel/SettingsViewModel.swift(4 hunks)Projects/FeatureDependency/Sources/Coordinator/Coordinator.swift(2 hunks)Projects/FeatureDependency/Sources/Coordinator/CoordinatorType.swift(1 hunks)Projects/FeatureDependency/Sources/Mock/MockBusStopArrivalInfoRepository.swift(1 hunks)Projects/MainFeature/Sources/TabBarCoordinator.swift(1 hunks)Projects/NetworkService/Sources/EndPoint/EndPoint.swift(1 hunks)Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift(1 hunks)Projects/NetworkService/Sources/NetworkService/NetworkService.swift(1 hunks)
💤 Files with no reviewable changes (2)
- Projects/App/Sources/AppDelegate.swift
- Projects/App/Sources/AppDelegate+Register.swift
🧰 Additional context used
🧬 Code Graph Analysis (18)
Projects/NetworkService/Sources/NetworkService/NetworkService.swift (1)
Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift (3)
request(16-26)request(28-72)request(74-127)
Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift (1)
Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (1)
checkForceUpdateNeeded(23-40)
Projects/Domain/Sources/RepositoryInterface/BusStopArrivalInfoRepository.swift (2)
Projects/Data/Sources/Repository/DefaultBusStopArrivalInfoRepository.swift (2)
fetchArrivalList(22-26)fetchArrivalList(28-40)Projects/FeatureDependency/Sources/Mock/MockBusStopArrivalInfoRepository.swift (2)
fetchArrivalList(18-202)fetchArrivalList(206-397)
Projects/FeatureDependency/Sources/Coordinator/Coordinator.swift (1)
Projects/App/Sources/Coordinator/AppCoordinator.swift (1)
openURL(61-65)
Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift (1)
Projects/NetworkService/Sources/EndPoint/EndPoint.swift (1)
toURLRequest(36-63)
Projects/Feature/SettingsFeature/Sources/ViewModel/SettingsViewModel.swift (1)
Projects/Feature/SettingsFeature/Sources/Coordinator/DefaultSettingsCoordinator.swift (1)
presentPrivacy(34-42)
Projects/CoreDataService/Sources/CoreDataStorage.swift (1)
Projects/CoreDataService/Sources/CoreDataStorageImpl.swift (4)
create(58-70)read(72-81)update(83-89)delete(91-97)
Projects/Data/Sources/Repository/DefaultFavoritesRepository.swift (1)
Projects/CoreDataService/Sources/CoreDataStorageImpl.swift (3)
read(72-81)create(58-70)delete(91-97)
Projects/CoreDataService/Sources/CoreDataModel.swift (1)
Projects/Data/Sources/DTO/CoreDataModelObject/FavoritesBusResponseMO+CoreDataClass.swift (2)
toDataModel(38-52)sync(54-61)
Projects/App/Sources/Coordinator/AppCoordinator.swift (3)
Projects/App/Sources/SceneDelegate.swift (1)
sceneWillEnterForeground(80-82)Projects/App/Sources/Coordinator/Splash/SplashCoordinator.swift (1)
start(39-44)Projects/FeatureDependency/Sources/Coordinator/Coordinator.swift (1)
openURL(54-56)
Projects/App/Sources/Coordinator/Splash/SplashViewController.swift (1)
Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift (1)
transform(39-70)
Projects/FeatureDependency/Sources/Mock/MockBusStopArrivalInfoRepository.swift (1)
Projects/Data/Sources/Repository/DefaultBusStopArrivalInfoRepository.swift (2)
fetchArrivalList(22-26)fetchArrivalList(28-40)
Projects/Domain/Sources/RepositoryInterface/FavoritesRepository.swift (3)
Projects/Data/Sources/Repository/DefaultFavoritesRepository.swift (6)
fetchFavorites(36-41)fetchFavorites(101-115)addFavorites(43-47)addFavorites(68-75)removeFavorites(49-53)removeFavorites(77-86)Projects/FeatureDependency/Sources/Mock/MockFavoritesRepository.swift (4)
fetchFavorites(25-27)fetchFavorites(28-31)addFavorites(32-34)removeFavorites(35-37)Projects/CoreDataService/Sources/CoreDataStorageImpl.swift (1)
create(58-70)
Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (2)
Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift (4)
getCachedVersionCheckInfo(29-31)getAppStoreURL(45-47)fetchRequiredVersion(33-39)saveVersionCheckInfoCache(41-43)Projects/Core/Sources/Extension/TimeInterval+.swift (1)
hour(12-14)
Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift (1)
Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift (4)
getCachedVersionCheckInfo(29-31)fetchRequiredVersion(33-39)saveVersionCheckInfoCache(41-43)getAppStoreURL(45-47)
Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift (7)
Projects/App/Sources/Coordinator/Splash/SplashViewController.swift (1)
viewDidLoad(31-63)Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (1)
checkForceUpdateNeeded(23-40)Projects/App/Sources/Coordinator/Splash/SplashCoordinator.swift (1)
startTabFlow(46-53)Projects/App/Sources/Coordinator/AppCoordinator.swift (1)
openURL(61-65)Projects/FeatureDependency/Sources/Coordinator/Coordinator.swift (1)
openURL(54-56)Projects/CoreDataService/Sources/CoreDataContainerBuilder.swift (1)
buildContainer(22-45)Projects/Core/Sources/DIContainer/DIContainer.swift (1)
setLogger(17-19)
Projects/CoreDataService/Sources/CoreDataStorageImpl.swift (2)
Projects/CoreDataService/Sources/Legacy/DefaultCoreDataService.swift (1)
save(148-168)Projects/Data/Sources/DTO/CoreDataModelObject/FavoritesBusResponseMO+CoreDataClass.swift (2)
sync(54-61)toDataModel(38-52)
Projects/App/Sources/Coordinator/Splash/SplashCoordinator.swift (3)
Projects/App/Sources/Coordinator/AppCoordinator.swift (2)
openURL(61-65)start(38-47)Projects/FeatureDependency/Sources/Coordinator/Coordinator.swift (1)
openURL(54-56)Projects/MainFeature/Sources/TabBarCoordinator.swift (1)
start(33-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build_test
🔇 Additional comments (49)
Projects/Core/Sources/Extension/TimeInterval+.swift (1)
12-14: 시간 변환 로직이 정확하고 유용한 유틸리티입니다.구현이 간단하고 명확하며, 1시간 = 3600초 계산이 올바릅니다.
Projects/Core/Sources/TypeBuilder.swift (1)
11-16: 결과 빌더 구현이 올바르고 제네릭 설계가 우수합니다.
@resultBuilder를 사용한 선언적 문법 지원이 잘 구현되어 있습니다. 특히 제네릭 타입T를 사용하여 재사용성을 높인 점이 좋습니다.Projects/Domain/Sources/Entity/Response/FavoritesBusResponse.swift (1)
11-11: CoreData 가져오기가 적절하며 Core Data 통합을 지원합니다.
CoreDataModel프로토콜 준수를 위한 필요한 import로 보입니다. Swift Concurrency 전환 과정에서 Core Data 저장소 구현을 지원하는 변경사항입니다.Projects/FeatureDependency/Sources/Coordinator/CoordinatorType.swift (1)
13-13: 스플래시 화면 지원을 위한 적절한 열거형 케이스 추가입니다.
splash케이스가 기존 코디네이터 타입들과 일관성 있게 추가되었으며, 스플래시 화면 플로우를 지원하는 변경사항입니다.Projects/NetworkService/Sources/NetworkService/NetworkService.swift (1)
14-14: Swift Concurrency 지원을 위한 비동기 메서드 추가가 적절합니다.
async throws -> Data시그니처가 올바르게 구현되어 있으며, 기존 리액티브 메서드들과 함께 공존하여 점진적 마이그레이션을 지원합니다.DefaultNetworkService의 구현과도 일치합니다.Projects/Domain/Sources/RepositoryInterface/BusStopArrivalInfoRepository.swift (1)
14-15: Swift Concurrency 전환을 위한 async 메서드 추가가 적절합니다.기존 RxSwift 기반 메서드와 함께 async/await 패턴을 지원하는 것은 점진적 마이그레이션에 좋은 접근입니다.
Projects/FeatureDependency/Sources/Coordinator/Coordinator.swift (2)
21-21: URL 처리를 위한 프로토콜 확장이 적절합니다.코디네이터 계층구조에서 URL 처리를 표준화하는 좋은 접근입니다.
54-56: 기본 구현이 올바른 위임 패턴을 따릅니다.부모 코디네이터로 URL 처리를 위임하는 체인 오브 리스폰서빌리티 패턴이 적절히 구현되었습니다.
Projects/Core/Sources/PropertyWrapper/UserDefaultsWrapper.swift (1)
49-56: 옵셔널 값을 위한 편의 이니셜라이저가 잘 설계되었습니다.제네릭 제약 조건을 통해 타입 안전성을 보장하면서 기존 로직을 재사용하는 좋은 구현입니다.
Projects/Data/Sources/Repository/DefaultBusStopArrivalInfoRepository.swift (1)
22-26: async/await 메서드 구현이 올바르게 되었습니다.새로운 비동기 메서드가 적절히 구현되었으며, 기존 RxSwift 메서드와 병행하여 사용할 수 있도록 잘 설계되었습니다.
_toDomain프로퍼티가 throws를 지원하여 에러 처리도 적절합니다.Projects/Domain/Sources/Entity/VersionCheckInfo.swift (1)
11-19: 도메인 엔티티 설계가 적절합니다.
VersionCheckInfo구조체가 깔끔하게 정의되었으며, 캐싱 메커니즘을 위한updatedAt프로퍼티와 버전 정보를 위한requiredVersion프로퍼티가 적절히 포함되어 있습니다.Codable준수로 직렬화/역직렬화도 지원됩니다.Projects/Domain/Sources/UseCase/Protocol/VersionCheckUseCase.swift (1)
14-14: 프로토콜 메서드의 async/await 전환이 적절합니다.메서드 시그니처가
checkForceUpdateNeeded() async throws -> ForceUpdate로 변경되어 더 명확한 의도를 나타냅니다. 기존의fetchAppStoreURL()메서드보다 더 구체적인 기능을 나타내며,ForceUpdate열거형을 반환하여 상태를 더 명확하게 표현합니다.Projects/Domain/Sources/Entity/ForceUpdate.swift (1)
11-14: 열거형으로의 리팩토링이 적절합니다.
ForceUpdate를 구조체에서 열거형으로 변경한 것은 훌륭한 개선입니다. 업데이트 필요 여부를 상태로 명확하게 표현하며,needed케이스에서appStoreURL을 연관값으로 제공하여 필요할 때만 URL을 포함하도록 설계되었습니다.Projects/Feature/SettingsFeature/Sources/ViewModel/SettingsViewModel.swift (5)
3-3: Core 모듈 import 추가가 적절합니다.
@InfoPlistWrapper프로퍼티 래퍼를 사용하기 위해 Core 모듈을 import한 것이 올바릅니다.
35-39: @InfoPlistWrapper 사용으로 코드가 개선되었습니다.기존의
Bundle.main직접 접근 방식에서@InfoPlistWrapper프로퍼티 래퍼를 사용하는 것으로 변경되어 타입 안전성과 코드 가독성이 향상되었습니다.
46-50: 일관된 프로퍼티 래퍼 사용으로 코드 일관성이 향상되었습니다.위치 정보 관련 URL 접근도 동일한 패턴으로 변경되어 코드 일관성이 좋습니다.
70-70: 앱 버전 접근 방식 개선
String.getCurrentVersion()에서String.currentVersion으로 변경되어 더 간결하고 일관된 API 사용이 이루어졌습니다.
81-83: defaultValue 파라미터 사용으로 에러 처리가 개선되었습니다.
@InfoPlistWrapper에서defaultValue파라미터를 사용하여 빈 문자열을 기본값으로 설정함으로써 guard문 없이도 안전한 처리가 가능해졌습니다.Projects/CoreDataService/Sources/CoreDataStorage.swift (1)
9-14: 잘 설계된 CoreData 추상화 프로토콜입니다.Swift Concurrency 패턴을 적절히 활용하여 비동기 CRUD 작업을 정의했습니다. 제네릭 제약 조건과 메서드 시그니처가 깔끔하게 구성되어 있습니다.
Projects/Domain/Sources/Entity/Response/AppVersionInfoResponse.swift (1)
14-14: 기본 버전 정의가 좋습니다.기본 버전을 정적 상수로 정의한 것은 좋은 접근 방식입니다.
Projects/CoreDataService/Sources/CoreDataModel.swift (1)
11-16: Core Data 모델 매핑을 위한 훌륭한 프로토콜 설계입니다.타입 안전성을 보장하면서 Core Data 관리 객체와 Swift 데이터 모델 간의 변환을 깔끔하게 정의했습니다. 제약 조건들이 적절히 설정되어 있습니다.
Projects/Data/Sources/DTO/BusStopArrivalInfoDTO.swift (2)
19-21: 명확한 에러 타입 정의가 좋습니다.연관 값을 통해 구체적인 에러 메시지를 제공하는 것은 디버깅과 사용자 경험에 도움이 됩니다.
23-35: 명시적 에러 처리를 위한 throwing 프로퍼티가 잘 구현되었습니다.기존 옵셔널 반환 방식의
toDomain프로퍼티와 함께 제공하여 사용 상황에 따라 선택할 수 있도록 한 것이 좋습니다. Swift Concurrency 패턴과 잘 어울립니다.Projects/Domain/Sources/Entity/Alert.swift (1)
13-33: Alert 모델 구현이 잘 되어 있습니다.TypeBuilder를 활용한 선언적 구문으로 Alert 액션을 구성하는 방식이 깔끔하고 직관적입니다. 구조체 설계가 단순하면서도 필요한 기능을 모두 포함하고 있어 좋습니다.
Projects/Data/Sources/DTO/CoreDataModelObject/FavoritesBusResponseMO+CoreDataClass.swift (1)
35-62: CoreDataModel 프로토콜 준수가 잘 구현되었습니다.도메인 모델과 CoreData 모델 간의 양방향 매핑이 적절히 구현되어 있고, sync 메서드를 통한 데이터 동기화 로직도 깔끔합니다. 필수 프로퍼티들에 대한 nil 체크도 포함되어 있어 안전합니다.
Projects/App/Sources/Coordinator/Splash/SplashViewController.swift (2)
15-29: 뷰 컨트롤러 초기화가 적절히 구현되었습니다.의존성 주입을 통한 뷰 모델 초기화와 스토리보드 초기화 방지가 적절하게 처리되어 있습니다.
31-63: Alert 처리 로직이 깔끔하게 구현되었습니다.뷰 모델의 transform 메서드를 통해 입력을 처리하고, alert 출력을 메인 스레드에서 바인딩하여 UIAlertController로 변환하는 로직이 적절합니다. RxSwift의 리액티브 패턴을 잘 활용했습니다.
Projects/Feature/SettingsFeature/Sources/View/SettingButtonView.swift (2)
22-30: 프로퍼티 선언과 버전 정보 접근 방식이 개선되었습니다.lazy var에서 let으로 변경하여 불필요한 지연 초기화를 제거했고, String.currentVersion을 사용하여 새로운 프로퍼티 래퍼 방식과 일치시킨 것이 좋습니다.
32-60: 코드 일관성이 향상되었습니다.모든 버튼 프로퍼티를 let으로 통일하고 불필요한 공백을 제거하여 코드의 일관성과 가독성이 개선되었습니다.
Projects/Data/Sources/Repository/DefaultFavoritesRepository.swift (1)
18-54: Actor 기반의 우수한 비동기 구현입니다!Swift Concurrency를 활용한 thread-safe한 구현과 AsyncStream을 통한 반응형 업데이트가 잘 구성되어 있습니다.
Projects/App/Sources/Coordinator/AppCoordinator.swift (2)
15-20: 의존성 주입을 위한 명확한 프로토콜 정의입니다.AppCoordinatorDependency 프로토콜이 필요한 의존성을 명확하게 정의하고 있어 테스트 가능성과 유지보수성이 향상됩니다.
61-65: URL 검증을 포함한 안전한 구현입니다.
canOpenURL검사를 통해 URL을 안전하게 처리하고 있습니다.Projects/App/Sources/SceneDelegate.swift (2)
27-30: Scene 라이프사이클을 위한 AsyncStream 설정이 적절합니다.
bufferingNewest(1)정책으로 최신 이벤트만 유지하는 것이 scene 이벤트 처리에 적합합니다.
32-42: 타입 안전한 Info.plist 접근 방식이 훌륭합니다.
@InfoPlistWrapper를 사용하여 타입 안전성과 기본값 처리가 개선되었습니다.Projects/App/Sources/Coordinator/Splash/SplashCoordinator.swift (1)
14-17: openURL 메서드 구현 확인이 필요합니다.
SplashCoordinator프로토콜에서openURL(_:)을 선언했지만,SplashCoordinatorImpl에서는 구현하지 않았습니다. 이는Coordinator의 기본 구현(parent에게 위임)을 사용하도록 의도된 것인가요?만약 의도적이라면 문제없지만, 특별한 처리가 필요하다면 구현을 추가해야 합니다.
Also applies to: 19-54
Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift (1)
11-16: Swift Concurrency 전환이 잘 되었습니다프로토콜 메서드들이 RxSwift에서 async/await 패턴으로 깔끔하게 전환되었습니다.
Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (1)
23-40: 버전 체크 로직이 잘 구현되었습니다캐싱과 버전 비교 로직이 효율적으로 구현되었고, async/await 패턴을 잘 활용하고 있습니다.
Projects/Core/Sources/Extension/String+.swift (1)
28-38: Info.plist 접근 리팩토링이 훌륭합니다
@InfoPlistWrapper를 사용하여 타입 안전성과 기본값 처리가 개선되었습니다. 중복 코드도 제거되고 가독성도 향상되었네요.Projects/Core/Sources/PropertyWrapper/InfoPlistWrapper.swift (4)
16-18: LGTM! 깔끔한 프로퍼티 래퍼 구현입니다.
Bundle.main.object(forInfoDictionaryKey:)는 thread-safe하므로 동시성 환경에서도 안전하게 사용할 수 있습니다.
29-37: Optional 타입 지원이 잘 구현되었습니다.이 확장을 통해 Info.plist의 선택적 값들을 기본값 없이도 안전하게 처리할 수 있습니다.
83-100: Bool 타입의 강건한 구현이 인상적입니다.다양한 문자열 표현("true", "yes", "1", "false", "no", "0")을 지원하여 Info.plist 설정의 유연성을 높였습니다.
102-117: 컬렉션 타입의 엄격한 변환 정책을 확인해주세요.현재 구현은 하나의 요소라도 변환에 실패하면 전체 배열이 nil을 반환합니다. 이는 타입 안전성을 보장하지만, 부분적인 데이터 로드가 필요한 경우 제한적일 수 있습니다.
Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift (3)
15-27: 의존성 주입을 통한 설정 관리 개선이 좋습니다.
appStoreID와domainURL을 생성자 주입으로 변경하여 테스트 가능성과 설정의 명확성이 향상되었습니다.
33-39: Swift Concurrency로의 전환이 깔끔합니다.async/await 패턴으로 변경하여 비동기 코드의 가독성이 크게 향상되었습니다.
45-47: 명시적인 에러 처리로 개선되었습니다.Optional 대신 throws를 사용하여 실패 원인을 명확히 전달할 수 있게 되었습니다.
Projects/CoreDataService/Sources/CoreDataStorageImpl.swift (4)
19-27: 백그라운드 컨텍스트 설정이 잘 구성되었습니다.성능 최적화를 위한 undo 비활성화와 명시적인 병합 정책 설정이 적절합니다. 한글 주석으로 설계 의도를 명확히 설명한 점도 좋습니다.
72-81: 배치 사이즈를 활용한 효율적인 읽기 구현입니다.
fetchBatchSize설정으로 대량 데이터 처리 시 메모리 효율성을 확보했습니다.
43-54: 안전한 저장 로직이 잘 구현되었습니다.변경사항 확인 후 저장하고, 실패 시 롤백하는 방어적 프로그래밍이 적절합니다.
32-32: CoreDataModel의 id 프로퍼티가 보장됩니다CoreDataModel 프로토콜이 Identifiable을 상속하고
where ID: CVarArg제약을 두고 있어, 모든 T: CoreDataModel 타입은var id: ID프로퍼티를 반드시 구현합니다. 따라서request.predicate = NSPredicate(format: "id == %@", data.id as CVarArg)구문은 안전하게 사용할 수 있습니다.
| public var toURL: URL { | ||
| get throws { | ||
| var urlComponent = URLComponents() | ||
| urlComponent.scheme = scheme.toString | ||
| urlComponent.host = host | ||
| urlComponent.port = Int(port) | ||
| urlComponent.path = path | ||
| if !query.isEmpty { | ||
| urlComponent.queryItems = query.map { | ||
| .init(name: $0.key, value: $0.value) | ||
| } | ||
| } | ||
| guard let url = urlComponent.url else { | ||
| throw URLError(.badURL) | ||
| } | ||
| return url | ||
| } | ||
| let urlStr = urlComponent.url?.absoluteString | ||
| .replacingOccurrences(of: "%25", with: "%") | ||
|
|
||
| return urlStr | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
URL 문자열 처리 로직의 일관성을 확인해야 합니다.
새로운 toURL 프로퍼티가 toURLRequest 메서드와 다르게 "%25"를 "%"로 치환하는 로직이 누락되어 있습니다. 이는 일관성 문제를 야기할 수 있습니다.
🏁 Script executed:
#!/bin/bash
# toURLRequest와 toURL의 URL 생성 로직 비교
echo "=== toURLRequest 메서드에서의 URL 생성 로직 ==="
ast-grep --pattern 'func toURLRequest() throws -> URLRequest {
$$$
guard let urlStr = urlComponent.url?.absoluteString
.replacingOccurrences(of: "%25", with: "%"),
let url = URL(string: urlStr)
$$$
}'
echo "=== toURL 프로퍼티에서의 URL 생성 로직 ==="
ast-grep --pattern 'var toURL: URL {
get throws {
$$$
guard let url = urlComponent.url else {
$$$
}
return url
}
}'Length of output: 518
추가 확인: toURLRequest 구현과 %25 치환 로직 위치를 검색합니다.
🏁 Script executed:
#!/bin/bash
echo "=== toURLRequest 메서드 구현 확인 ==="
rg -n "func toURLRequest" -C 3
echo "=== ‘%25’ 치환 로직 확인 ==="
rg -n "replacingOccurrences(of: \"%25\"" -C 3Length of output: 919
다음 스크립트로 toURLRequest 전체 구현과 %25 치환 로직 위치를 다시 확인해 주세요.
🏁 Script executed:
#!/bin/bash
echo "=== toURLRequest 전체 구현 ==="
rg -n "func toURLRequest" -C 10 Projects/NetworkService/Sources/EndPoint/EndPoint.swift
echo
echo "=== replacingOccurrences 로직 검색 ==="
rg -n "replacingOccurrences" -C 5 Projects/NetworkService/Sources/EndPoint/EndPoint.swiftLength of output: 1453
toURL 프로퍼티에 %25 → % 치환 로직 추가 필요
toURLRequest()에서 %25를 %로 바꾸는 로직을 적용한 것과 달리, toURL 프로퍼티에는 해당 로직이 빠져 있어 이중 인코딩된 퍼센트 문자가 남을 수 있습니다. 아래 위치를 수정해주세요.
• 파일: Projects/NetworkService/Sources/EndPoint/EndPoint.swift
범위: public var toURL: URL { … } (약 65–82행)
예시 수정안:
public var toURL: URL {
get throws {
var urlComponent = URLComponents()
urlComponent.scheme = scheme.toString
urlComponent.host = host
urlComponent.port = Int(port)
urlComponent.path = path
if !query.isEmpty {
urlComponent.queryItems = query.map {
.init(name: $0.key, value: $0.value)
}
}
- guard let url = urlComponent.url else {
- throw URLError(.badURL)
- }
- return url
+ // toURLRequest와 동일하게 “%25” → “%” 치환
+ guard let urlStr = urlComponent.url?.absoluteString
+ .replacingOccurrences(of: "%25", with: "%"),
+ let url = URL(string: urlStr)
+ else {
+ throw URLError(.badURL)
+ }
+ return url
}
}🤖 Prompt for AI Agents
In Projects/NetworkService/Sources/EndPoint/EndPoint.swift between lines 65 and
82, the toURL computed property lacks the logic to replace "%25" with "%" in the
generated URL string, which can cause double-encoded percent characters. After
constructing the URL from URLComponents, convert it to a string, replace all
occurrences of "%25" with "%", then create and return a new URL from the
modified string. Ensure to handle the optional URL creation safely and throw
URLError(.badURL) if the final URL is invalid.
| public func request(endPoint: any EndPoint) async throws -> Data { | ||
| let urlRequest = try endPoint.toURLRequest() | ||
| let (data, response) = try await URLSession.shared.data(for: urlRequest) | ||
| guard let httpURLResponse = response as? HTTPURLResponse else { | ||
| throw NetworkError.invalidResponse | ||
| } | ||
| guard 200..<300 ~= httpURLResponse.statusCode else { | ||
| throw NetworkError.invalidStatusCode(httpURLResponse.statusCode) | ||
| } | ||
| return data | ||
| } |
There was a problem hiding this comment.
async 메서드에서 데이터 검증 로직이 누락되었습니다.
기존 Observable 버전과 달리 async 메서드에서는 data가 nil인 경우에 대한 검증이 없습니다. Observable 버전에서는 55-59번 라인에서 이를 처리하고 있습니다.
public func request(endPoint: any EndPoint) async throws -> Data {
let urlRequest = try endPoint.toURLRequest()
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpURLResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard 200..<300 ~= httpURLResponse.statusCode else {
throw NetworkError.invalidStatusCode(httpURLResponse.statusCode)
}
+ guard !data.isEmpty else {
+ throw NetworkError.invalidData
+ }
return data
}📝 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.
| public func request(endPoint: any EndPoint) async throws -> Data { | |
| let urlRequest = try endPoint.toURLRequest() | |
| let (data, response) = try await URLSession.shared.data(for: urlRequest) | |
| guard let httpURLResponse = response as? HTTPURLResponse else { | |
| throw NetworkError.invalidResponse | |
| } | |
| guard 200..<300 ~= httpURLResponse.statusCode else { | |
| throw NetworkError.invalidStatusCode(httpURLResponse.statusCode) | |
| } | |
| return data | |
| } | |
| public func request(endPoint: any EndPoint) async throws -> Data { | |
| let urlRequest = try endPoint.toURLRequest() | |
| let (data, response) = try await URLSession.shared.data(for: urlRequest) | |
| guard let httpURLResponse = response as? HTTPURLResponse else { | |
| throw NetworkError.invalidResponse | |
| } | |
| guard 200..<300 ~= httpURLResponse.statusCode else { | |
| throw NetworkError.invalidStatusCode(httpURLResponse.statusCode) | |
| } | |
| guard !data.isEmpty else { | |
| throw NetworkError.invalidData | |
| } | |
| return data | |
| } |
🤖 Prompt for AI Agents
In Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift
between lines 16 and 26, the async request method lacks validation for nil data,
unlike the Observable version which checks for this condition. Add a check after
receiving the data to verify it is not nil, and throw an appropriate error if it
is nil to ensure consistent data validation.
| public func fetchArrivalList(busStopId: String) async throws -> Domain.BusStopArrivalInfoResponse { | ||
| BusStopArrivalInfoResponse( | ||
| busStopId: "23290", | ||
| busStopName: "강남구보건소", | ||
| direction: "강남구청역", | ||
| buses: [ | ||
| BusArrivalInfoResponse( | ||
| busId: "124000038", | ||
| busName: "342", | ||
| busType: BusType.trunkLine.rawValue, | ||
| nextStation: "강남구청역", | ||
| firstArrivalState: ArrivalState | ||
| .arrivalTime(time: 62), | ||
| firstArrivalRemaining: "3번째 전", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 913), | ||
| secondArrivalRemaining: "6번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "100100075", | ||
| busName: "472", | ||
| busType: BusType.trunkLine.rawValue, | ||
| nextStation: "강남구청역", | ||
| firstArrivalState: ArrivalState | ||
| .arrivalTime(time: 198), | ||
| firstArrivalRemaining: "1번째 전", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 566), | ||
| secondArrivalRemaining: "5번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "100100226", | ||
| busName: "3414", | ||
| busType: BusType.branchLine.rawValue, | ||
| nextStation: "삼성동서광아파트", | ||
| firstArrivalState: ArrivalState.soon, | ||
| firstArrivalRemaining: "", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 1086), | ||
| secondArrivalRemaining: "9번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "100100612", | ||
| busName: "3426", | ||
| busType: BusType.branchLine.rawValue, | ||
| nextStation: "삼성동서광아파트", | ||
| firstArrivalState: ArrivalState.soon, | ||
| firstArrivalRemaining: "", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 689), | ||
| secondArrivalRemaining: "6번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "100100500", | ||
| busName: "4312", | ||
| busType: BusType.branchLine.rawValue, | ||
| nextStation: "강남구청역", | ||
| firstArrivalState: ArrivalState | ||
| .arrivalTime(time: 490), | ||
| firstArrivalRemaining: "4번째 전", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 916), | ||
| secondArrivalRemaining: "9번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "100100226", | ||
| busName: "3414", | ||
| busType: BusType.airport.rawValue, | ||
| nextStation: "삼성동서광아파트", | ||
| firstArrivalState: ArrivalState.soon, | ||
| firstArrivalRemaining: "", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 1086), | ||
| secondArrivalRemaining: "9번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "100100612", | ||
| busName: "3426", | ||
| busType: BusType.airport.rawValue, | ||
| nextStation: "삼성동서광아파트", | ||
| firstArrivalState: ArrivalState.soon, | ||
| firstArrivalRemaining: "", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 689), | ||
| secondArrivalRemaining: "6번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "100100500", | ||
| busName: "4312", | ||
| busType: BusType.airport.rawValue, | ||
| nextStation: "강남구청역", | ||
| firstArrivalState: ArrivalState | ||
| .arrivalTime(time: 490), | ||
| firstArrivalRemaining: "4번째 전", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 916), | ||
| secondArrivalRemaining: "9번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "1001005001", | ||
| busName: "4312", | ||
| busType: BusType.branchLine.rawValue, | ||
| nextStation: "강남구청역", | ||
| firstArrivalState: ArrivalState | ||
| .arrivalTime(time: 490), | ||
| firstArrivalRemaining: "4번째 전", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 916), | ||
| secondArrivalRemaining: "9번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "1001005002", | ||
| busName: "4312", | ||
| busType: BusType.branchLine.rawValue, | ||
| nextStation: "강남구청역", | ||
| firstArrivalState: ArrivalState | ||
| .arrivalTime(time: 490), | ||
| firstArrivalRemaining: "4번째 전", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 916), | ||
| secondArrivalRemaining: "9번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "1001005003", | ||
| busName: "4312", | ||
| busType: BusType.branchLine.rawValue, | ||
| nextStation: "강남구청역", | ||
| firstArrivalState: ArrivalState | ||
| .arrivalTime(time: 490), | ||
| firstArrivalRemaining: "4번째 전", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 916), | ||
| secondArrivalRemaining: "9번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ), | ||
| BusArrivalInfoResponse( | ||
| busId: "1001005004", | ||
| busName: "4312", | ||
| busType: BusType.branchLine.rawValue, | ||
| nextStation: "강남구청역", | ||
| firstArrivalState: ArrivalState | ||
| .arrivalTime(time: 490), | ||
| firstArrivalRemaining: "4번째 전", | ||
| secondArrivalState: ArrivalState | ||
| .arrivalTime(time: 916), | ||
| secondArrivalRemaining: "9번째 전", | ||
| adirection: "", | ||
| isFavorites: false, | ||
| isAlarmOn: false | ||
| ) | ||
| ] | ||
| ) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
코드 중복을 줄이기 위한 리팩토링이 필요합니다.
새로운 async 메서드가 추가되어 Swift Concurrency 지원이 개선되었습니다. 하지만 기존 RxSwift 메서드와 동일한 목업 데이터가 중복되어 있어 유지보수성이 떨어집니다.
다음과 같이 공통 메서드로 리팩토링하는 것을 고려해보세요:
+ private func createMockResponse() -> BusStopArrivalInfoResponse {
+ BusStopArrivalInfoResponse(
+ busStopId: "23290",
+ busStopName: "강남구보건소",
+ direction: "강남구청역",
+ buses: [
+ // ... 목업 데이터 ...
+ ]
+ )
+ }
+
public func fetchArrivalList(busStopId: String) async throws -> Domain.BusStopArrivalInfoResponse {
- BusStopArrivalInfoResponse(
- // ... 중복된 목업 데이터 ...
- )
+ createMockResponse()
}그리고 기존 RxSwift 메서드도 같은 방식으로 수정하여 데이터 일관성을 보장하세요.
🤖 Prompt for AI Agents
In
Projects/FeatureDependency/Sources/Mock/MockBusStopArrivalInfoRepository.swift
around lines 18 to 202, the fetchArrivalList async method contains duplicated
mock data that is also used in the existing RxSwift method, reducing
maintainability. Refactor by extracting the common mock data creation into a
shared private method that returns the BusStopArrivalInfoResponse, then have
both the async and RxSwift methods call this shared method to ensure data
consistency and reduce duplication.
| func fetchFavorites() -> Observable<[FavoritesBusResponse]> { | ||
| Single.create { | ||
| try await fetchFavorites() | ||
| } | ||
| .asObservable() | ||
| } |
There was a problem hiding this comment.
Single.create 내에서 올바른 completion 처리가 필요합니다.
현재 구현은 Single.create의 observer를 올바르게 사용하지 않고 있습니다. async 메서드의 결과나 에러를 적절히 전달해야 합니다.
다음과 같이 수정하세요:
-extension AsyncFavoritesRepository {
- func fetchFavorites() -> Observable<[FavoritesBusResponse]> {
- Single.create {
- try await fetchFavorites()
- }
- .asObservable()
- }
+extension AsyncFavoritesRepository {
+ func fetchFavorites() -> Observable<[FavoritesBusResponse]> {
+ Single.create { observer in
+ Task {
+ do {
+ let favorites = try await self.fetchFavorites()
+ observer(.success(favorites))
+ } catch {
+ observer(.failure(error))
+ }
+ }
+ return Disposables.create()
+ }
+ .asObservable()
+ }
+}📝 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.
| func fetchFavorites() -> Observable<[FavoritesBusResponse]> { | |
| Single.create { | |
| try await fetchFavorites() | |
| } | |
| .asObservable() | |
| } | |
| extension AsyncFavoritesRepository { | |
| func fetchFavorites() -> Observable<[FavoritesBusResponse]> { | |
| Single.create { observer in | |
| Task { | |
| do { | |
| let favorites = try await self.fetchFavorites() | |
| observer(.success(favorites)) | |
| } catch { | |
| observer(.failure(error)) | |
| } | |
| } | |
| return Disposables.create() | |
| } | |
| .asObservable() | |
| } | |
| } |
🤖 Prompt for AI Agents
In Projects/Domain/Sources/RepositoryInterface/FavoritesRepository.swift around
lines 30 to 35, the Single.create block does not correctly handle the observer's
completion by passing the async method's result or error. Modify the
Single.create closure to call observer(.success(result)) when fetchFavorites()
succeeds and observer(.failure(error)) if it throws, ensuring proper completion
signaling.
| let appGroupStoreUrl = FileManager.default | ||
| .containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName)? | ||
| .appendingPathComponent("\(Constants.fileName).sqlite") ?? URL(filePath: "") |
There was a problem hiding this comment.
App Group URL 실패 시 적절한 에러 처리가 필요합니다
빈 문자열로 URL을 생성하는 것은 예측할 수 없는 동작을 유발할 수 있습니다.
guard let을 사용하여 명시적으로 에러를 처리하세요:
-let appGroupStoreUrl = FileManager.default
- .containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName)?
- .appendingPathComponent("\(Constants.fileName).sqlite") ?? URL(filePath: "")
+guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName) else {
+ throw CoreDataError.appGroupContainerNotFound
+}
+let appGroupStoreUrl = containerURL.appendingPathComponent("\(Constants.fileName).sqlite")📝 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.
| let appGroupStoreUrl = FileManager.default | |
| .containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName)? | |
| .appendingPathComponent("\(Constants.fileName).sqlite") ?? URL(filePath: "") | |
| // Before: silently defaulting to an empty URL, which is invalid | |
| guard let containerURL = FileManager.default | |
| .containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName) else { | |
| throw CoreDataError.appGroupContainerNotFound | |
| } | |
| let appGroupStoreUrl = containerURL | |
| .appendingPathComponent("\(Constants.fileName).sqlite") |
🤖 Prompt for AI Agents
In Projects/CoreDataService/Sources/CoreDataContainerBuilder.swift around lines
25 to 27, the code assigns an empty URL when the app group container URL
retrieval fails, which can cause unpredictable behavior. Replace the current
optional chaining with a guard let statement to explicitly unwrap the container
URL; if it fails, handle the error appropriately by returning or throwing an
error instead of creating an empty URL.
| // 에러가 방출될 때 처리 방식을 고민해야 한다. | ||
| // 1. appGroupStoreUrl, 2. CKContainer.default().accountStatus(), 3. loadPersistentStores | ||
| public func buildContainer() async -> NSPersistentContainer { | ||
| let container: NSPersistentContainer = await { | ||
| let container: NSPersistentContainer | ||
| let appGroupStoreUrl = FileManager.default | ||
| .containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName)? | ||
| .appendingPathComponent("\(Constants.fileName).sqlite") ?? URL(filePath: "") | ||
| let persistentStoreDescription = NSPersistentStoreDescription(url: appGroupStoreUrl) | ||
| if await CKContainer.shouldUseCloudKit { | ||
| container = NSPersistentCloudKitContainer(name: Constants.fileName) | ||
| persistentStoreDescription.cloudKitContainerOptions = .init(containerIdentifier: Constants.containerIdentifier) | ||
| } else { | ||
| container = NSPersistentContainer(name: Constants.fileName) | ||
| } | ||
| container.viewContext.automaticallyMergesChangesFromParent = true | ||
| container.persistentStoreDescriptions = [persistentStoreDescription] | ||
| return container | ||
| }() | ||
| let _: Void = await withCheckedContinuation { continuation in | ||
| container.loadPersistentStores { _, error in | ||
| continuation.resume() | ||
| } | ||
| } | ||
| return container | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Core Data 초기화 에러 처리가 필요합니다
주석에서도 언급하셨듯이 에러 처리가 필요한데, 현재는 모든 에러를 무시하고 있습니다. Core Data 초기화 실패는 앱 전체 기능에 영향을 미치므로 적절한 에러 처리가 필수입니다.
다음과 같이 에러를 throw하도록 수정하세요:
-public func buildContainer() async -> NSPersistentContainer {
+public func buildContainer() async throws -> NSPersistentContainer {그리고 loadPersistentStores에서 에러 처리:
-let _: Void = await withCheckedContinuation { continuation in
- container.loadPersistentStores { _, error in
- continuation.resume()
- }
-}
+try await withCheckedThrowingContinuation { continuation in
+ container.loadPersistentStores { _, error in
+ if let error = error {
+ continuation.resume(throwing: error)
+ } else {
+ continuation.resume()
+ }
+ }
+}📝 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.
| // 에러가 방출될 때 처리 방식을 고민해야 한다. | |
| // 1. appGroupStoreUrl, 2. CKContainer.default().accountStatus(), 3. loadPersistentStores | |
| public func buildContainer() async -> NSPersistentContainer { | |
| let container: NSPersistentContainer = await { | |
| let container: NSPersistentContainer | |
| let appGroupStoreUrl = FileManager.default | |
| .containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName)? | |
| .appendingPathComponent("\(Constants.fileName).sqlite") ?? URL(filePath: "") | |
| let persistentStoreDescription = NSPersistentStoreDescription(url: appGroupStoreUrl) | |
| if await CKContainer.shouldUseCloudKit { | |
| container = NSPersistentCloudKitContainer(name: Constants.fileName) | |
| persistentStoreDescription.cloudKitContainerOptions = .init(containerIdentifier: Constants.containerIdentifier) | |
| } else { | |
| container = NSPersistentContainer(name: Constants.fileName) | |
| } | |
| container.viewContext.automaticallyMergesChangesFromParent = true | |
| container.persistentStoreDescriptions = [persistentStoreDescription] | |
| return container | |
| }() | |
| let _: Void = await withCheckedContinuation { continuation in | |
| container.loadPersistentStores { _, error in | |
| continuation.resume() | |
| } | |
| } | |
| return container | |
| } | |
| // 에러가 방출될 때 처리 방식을 고민해야 한다. | |
| // 1. appGroupStoreUrl, 2. CKContainer.default().accountStatus(), 3. loadPersistentStores | |
| public func buildContainer() async throws -> NSPersistentContainer { | |
| let container: NSPersistentContainer = await { | |
| let container: NSPersistentContainer | |
| let appGroupStoreUrl = FileManager.default | |
| .containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName)? | |
| .appendingPathComponent("\(Constants.fileName).sqlite") ?? URL(filePath: "") | |
| let persistentStoreDescription = NSPersistentStoreDescription(url: appGroupStoreUrl) | |
| if await CKContainer.shouldUseCloudKit { | |
| container = NSPersistentCloudKitContainer(name: Constants.fileName) | |
| persistentStoreDescription.cloudKitContainerOptions = .init(containerIdentifier: Constants.containerIdentifier) | |
| } else { | |
| container = NSPersistentContainer(name: Constants.fileName) | |
| } | |
| container.viewContext.automaticallyMergesChangesFromParent = true | |
| container.persistentStoreDescriptions = [persistentStoreDescription] | |
| return container | |
| }() | |
| try await withCheckedThrowingContinuation { continuation in | |
| container.loadPersistentStores { _, error in | |
| if let error = error { | |
| continuation.resume(throwing: error) | |
| } else { | |
| continuation.resume() | |
| } | |
| } | |
| } | |
| return container | |
| } |
🤖 Prompt for AI Agents
In Projects/CoreDataService/Sources/CoreDataContainerBuilder.swift around lines
20 to 45, the buildContainer function currently ignores errors from
loadPersistentStores, which can cause silent failures. Modify the function to be
throwable by adding throws to its signature. Inside the loadPersistentStores
completion handler, check if an error exists and if so, throw it or propagate it
properly to the caller. This ensures that Core Data initialization errors are
not ignored and can be handled appropriately by the caller.
| private func registerDependency() async { | ||
| let coreDataContainer = await CoreDataContainerBuilder().buildContainer() | ||
| let firebaseLogger = FirebaseLoggerImpl() | ||
|
|
||
| DIContainer.setLogger(firebaseLogger) | ||
|
|
||
| DIContainer.register(type: CoreDataStorage.self, CoreDataStorageImpl(container: coreDataContainer)) | ||
| DIContainer.register(type: ForceUpdateService.self, DefaultForceUpdateService()) | ||
| DIContainer.register(type: CoreDataService.self, DefaultCoreDataService()) | ||
| DIContainer.register(type: NetworkService.self, DefaultNetworkService()) | ||
| DIContainer.register(type: LocationService.self, DefaultLocationService()) | ||
|
|
||
| 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: VersionCheckUseCase.self, | ||
| VersionCheckUseCaseImpl( | ||
| currentVersion: dependency.appVersion | ||
| ) | ||
| ) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
의존성 중복 등록 방지 필요
스플래시 화면이 다시 나타날 경우 의존성이 재등록될 수 있습니다.
DIContainer에 이미 등록되었는지 확인하는 로직을 추가하거나, 등록 상태를 추적하는 것이 좋겠습니다:
private func registerDependency() async {
+ // 이미 등록되었는지 확인
+ guard DIContainer.resolve(type: CoreDataStorage.self) == nil else {
+ return
+ }
+
let coreDataContainer = await CoreDataContainerBuilder().buildContainer()
// ...또는 DIContainer에 isRegistered 메서드를 추가하는 것도 고려해보세요.
📝 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.
| private func registerDependency() async { | |
| let coreDataContainer = await CoreDataContainerBuilder().buildContainer() | |
| let firebaseLogger = FirebaseLoggerImpl() | |
| DIContainer.setLogger(firebaseLogger) | |
| DIContainer.register(type: CoreDataStorage.self, CoreDataStorageImpl(container: coreDataContainer)) | |
| DIContainer.register(type: ForceUpdateService.self, DefaultForceUpdateService()) | |
| DIContainer.register(type: CoreDataService.self, DefaultCoreDataService()) | |
| DIContainer.register(type: NetworkService.self, DefaultNetworkService()) | |
| DIContainer.register(type: LocationService.self, DefaultLocationService()) | |
| 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: VersionCheckUseCase.self, | |
| VersionCheckUseCaseImpl( | |
| currentVersion: dependency.appVersion | |
| ) | |
| ) | |
| } | |
| private func registerDependency() async { | |
| // 이미 등록되었는지 확인 | |
| guard DIContainer.resolve(type: CoreDataStorage.self) == nil else { | |
| return | |
| } | |
| let coreDataContainer = await CoreDataContainerBuilder().buildContainer() | |
| let firebaseLogger = FirebaseLoggerImpl() | |
| DIContainer.setLogger(firebaseLogger) | |
| DIContainer.register(type: CoreDataStorage.self, CoreDataStorageImpl(container: coreDataContainer)) | |
| DIContainer.register(type: ForceUpdateService.self, DefaultForceUpdateService()) | |
| DIContainer.register(type: CoreDataService.self, DefaultCoreDataService()) | |
| DIContainer.register(type: NetworkService.self, DefaultNetworkService()) | |
| DIContainer.register(type: LocationService.self, DefaultLocationService()) | |
| 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: VersionCheckUseCase.self, | |
| VersionCheckUseCaseImpl( | |
| currentVersion: dependency.appVersion | |
| ) | |
| ) | |
| } |
🤖 Prompt for AI Agents
In Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift around lines 72
to 111, the registerDependency function registers dependencies without checking
if they are already registered, which can cause duplicate registrations if the
splash screen appears again. To fix this, add a check before each
DIContainer.register call to verify if the dependency type is already
registered, either by using an isRegistered method if available or by
implementing a mechanism to track registration status, and only register the
dependency if it is not already registered.
| private func registerDependency() async { | ||
| let coreDataContainer = await CoreDataContainerBuilder().buildContainer() |
There was a problem hiding this comment.
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.
| 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.
There was a problem hiding this comment.
Actionable comments posted: 3
🔭 Outside diff range comments (2)
Projects/NetworkService/Sources/NetworkService/NetworkService.swift (1)
23-32: 제네릭 async 메서드 추가 고려deprecated 처리는 적절하나, 완전한 마이그레이션을 위해 제네릭 버전의 async 메서드도 추가하는 것을 고려해보세요.
public protocol NetworkService { func request(endPoint: EndPoint) async throws -> Data + func request<T: Decodable>(endPoint: EndPoint, responseType: T.Type) async throws -> T @available(이를 통해 개발자들이 더 쉽게 마이그레이션할 수 있습니다.
Projects/Domain/Sources/RepositoryInterface/RegularAlarmRepository.swift (1)
27-31: async 기반 deleteRegularAlarm 메서드 추가 필요완료 핸들러 기반 메서드가 적절히 deprecated 처리되었습니다. 완전한 마이그레이션을 위해 async 버전을 추가해주세요.
+ func deleteRegularAlarm(response: RegularAlarmResponse) async throws + @available(*, deprecated, message: "이 메서드는 제거될 예정입니다.") func deleteRegularAlarm( response: RegularAlarmResponse, completion: @escaping () -> Void )
♻️ Duplicate comments (2)
Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift (2)
73-74: Core Data 초기화 에러 처리 필요
CoreDataContainerBuilder의buildContainer()메서드가 에러를 발생시킬 수 있는 경우 적절한 에러 처리가 필요합니다.private func registerDependency() async { - let coreDataContainer = await CoreDataContainerBuilder().buildContainer() + do { + let coreDataContainer = try await CoreDataContainerBuilder().buildContainer() + // 나머지 의존성 등록... + } catch { + firebaseLogger.log(name: "Core Data 초기화 실패: \(error)") + throw error + }
73-112: 의존성 중복 등록 방지 필요스플래시 화면이 다시 나타날 경우 의존성이 재등록될 수 있습니다. 이미 등록되었는지 확인하는 로직을 추가해야 합니다.
private func registerDependency() async { + // 이미 등록되었는지 확인 + guard DIContainer.resolve(type: CoreDataStorage.self) == nil else { + return + } + let coreDataContainer = await CoreDataContainerBuilder().buildContainer() // ...
🧹 Nitpick comments (1)
Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift (1)
115-123: Input/Output 구조 승인 및 향후 개선 제안적절한 Input/Output 구조가 정의되어 있습니다. 하지만 Swift Concurrency 전환의 맥락에서 RxSwift 대신 AsyncStream 사용을 고려해보세요.
struct Output { - let alert: Observable<Alert> + let alert: AsyncStream<Alert> }이는 향후 개선 사항으로 현재는 문제없습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (26)
.swiftlint.yml(2 hunks)Projects/App/Sources/Coordinator/Splash/SplashViewController.swift(1 hunks)Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift(1 hunks)Projects/CoreDataService/Sources/CoreDataContainerBuilder.swift(1 hunks)Projects/CoreDataService/Sources/CoreDataStorageImpl.swift(1 hunks)Projects/CoreDataService/Sources/Legacy/CoreDataService.swift(1 hunks)Projects/Data/Sources/Service/ForceUpdateService/DefaultForceUpdateService.swift(0 hunks)Projects/DesignSystem/Project.swift(1 hunks)Projects/Domain/Sources/Entity/Response/BusStopArrivalInfoResponse.swift(1 hunks)Projects/Domain/Sources/RepositoryInterface/BusStopArrivalInfoRepository.swift(1 hunks)Projects/Domain/Sources/RepositoryInterface/FavoritesRepository.swift(1 hunks)Projects/Domain/Sources/RepositoryInterface/RegularAlarmRepository.swift(1 hunks)Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift(1 hunks)Projects/Domain/Sources/Service/ForceUpdateService/ForceUpdateService.swift(0 hunks)Projects/Feature/AlarmFeature/Project.swift(1 hunks)Projects/Feature/BusStopFeature/Project.swift(1 hunks)Projects/Feature/HomeFeature/Project.swift(1 hunks)Projects/Feature/HomeFeature/Sources/ViewModel/FavoritesViewModel.swift(1 hunks)Projects/Feature/NearMapFeature/Project.swift(1 hunks)Projects/Feature/NearMapFeature/Sources/ViewController/NearMapViewController.swift(1 hunks)Projects/Feature/SearchFeature/Project.swift(1 hunks)Projects/Feature/SettingsFeature/Project.swift(1 hunks)Projects/FeatureDependency/Project.swift(1 hunks)Projects/FirebaseModule/Sources/FirebaseLoggerImpl.swift(1 hunks)Projects/NetworkService/Sources/NetworkService/NetworkService.swift(1 hunks)Projects/ThirdPartyLibs/Project.swift(1 hunks)
💤 Files with no reviewable changes (2)
- Projects/Domain/Sources/Service/ForceUpdateService/ForceUpdateService.swift
- Projects/Data/Sources/Service/ForceUpdateService/DefaultForceUpdateService.swift
✅ Files skipped from review due to trivial changes (13)
- Projects/Feature/HomeFeature/Sources/ViewModel/FavoritesViewModel.swift
- Projects/DesignSystem/Project.swift
- Projects/ThirdPartyLibs/Project.swift
- Projects/Feature/NearMapFeature/Sources/ViewController/NearMapViewController.swift
- Projects/Feature/AlarmFeature/Project.swift
- Projects/FeatureDependency/Project.swift
- Projects/CoreDataService/Sources/Legacy/CoreDataService.swift
- Projects/Feature/SearchFeature/Project.swift
- Projects/Feature/BusStopFeature/Project.swift
- Projects/Feature/NearMapFeature/Project.swift
- Projects/Feature/SettingsFeature/Project.swift
- Projects/FirebaseModule/Sources/FirebaseLoggerImpl.swift
- Projects/Feature/HomeFeature/Project.swift
🚧 Files skipped from review as they are similar to previous changes (7)
- .swiftlint.yml
- Projects/Domain/Sources/RepositoryInterface/BusStopArrivalInfoRepository.swift
- Projects/App/Sources/Coordinator/Splash/SplashViewController.swift
- Projects/Domain/Sources/RepositoryInterface/FavoritesRepository.swift
- Projects/Domain/Sources/RepositoryInterface/VersionCheckRepository.swift
- Projects/CoreDataService/Sources/CoreDataContainerBuilder.swift
- Projects/CoreDataService/Sources/CoreDataStorageImpl.swift
🧰 Additional context used
🧬 Code Graph Analysis (2)
Projects/NetworkService/Sources/NetworkService/NetworkService.swift (1)
Projects/NetworkService/Sources/NetworkService/DefaultNetworkService.swift (3)
request(16-26)request(28-72)request(74-127)
Projects/Domain/Sources/RepositoryInterface/RegularAlarmRepository.swift (1)
Projects/Data/Sources/Repository/DefaultRegularAlarmRepository.swift (2)
createRegularAlarm(31-75)updateRegularAlarm(77-89)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build_test
🔇 Additional comments (5)
Projects/Domain/Sources/Entity/Response/BusStopArrivalInfoResponse.swift (1)
126-129: 중복 식별자 처리 개선을 위한 좋은 방어적 프로그래밍 변경사항입니다.
uniqueKeysWithValues대신Dictionary(_, uniquingKeysWith:)를 사용하는 것은 좋은 개선입니다. 이전 구현에서는 즐겨찾기 목록에 중복 식별자가 있을 경우 런타임 크래시가 발생할 수 있었지만, 새로운 구현에서는 첫 번째 값을 유지하는 방식으로 중복을 안전하게 처리합니다.Projects/NetworkService/Sources/NetworkService/NetworkService.swift (2)
14-14: Swift Concurrency 전환을 위한 새로운 async 메서드 추가 승인새로운 async/await 기반 메서드가 올바르게 추가되었습니다. 이는 현대적인 Swift 비동기 프로그래밍 패턴을 따르며 타입 안전성을 보장합니다.
16-22: 적절한 deprecation 처리 승인기존 RxSwift 기반 메서드가 올바르게 deprecated 처리되었습니다. 한국어 메시지와 함께 마이그레이션 가이드가 명확하게 제공되어 있습니다.
Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift (2)
20-38: 의존성 주입 구조 승인프로토콜과 클래스 구조가 잘 설계되어 있습니다.
@Injected프로퍼티 래퍼를 사용한 의존성 주입 방식이 깔끔하게 구현되었습니다.
40-71: 비동기 처리 및 개선된 에러 처리 승인async/await 패턴이 적절히 구현되어 있으며, 이전 리뷰 피드백이 반영되어 Firebase 로깅이 추가되었습니다. 버전 체크 실패 시 적절한 에러 처리가 구현되어 있습니다.
| @available(*, deprecated, message: "이 메서드는 제거될 예정입니다.") | ||
| func updateRegularAlarm( | ||
| response: RegularAlarmResponse, | ||
| completion: @escaping () -> Void | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
async 기반 updateRegularAlarm 메서드 추가 필요
완료 핸들러 기반 메서드가 적절히 deprecated 처리되었습니다. 일관성을 위해 async 버전을 추가해주세요.
+ func updateRegularAlarm(response: RegularAlarmResponse) async throws
+
@available(*, deprecated, message: "이 메서드는 제거될 예정입니다.")
func updateRegularAlarm(
response: RegularAlarmResponse,
completion: @escaping () -> Void
)📝 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.
| @available(*, deprecated, message: "이 메서드는 제거될 예정입니다.") | |
| func updateRegularAlarm( | |
| response: RegularAlarmResponse, | |
| completion: @escaping () -> Void | |
| ) | |
| func updateRegularAlarm(response: RegularAlarmResponse) async throws | |
| @available(*, deprecated, message: "이 메서드는 제거될 예정입니다.") | |
| func updateRegularAlarm( | |
| response: RegularAlarmResponse, | |
| completion: @escaping () -> Void | |
| ) |
🤖 Prompt for AI Agents
In Projects/Domain/Sources/RepositoryInterface/RegularAlarmRepository.swift
around lines 22 to 26, the existing updateRegularAlarm method uses a completion
handler and is marked deprecated. To maintain consistency, add a new async
version of updateRegularAlarm that accepts a RegularAlarmResponse parameter and
returns asynchronously without a completion handler.
| @available(*, deprecated, message: "이 메서드는 제거될 예정입니다.") | ||
| func createRegularAlarm( | ||
| response: RegularAlarmResponse, | ||
| completion: @escaping () -> Void | ||
| ) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
async 기반 createRegularAlarm 메서드 추가 필요
완료 핸들러 기반 메서드가 적절히 deprecated 처리되었습니다. 완전한 마이그레이션을 위해 async 버전을 추가해주세요.
public protocol RegularAlarmRepository {
+ func createRegularAlarm(response: RegularAlarmResponse) async throws
+
@available(*, deprecated, message: "이 메서드는 제거될 예정입니다.")
func createRegularAlarm(
response: RegularAlarmResponse,
completion: @escaping () -> Void
)📝 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.
| @available(*, deprecated, message: "이 메서드는 제거될 예정입니다.") | |
| func createRegularAlarm( | |
| response: RegularAlarmResponse, | |
| completion: @escaping () -> Void | |
| ) | |
| public protocol RegularAlarmRepository { | |
| func createRegularAlarm(response: RegularAlarmResponse) async throws | |
| @available(*, deprecated, message: "이 메서드는 제거될 예정입니다.") | |
| func createRegularAlarm( | |
| response: RegularAlarmResponse, | |
| completion: @escaping () -> Void | |
| ) | |
| } |
🤖 Prompt for AI Agents
In Projects/Domain/Sources/RepositoryInterface/RegularAlarmRepository.swift
around lines 17 to 21, the existing createRegularAlarm method uses a completion
handler and is marked deprecated. To complete the migration, add a new async
version of createRegularAlarm that takes a RegularAlarmResponse parameter and
returns asynchronously without a completion handler.
| @available(*, deprecated, message: "이 변수는 제거될 예정입니다.") | ||
| var currentRegularAlarm: BehaviorSubject<[RegularAlarmResponse]> { get } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
async 기반 대체 프로퍼티 추가 필요
BehaviorSubject 기반 프로퍼티가 적절히 deprecated 처리되었습니다. 완전한 마이그레이션을 위해 async 기반 대체 프로퍼티를 추가하는 것을 고려해보세요.
public protocol RegularAlarmRepository {
+ var currentRegularAlarm: AsyncStream<[RegularAlarmResponse]> { get }
+
@available(*, deprecated, message: "이 변수는 제거될 예정입니다.")
var currentRegularAlarm: BehaviorSubject<[RegularAlarmResponse]> { get }📝 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.
| @available(*, deprecated, message: "이 변수는 제거될 예정입니다.") | |
| var currentRegularAlarm: BehaviorSubject<[RegularAlarmResponse]> { get } | |
| public protocol RegularAlarmRepository { | |
| var currentRegularAlarm: AsyncStream<[RegularAlarmResponse]> { get } | |
| @available(*, deprecated, message: "이 변수는 제거될 예정입니다.") | |
| var currentRegularAlarm: BehaviorSubject<[RegularAlarmResponse]> { get } | |
| } |
🤖 Prompt for AI Agents
In Projects/Domain/Sources/RepositoryInterface/RegularAlarmRepository.swift
around lines 14 to 15, the BehaviorSubject-based property currentRegularAlarm is
deprecated but lacks an async-based replacement. Add a new async property or
method that provides the same data asynchronously, enabling full migration away
from BehaviorSubject. Ensure the new async property is clearly named and
documented as the recommended alternative.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
Projects/Domain/Sources/UseCase/Protocol/AppVersionCheckUseCase.swift (1)
11-11: 불필요한 RxSwift import를 제거해주세요.프로토콜이 async/await 패턴으로 전환되면서 RxSwift를 더 이상 사용하지 않으므로 import문을 제거해야 합니다.
-import RxSwiftProjects/App/Sources/SceneDelegate.swift (1)
16-16: 불필요한 RxSwift import를 제거해주세요.SceneDelegate에서 더 이상 RxSwift를 사용하지 않으므로 import문을 제거해야 합니다.
-import RxSwiftProjects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (1)
13-13: 불필요한 RxSwift import를 제거해주세요.구현이 async/await 패턴으로 전환되면서 RxSwift를 더 이상 사용하지 않으므로 import문을 제거해야 합니다.
-import RxSwift
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift(1 hunks)Projects/App/Sources/SceneDelegate.swift(3 hunks)Projects/CoreDataService/Sources/CoreDataRepresentable.swift(1 hunks)Projects/CoreDataService/Sources/CoreDataStorage.swift(1 hunks)Projects/CoreDataService/Sources/CoreDataStorageImpl.swift(1 hunks)Projects/Data/Sources/DTO/CoreDataModelObject/CoreDataModelObject+Error.swift(1 hunks)Projects/Data/Sources/DTO/CoreDataModelObject/FavoritesBusResponseMO+CoreDataClass.swift(3 hunks)Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift(1 hunks)Projects/Domain/Sources/UseCase/Protocol/AppVersionCheckUseCase.swift(2 hunks)
✅ Files skipped from review due to trivial changes (1)
- Projects/CoreDataService/Sources/CoreDataRepresentable.swift
🚧 Files skipped from review as they are similar to previous changes (3)
- Projects/CoreDataService/Sources/CoreDataStorageImpl.swift
- Projects/Data/Sources/DTO/CoreDataModelObject/FavoritesBusResponseMO+CoreDataClass.swift
- Projects/App/Sources/Coordinator/Splash/SplashViewModel.swift
🧰 Additional context used
🧬 Code Graph Analysis (3)
Projects/Domain/Sources/UseCase/Protocol/AppVersionCheckUseCase.swift (1)
Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (1)
checkForceUpdateNeeded(23-40)
Projects/CoreDataService/Sources/CoreDataStorage.swift (1)
Projects/CoreDataService/Sources/CoreDataStorageImpl.swift (4)
create(58-72)read(74-83)update(85-91)delete(93-99)
Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (2)
Projects/Data/Sources/Repository/DefaultVersionCheckRepository.swift (4)
getCachedVersionCheckInfo(29-31)getAppStoreURL(45-47)fetchRequiredVersion(33-39)saveVersionCheckInfoCache(41-43)Projects/Core/Sources/Extension/TimeInterval+.swift (1)
hour(12-14)
🔇 Additional comments (10)
Projects/CoreDataService/Sources/CoreDataStorage.swift (1)
9-14: 프로토콜 설계가 우수합니다!Swift Concurrency 패턴을 올바르게 적용한 깔끔한 CRUD 프로토콜입니다. 제네릭 타입 제약조건
CoreDataRepresentable을 사용하여 타입 안전성을 보장하고, 모든 메서드가async throws로 일관성 있게 정의되어 있습니다.Projects/Data/Sources/DTO/CoreDataModelObject/CoreDataModelObject+Error.swift (2)
13-22: 에러 타입 정의가 적절합니다.
LocalizedError프로토콜을 준수하여 현지화된 오류 메시지를 제공하고, 한국어 메시지로 일관성을 유지하고 있습니다.
24-34: KeyPath 기반 unwrap 유틸리티의 private API 사용을 검토해주세요.
_kvcKeyPathString은 Swift의 internal API로 향후 버전에서 변경될 수 있습니다. 더 안정적인 대안을 고려해보세요.// 대안 구현 예시 func unwrap<T>(_ keyPath: KeyPath<Self, T?>, name: String? = nil) throws -> T { guard let value = self[keyPath: keyPath] else { let key = name ?? String(describing: keyPath) throw DTOParsingError.missingAttribute(key) } return value }또는 KeyPath를 위한 별도의 메타데이터 시스템을 고려해보세요.
Projects/Domain/Sources/UseCase/Protocol/AppVersionCheckUseCase.swift (1)
13-15: 프로토콜 설계 개선이 우수합니다!
- 프로토콜명이 더 구체적이고 명확해졌습니다 (
VersionCheckUseCase→AppVersionCheckUseCase)- 메서드명이 실제 기능을 더 잘 표현합니다 (
fetchAppStoreURL→checkForceUpdateNeeded)ForceUpdate열거형 반환으로 의미가 더 명확해졌습니다Projects/App/Sources/SceneDelegate.swift (4)
18-21: 의존성 주입 패턴 개선이 우수합니다!
AppCoordinatorDependency프로토콜 준수를 통해 깔끔한 의존성 주입을 구현했고,AppVersionCheckUseCase로 업데이트된 프로토콜을 올바르게 사용하고 있습니다.
27-30: AsyncStream 패턴이 잘 구현되었습니다!포그라운드 진입 이벤트를 비동기 스트림으로 전달하는 패턴이 적절하며,
bufferingNewest(1)정책으로 최신 이벤트만 유지하는 것이 합리적입니다.
32-42: @InfoPlistWrapper 사용이 효율적입니다!Info.plist 값들을 프로퍼티 래퍼로 간편하게 접근하도록 개선되었고, 기본값 설정도 적절합니다.
81-81: AsyncStream 이벤트 전달이 올바르게 구현되었습니다.
continuation.yield(scene)을 통해 포그라운드 진입 이벤트를 비동기적으로 전달하는 방식이 적절합니다.Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (2)
15-21: 클래스 이름 변경과 생성자 설계가 적절합니다!
VersionCheckUseCaseImpl로 네이밍이 구현체임을 명확히 표현합니다currentVersion매개변수를 통한 명시적 의존성 주입이 테스트 가능성을 높입니다
23-40: 캐싱 로직과 비동기 구현이 우수합니다!버전 체크 로직이 잘 구현되었습니다:
- 4시간 캐시 만료 정책이 합리적입니다
- 캐시된 버전과 현재 버전 비교가 적절합니다
- async/await 패턴을 올바르게 사용하고 있습니다
- 에러 전파가 적절하게 처리됩니다
| func openURL(_ url: URL) | ||
| } | ||
|
|
||
| final class SplashCoordinatorImpl: SplashCoordinator { |
There was a problem hiding this comment.
구조체 이름 앞에 Default를 안쓰고 뒤에 Impl를 붙이는 걸로 컨벤션이 바뀌는 걸까요?
There was a problem hiding this comment.
구현체라는 표현을 목적으로 네이밍을 수정해봤는데 어떤지 의견 부탁드려요! @isakatty
|
|
||
| final class SplashViewModel: ViewModel { | ||
| private weak var coordinator: SplashCoordinator? | ||
| @Injected private var versionCheckUseCase: AppVersionCheckUseCase |
There was a problem hiding this comment.
왜 SplashViewModel에서 @injected 를 사용하는게 안 좋은 형태라고 생각하셨는지 궁금합니다.
| var deeplinkHandler: DeeplinkHandler? | ||
|
|
||
| let disposeBag = DisposeBag() | ||
| let _sceneWillEnterForeground = AsyncStream<UIScene>.makeStream(bufferingPolicy: .bufferingNewest(1)) |
There was a problem hiding this comment.
내부에서만 사용하기 위한 컨벤션으로 _(언더바)를 붙이신걸까요? 그럼 private으로 변수를 만든거하고 쓰임새가 다른건가요?
| func transform(input: Input) -> Output { | ||
| let alertRelay = PublishRelay<Alert>() | ||
| Task { | ||
| try await input.viewDidLoad.value |
There was a problem hiding this comment.
transform 함수 자체가 viewDidLoad될 때 호출하게 되는데, input으로 시퀀스를 받아서 한번 더 호출하는 이유가 있을까요?
|
앱 실행시 버스를 즐겨찾기 할 때 간헐적으로 홈 UI에 즉시 적용이 안되는 현상이 있는데, 저만 그럴까요? |
작업내용
주요 변경사항은 코드레빗이 요약해주어 잘 설명되지 않는 부분만 추가하는 점 참고해주세요.
리뷰요청
관련 이슈
close #330
Summary by CodeRabbit
신규 기능
개선 및 변경
버그 수정
문서 및 스타일
Deprecated