Skip to content

Fix/#330 NetworkService, CoreDataService Swift Concurrency 형태로 전환#332

Open
gnksbm wants to merge 20 commits intodevfrom
fix/#330
Open

Fix/#330 NetworkService, CoreDataService Swift Concurrency 형태로 전환#332
gnksbm wants to merge 20 commits intodevfrom
fix/#330

Conversation

@gnksbm
Copy link
Contributor

@gnksbm gnksbm commented Jul 7, 2025

작업내용

주요 변경사항은 코드레빗이 요약해주어 잘 설명되지 않는 부분만 추가하는 점 참고해주세요.

  • Networking 리팩토링
    • async 형태로 사용할 수 있도록 코드를 추가하고 Rx 형태는 deprecated 처리하였습니다.
    • 디코딩 처리를 해주는 메서드는 네트워크 통신 객체가 수행해야할 기능이 아니라고 판단하여 추가하지 않았습니다.
  • CoreData 리팩토링
    • CoreDataContainer를 구성하는 Builder와 CoreData CRUD를 수행하는 두가지 객체로 분리하였습니다.
    • CoreDataStorage 객체에 필요한 Container를 Builder가 비동기로 생성하며 순차적인 의존성 주입이 필요합니다.
  • Splash 추가
    • CoreData를 리팩토링하며 초기화 단계가 async 비동기 형태로 변경되었고 지연 실행이 필요했습니다.
      • Home 이전 플로우를 관리할 수 있는 Splash 단계를 추가하여 해결하였습니다.
    • DI를 관리하는 SplashViewModel 내부에서 @injected 코드를 사용한 형태는 좋지 않다고 느겼습니다.
      • 객체 내부 코드에서 순차적 실행이 보장되어 크래시 위험은 없고 추후 리팩토링에서 개선할 예정입니다.
  • VersionCheck 리팩토링
    • VersionCheckUseCase는 SceneDelegate에서 사용되고 있었으며 DI를 SplashViewModel이 수행하기에 크래시가 발생하여 Splash에서 UseCase를 사용하도록 변경하였습니다.
    • VersionCheckUseCase 코드를 사용해보았을 때 Repository가 비즈니스 로직의 대부분을 수행하는 형태가 아키텍처에 맞지 않는 구현으로 판단되어 리팩토링을 진행하였습니다.
      • @isakatty 이 부분은 리팩토링이 올바르게 되었는지 다른 코드들보다 유심히 봐주시길 부탁드립니다!
  • infoPlistWrapper
    • UserDefaultsWrapper처럼 Bundle.main.object를 간편하게 사용할 수 있는 프로퍼티 래퍼를 추가하였습니다.

리뷰요청

관련 이슈

close #330

Summary by CodeRabbit

  • 신규 기능

    • 앱 실행 시 스플래시 화면 및 강제 업데이트 알림 기능이 추가되었습니다.
    • Info.plist 값을 안전하게 읽을 수 있는 프로퍼티 래퍼가 도입되었습니다.
    • Core Data 비동기 저장소 및 CRUD 기능이 추가되었습니다.
    • 즐겨찾기, 버스 도착 정보, 버전 체크 등 주요 저장소와 서비스가 비동기/async-await 방식으로 개선되었습니다.
    • Alert 및 AlertAction 구조체를 통한 알림 UI 구성 기능이 추가되었습니다.
    • 네트워크 서비스에 async/await 기반 요청 메서드가 추가되었습니다.
  • 개선 및 변경

    • RxSwift 기반 로직이 비동기/async-await 방식으로 대체되었습니다.
    • 앱 버전 체크 및 강제 업데이트 로직이 단순화되고, 캐시 기능이 추가되었습니다.
    • Info.plist 접근 및 버전 정보 표출 방식이 개선되었습니다.
    • Coordinator 구조에 URL 열기 기능이 추가되고, 스플래시 코디네이터가 도입되었습니다.
    • CoreData 모델과 DTO 간 변환 및 에러 처리 로직이 강화되었습니다.
    • 앱 실행 초기화 및 의존성 주입 방식이 리팩토링되었습니다.
    • 기존 프로젝트 설정 코드 포맷팅이 일관성 있게 개선되었습니다.
  • 버그 수정

    • 즐겨찾기, 버스 도착 정보 등에서 중복 키 처리 및 에러 핸들링이 강화되었습니다.
  • 문서 및 스타일

    • 코드 포맷팅 및 선언 방식이 일관성 있게 개선되었습니다.
    • 불필요한 파일 및 프로토콜이 제거되었습니다.
  • Deprecated

    • 기존 RxSwift 기반 저장소 및 서비스, 일부 프로토콜/메서드는 향후 제거 예정으로 deprecated 처리되었습니다.

@gnksbm gnksbm requested review from MUKER-WON and isakatty July 7, 2025 11:39
@gnksbm gnksbm self-assigned this Jul 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 7, 2025

Walkthrough

이번 변경에서는 NetworkService와 CoreDataService 모듈을 RxSwift 기반에서 Swift Concurrency(async/await) 기반으로 전환하였습니다. 이에 따라 관련 프로토콜, 구현체, 레포지토리, 유즈케이스, DTO, DI, 앱 진입 플로우, 버전 체크, Alert 구조 등이 대대적으로 개편되었습니다. RxSwift 기반 메서드는 deprecated 처리되었고, Info.plist 접근은 프로퍼티 래퍼로 대체되었습니다.

Changes

Cohort / File(s) Change Summary
NetworkService Swift Concurrency 전환
Projects/NetworkService/Sources/NetworkService/NetworkService.swift, .../DefaultNetworkService.swift, .../EndPoint/EndPoint.swift
프로토콜에 async/await 메서드 추가, 기존 Rx 메서드 deprecated, 구현체에 async 메서드 구현, EndPoint URL 생성 메서드도 throw 기반으로 변경
CoreDataService Swift Concurrency 전환
Projects/CoreDataService/Sources/CoreDataStorage.swift, .../CoreDataStorageImpl.swift, .../CoreDataRepresentable.swift, .../CoreDataContainerBuilder.swift, .../Legacy/CoreDataService.swift
CoreDataStorage 프로토콜 및 구현체 신설, CRUD 메서드 async/await로 제공, 기존 CoreDataService deprecated, NSPersistentContainer 비동기 빌더 추가
Favorites/BusStop/VersionCheck 레포지토리/유즈케이스 async화
Projects/Data/Sources/Repository/DefaultFavoritesRepository.swift, .../DefaultBusStopArrivalInfoRepository.swift, .../DefaultVersionCheckRepository.swift, Projects/Domain/Sources/RepositoryInterface/FavoritesRepository.swift, .../BusStopArrivalInfoRepository.swift, .../VersionCheckRepository.swift, .../UseCase/DefaultVersionCheckUseCase.swift, .../UseCase/Protocol/AppVersionCheckUseCase.swift, .../Entity/VersionCheckInfo.swift
기존 Rx 기반 메서드 deprecated 및 async/await 기반 메서드 추가/교체, ForceUpdate 구조 변경, 버전 체크 캐시 구조 신설, 관련 인터페이스 및 구현체 전면 수정
DI, 앱 초기화, Coordinator 구조 개편
Projects/App/Sources/AppDelegate+Register.swift, .../AppDelegate.swift, .../Coordinator/AppCoordinator.swift, .../Coordinator/Splash/SplashCoordinator.swift, .../Coordinator/Splash/SplashViewController.swift, .../Coordinator/Splash/SplashViewModel.swift, .../SceneDelegate.swift
AppDelegate에서 DI 등록 제거, SplashCoordinator 및 ViewModel 신설, DI 등록 및 버전 체크 Splash 단계에서 async로 처리, SceneDelegate가 DI 및 버전 정보 제공
Info.plist 접근 개선 및 PropertyWrapper 도입
Projects/Core/Sources/PropertyWrapper/InfoPlistWrapper.swift, .../Extension/String+.swift, .../Extension/TimeInterval+.swift, .../PropertyWrapper/UserDefaultsWrapper.swift, Projects/Feature/SettingsFeature/Sources/ViewModel/SettingsViewModel.swift
Info.plist 접근을 프로퍼티 래퍼로 대체, String 확장자 및 세팅 뷰모델에서 적용, TimeInterval에 hour 변환 메서드 추가, UserDefaultsWrapper에 옵셔널 지원
Alert, TypeBuilder 등 유틸/엔티티 신설 및 개선
Projects/Domain/Sources/Entity/Alert.swift, .../Entity/ForceUpdate.swift, .../Entity/Response/AppVersionInfoResponse.swift, .../Entity/Response/FavoritesBusResponse.swift, .../Entity/Response/BusStopArrivalInfoResponse.swift, Projects/Core/Sources/TypeBuilder.swift
Alert/AlertAction 구조 신설, ForceUpdate 구조체 → enum 변경, AppVersionInfoResponse InfoPlistLoadable 채택, TypeBuilder result builder 신설 등
CoreData DTO 파싱/에러 처리 개선
Projects/Data/Sources/DTO/CoreDataModelObject/CoreDataModelObject+Error.swift, .../CoreDataModelObject/FavoritesBusResponseMO+CoreDataClass.swift, .../DTO/BusStopArrivalInfoDTO.swift
DTO 파싱 에러 enum 및 프로토콜 신설, 필수값 언랩 메서드 추가, FavoritesBusResponse CoreDataRepresentable 채택, BusStopArrivalInfoDTO에 throw 기반 변환 추가
코디네이터 개선 및 URL 오픈 지원
Projects/FeatureDependency/Sources/Coordinator/Coordinator.swift, .../CoordinatorType.swift, Projects/MainFeature/Sources/TabBarCoordinator.swift
Coordinator에 openURL 메서드 추가 및 기본 구현, CoordinatorType에 splash case 추가, TabBarCoordinator UI 업데이트 메인 액터에서 async 처리
기타 리팩토링, 포맷팅, 불필요 코드 제거
Projects/Data/Sources/Service/ForceUpdateService/DefaultForceUpdateService.swift, Projects/Domain/Sources/Service/ForceUpdateService/ForceUpdateService.swift, Projects/DesignSystem/Project.swift, Projects/FeatureDependency/Project.swift, Projects/ThirdPartyLibs/Project.swift, Projects/Feature/SettingsFeature/Sources/View/SettingButtonView.swift, Projects/Feature/HomeFeature/Sources/ViewModel/FavoritesViewModel.swift, Projects/Feature/NearMapFeature/Sources/ViewController/NearMapViewController.swift, Projects/Feature/SettingsFeature/Project.swift, Projects/Feature/SearchFeature/Project.swift, Projects/Feature/HomeFeature/Project.swift, Projects/Feature/BusStopFeature/Project.swift, Projects/Feature/AlarmFeature/Project.swift, Projects/Feature/NearMapFeature/Project.swift, Projects/FirebaseModule/Sources/FirebaseLoggerImpl.swift, .swiftlint.yml
ForceUpdateService 관련 프로토콜/구현 제거, 포맷팅 및 선언 방식 통일, SwiftLint 룰 일부 조정 등

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
NetworkService 모듈 작업 (#330)
CoreDataService 모듈 작업 (#330)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Alert, AlertAction 구조 신설 (Projects/Domain/Sources/Entity/Alert.swift) Alert 구조 신설은 버전 체크 등 알림 UX 개선 목적이지만, 본 이슈의 NetworkService/CoreDataService 전환과 직접적 연관은 없음.
TypeBuilder result builder 신설 (Projects/Core/Sources/TypeBuilder.swift) TypeBuilder는 Alert 등에서 사용되지만, 본 이슈의 주요 목표에는 포함되지 않음.
ForceUpdate 구조체 → enum 변경 (Projects/Domain/Sources/Entity/ForceUpdate.swift) 버전 체크 로직 단순화 목적이지만, 이슈 명시적 범위에는 포함되지 않음.
SettingsFeature, HomeFeature 등 일부 포맷팅/불필요 코드 정리 주요 목표와 직접 관련 없음.

Possibly related PRs

Suggested reviewers

  • isakatty
  • MUKER-WON

Poem

🐇
Swift의 바람이 분다,
Rx는 가고 async가 온다.
코어데이터, 네트워크도
await로 춤을 춘다.
토끼는 깡총깡총 기뻐하며,
코드가 깔끔해진 오늘을
파란 하늘에 자랑한다!
🌱🚀

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c806a5e and 85f0e0f.

📒 Files selected for processing (3)
  • Projects/App/Sources/SceneDelegate.swift (3 hunks)
  • Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift (1 hunks)
  • Projects/Domain/Sources/UseCase/Protocol/AppVersionCheckUseCase.swift (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • Projects/Domain/Sources/UseCase/Protocol/AppVersionCheckUseCase.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Projects/App/Sources/SceneDelegate.swift
  • Projects/Domain/Sources/UseCase/DefaultVersionCheckUseCase.swift
⏰ 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
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#330

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

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

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 RxSwift
Projects/CoreDataService/Sources/CoreDataStorageImpl.swift (1)

100-111: 주석 처리된 API 설정의 용도를 확인해주세요.

이 주석은 향후 버전 관리 기능을 위한 것으로 보입니다. 구현 계획이 있다면 TODO 주석으로 변경하거나, 불필요하다면 제거를 고려해주세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f08e87 and 97bc5e2.

📒 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: 의존성 주입을 통한 설정 관리 개선이 좋습니다.

appStoreIDdomainURL을 생성자 주입으로 변경하여 테스트 가능성과 설정의 명확성이 향상되었습니다.


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)

구문은 안전하게 사용할 수 있습니다.

Comment on lines +65 to 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
}
let urlStr = urlComponent.url?.absoluteString
.replacingOccurrences(of: "%25", with: "%")

return urlStr
}
Copy link

Choose a reason for hiding this comment

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

💡 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 3

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

Length 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.

Comment on lines +16 to +26
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
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +18 to +202
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
)
]
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +30 to +35
func fetchFavorites() -> Observable<[FavoritesBusResponse]> {
Single.create {
try await fetchFavorites()
}
.asObservable()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +25 to +27
let appGroupStoreUrl = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: Constants.appGroupName)?
.appendingPathComponent("\(Constants.fileName).sqlite") ?? URL(filePath: "")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines 20 to 45
// 에러가 방출될 때 처리 방식을 고민해야 한다.
// 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
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

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.

Suggested change
// 에러가 방출될 때 처리 방식을 고민해야 한다.
// 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.

Comment on lines 72 to 111
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
)
)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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.

Comment on lines +72 to +73
private func registerDependency() async {
let coreDataContainer = await CoreDataContainerBuilder().buildContainer()
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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 초기화 에러 처리 필요

CoreDataContainerBuilderbuildContainer() 메서드가 에러를 발생시킬 수 있는 경우 적절한 에러 처리가 필요합니다.

 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

📥 Commits

Reviewing files that changed from the base of the PR and between 97bc5e2 and 60364fc.

📒 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 로깅이 추가되었습니다. 버전 체크 실패 시 적절한 에러 처리가 구현되어 있습니다.

Comment on lines +22 to 26
@available(*, deprecated, message: "이 메서드는 제거될 예정입니다.")
func updateRegularAlarm(
response: RegularAlarmResponse,
completion: @escaping () -> Void
)
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
@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.

Comment on lines +17 to 21
@available(*, deprecated, message: "이 메서드는 제거될 예정입니다.")
func createRegularAlarm(
response: RegularAlarmResponse,
completion: @escaping () -> Void
)
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
@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.

Comment on lines +14 to 15
@available(*, deprecated, message: "이 변수는 제거될 예정입니다.")
var currentRegularAlarm: BehaviorSubject<[RegularAlarmResponse]> { get }
Copy link

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
@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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
Projects/Domain/Sources/UseCase/Protocol/AppVersionCheckUseCase.swift (1)

11-11: 불필요한 RxSwift import를 제거해주세요.

프로토콜이 async/await 패턴으로 전환되면서 RxSwift를 더 이상 사용하지 않으므로 import문을 제거해야 합니다.

-import RxSwift
Projects/App/Sources/SceneDelegate.swift (1)

16-16: 불필요한 RxSwift import를 제거해주세요.

SceneDelegate에서 더 이상 RxSwift를 사용하지 않으므로 import문을 제거해야 합니다.

-import RxSwift
Projects/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

📥 Commits

Reviewing files that changed from the base of the PR and between 41590cf and c806a5e.

📒 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: 프로토콜 설계 개선이 우수합니다!

  • 프로토콜명이 더 구체적이고 명확해졌습니다 (VersionCheckUseCaseAppVersionCheckUseCase)
  • 메서드명이 실제 기능을 더 잘 표현합니다 (fetchAppStoreURLcheckForceUpdateNeeded)
  • 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 {
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


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 를 사용하는게 안 좋은 형태라고 생각하셨는지 궁금합니다.

var deeplinkHandler: DeeplinkHandler?

let disposeBag = DisposeBag()
let _sceneWillEnterForeground = AsyncStream<UIScene>.makeStream(bufferingPolicy: .bufferingNewest(1))
Copy link
Contributor

Choose a reason for hiding this comment

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

내부에서만 사용하기 위한 컨벤션으로 _(언더바)를 붙이신걸까요? 그럼 private으로 변수를 만든거하고 쓰임새가 다른건가요?

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으로 시퀀스를 받아서 한번 더 호출하는 이유가 있을까요?

@MUKER-WON
Copy link
Contributor

앱 실행시 버스를 즐겨찾기 할 때 간헐적으로 홈 UI에 즉시 적용이 안되는 현상이 있는데, 저만 그럴까요?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refact] NetworkService, CoreDataService Swift Concurrency 형태로 전환

2 participants