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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let project = Project(name: "App") {
MainFeature()
Data()
FirebaseModule()
FileManagerService()
SwiftLintScript()
UIKitInfoPlist()
AppInfoPlist(displayName: .displayName, marketingVersion: .marketingVersion, buildVersion: .buildVersion)
Expand Down
12 changes: 10 additions & 2 deletions Projects/App/Sources/AppDelegate+Register.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,32 @@ import Data
import Domain
import NetworkService
import FirebaseModule
import FileManagerService

extension AppDelegate {
func registerDependencies() {
let firebaseLogger = FirebaseLoggerImpl()
DIContainer.setLogger(firebaseLogger)

// MARK: Service
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: FileManagerService.self, DefaultFileManagerService())
DIContainer.register(type: LocalNotificationService.self, DefaultLocalNotificationService())
DIContainer.register(type: RegularAlarmEditingService.self, DefaultRegularAlarmEditingService())

// MARK: Repository
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, DefaultVersionCheckRepository())
DIContainer.register(type: BusStationVersionRepository.self, DefaultBusStationVersionRepository())
DIContainer.register(type: GithubFileDownloadRepository.self, DefaultGithubFileDownloadRepository())

// MARK: UseCase
DIContainer.register(type: FavoritesUseCase.self, DefaultFavoritesUseCase())
DIContainer.register(type: RegularAlarmUseCase.self, DefaultRegularAlarmUseCase())
DIContainer.register(type: AddRegularAlarmUseCase.self, DefaultAddRegularAlarmUseCase())
Expand All @@ -41,5 +48,6 @@ extension AppDelegate {
DIContainer.register(type: NearMapUseCase.self, DefaultNearMapUseCase())
DIContainer.register(type: FirebaseLogger.self, firebaseLogger)
DIContainer.register(type: VersionCheckUseCase.self, DefaultVersionCheckUseCase())
DIContainer.register(type: UpdateBusStationListUseCase.self, DefaultUpdateBusStationListUseCase())
}
}
23 changes: 22 additions & 1 deletion Projects/App/Sources/Coordinator/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,30 @@

import UIKit

import Core
import Domain
import FeatureDependency
import MainFeature
import BusStopFeature
import RxSwift

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

init(navigationController: UINavigationController) {
self.navigationController = navigationController
}

func start() {
checkAndDownloadBusStationList()
Copy link
Contributor

Choose a reason for hiding this comment

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

다운로드를 하고 탭을 시작하게 되어있는데, 다운로드 실패 성공에 따라 시작을 다르게 되어야 원활하게 앱 사용이 될 것 같은데 의견 궁금하고 관련해서는 기획까지 이야기가 되어야할 것 같습니다.

retry를 해줄 수 있는 화면을 만든다던가 안내 화면을 띄운다던가, 혹시 다운로드되는 화면에서 프로그레스 뷰를 보여준다던가..!


let tabBarCoordinator = TabBarCoordinator(
navigationController: navigationController,
navigationController: navigationController,
coordinatorProvider: coordinatorProvider
)
childs.append(tabBarCoordinator)
Expand All @@ -43,4 +49,19 @@ final class AppCoordinator: Coordinator {
childs.append(busStopCoordinator)
busStopCoordinator.start()
}

private func checkAndDownloadBusStationList() {
Copy link
Contributor

Choose a reason for hiding this comment

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

#330 브랜치 merge해서 수정해주실 수 있을까요?
#332 작업에서 DI 시점이 변경되어 병합 시 문제가 있을 것 같습니다!

@Injected var useCase: UpdateBusStationListUseCase

useCase.execute()
.subscribe(
onError: { error in
print("🚏❌ bus_station_list.json 업데이트 실패: \(error)")
},
onCompleted: {
print("🚏✅ bus_station_list.json 업데이트 확인 및 처리 완료")
}
)
.disposed(by: disposeBag)
}
}
9 changes: 9 additions & 0 deletions Projects/Core/Sources/Extension/String+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ public extension String {
return serverKey
}

static var githubAccessToken: Self {
guard let any = Bundle.main.object(forInfoDictionaryKey: "GITHUB_ACCESS_TOKEN"),
let githubAccessToken = any as? String
else {
return ""
}
return githubAccessToken
}
Comment on lines +37 to +44
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 처리 방식의 일관성을 개선해주세요.

githubAccessToken은 빈 문자열을 반환하는 반면, serverKeyfatalError를 발생시킵니다. GitHub 토큰이 없으면 API 인증이 실패할 가능성이 높으므로, 다음 중 하나를 고려해보세요:

  1. serverKey와 같이 필수값으로 처리하여 fatalError 사용
  2. Optional 타입으로 변경하여 호출부에서 적절히 처리

현재 구현에서는 토큰이 없어도 네트워크 요청이 시도되어 인증 실패가 발생할 수 있습니다:

-    static var githubAccessToken: Self {
-        guard let any = Bundle.main.object(forInfoDictionaryKey: "GITHUB_ACCESS_TOKEN"),
-              let githubAccessToken = any as? String
-        else {
-            return ""
-        }
-        return githubAccessToken
-    }
+    static var githubAccessToken: Self {
+        guard let any = Bundle.main.object(forInfoDictionaryKey: "GITHUB_ACCESS_TOKEN"),
+              let githubAccessToken = any as? String
+        else { fatalError("GitHub Access Token이 설정되지 않았습니다") }
+        return githubAccessToken
+    }
📝 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
static var githubAccessToken: Self {
guard let any = Bundle.main.object(forInfoDictionaryKey: "GITHUB_ACCESS_TOKEN"),
let githubAccessToken = any as? String
else {
return ""
}
return githubAccessToken
}
static var githubAccessToken: Self {
guard let any = Bundle.main.object(forInfoDictionaryKey: "GITHUB_ACCESS_TOKEN"),
let githubAccessToken = any as? String
else { fatalError("GitHub Access Token이 설정되지 않았습니다") }
return githubAccessToken
}
🤖 Prompt for AI Agents
In Projects/Core/Sources/Extension/String+.swift around lines 37 to 44, the
githubAccessToken property returns an empty string when the token is missing,
unlike serverKey which triggers a fatalError. To improve error handling
consistency, either change githubAccessToken to also use fatalError when the
token is missing, treating it as a required value, or modify its return type to
Optional<String> so callers can handle the absence of the token explicitly. This
prevents silent failures during API authentication.

Comment on lines +37 to +44
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 토큰을 Core에 위치하게 되었을 때 다른 레이어에서도 접근할 수 있게 될 것 같은데, 특정 레이어에서만 필요한 데이터라서 여기에 위치시키는게 맞을지 고민이 되네요


/// domain url
static var domainURL: Self {
guard let any = Bundle.main.object(
Expand Down
1 change: 1 addition & 0 deletions Projects/Data/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ let project = Project(name: "Data") {
NetworkService()
CoreDataService()
FirebaseInterface()
FileManagerService()
FrameworkInfoPlist(marketingVersion: .marketingVersion)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

import Domain
import NetworkService
import Core

import RxSwift

public final class DefaultBusStationVersionRepository: BusStationVersionRepository {
@Injected private var networkService: NetworkService

@UserDefaultsWrapper(key: "busStationDataVersion", defaultValue: nil)
private var localVersion: String?

public init() { }

public func fetchRemoteVersion() -> Observable<BusStationVersion> {
let endPoint = GithubFileDownloadEndPoint(
repo: "BusStationData",
filePath: "bus_station_version.json"
)
return networkService.request(endPoint: endPoint)
.decode(type: BusStationVersion.self, decoder: JSONDecoder())
}

public func fetchLocalVersion() -> String? {
return localVersion
}

public func save(version: String) {
localVersion = version
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation

import Domain
import FileManagerService
import NetworkService
import Core

import RxSwift

public final class DefaultGithubFileDownloadRepository: GithubFileDownloadRepository {
@Injected private var networkService: NetworkService
@Injected private var fileManagerService: FileManagerService

public init() { }

public func downloadFile(
Copy link
Contributor

Choose a reason for hiding this comment

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

다운로드 받은 파일을 디바이스에 저장하는 의사결정은 UseCase에서 하는게 적합하지 않을까 생각이 드는데 어떻게 생각하시나요?
BusStationVersionRepository의 fetchRemoteVersion처럼 단방향으로 데이터만 전달해 주는 방향을 생각했습니다!

repo: String,
filePath: String,
directoryName: String,
fileName: String
) -> Observable<Void> {
let endPoint = GithubFileDownloadEndPoint(
repo: repo,
filePath: filePath
)
return networkService.request(endPoint: endPoint)
.flatMap { [weak self] data -> Observable<Void> in
guard let self = self else {
return .error(RxError.unknown) // Or a custom error
}
return self.fileManagerService.save(
data: data,
directoryName: directoryName,
fileName: fileName
)
}
Comment on lines +27 to +36
Copy link
Contributor

Choose a reason for hiding this comment

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

이렇게 한다면 내부에 weak self 해서 작성해야하는 반복 코드를 줄일 수 있을 것 같습니다.
만약 weak self로 한다면? return 시 떨어질 때 로깅을 넣어줘야하지 않을까 싶어요

image

}
}
5 changes: 5 additions & 0 deletions Projects/Domain/Sources/Entity/BusStationVersion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public struct BusStationVersion: Decodable {
public let busStationVersion: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

import RxSwift

public protocol BusStationVersionRepository {
/// 원격 저장소에서 최신 버스 정류장 데이터의 버전 정보를 가져옵니다.
func fetchRemoteVersion() -> Observable<BusStationVersion>

/// 로컬에 저장된 버스 정류장 데이터의 버전 정보를 가져옵니다.
func fetchLocalVersion() -> String?

/// 로컬에 새로운 버스 정류장 데이터의 버전 정보를 저장합니다.
func save(version: String)
Copy link
Contributor

Choose a reason for hiding this comment

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

함수명이 위의 fetch 메서드들을 보았을 때 불명확한부분이 있는 것 같습니다.
saveLocalVersion(: String)이나 saveLocalVersion( version: String)처럼 어떤 버전을 저장하는지 명확하게 나타낼 필요가 있을 것 같아요!
그리고 Repository의 비동기 메서드 Swift Concurrency 형태로 수정 부탁드립니다!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

import RxSwift

public protocol GithubFileDownloadRepository {
func downloadFile(
repo: String,
filePath: String,
directoryName: String,
fileName: String
) -> Observable<Void>
}
60 changes: 60 additions & 0 deletions Projects/Domain/Sources/UseCase/UpdateBusStationListUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation
import Core
import RxSwift

public protocol UpdateBusStationListUseCase {
func execute() -> Observable<Void>
}
Comment on lines +5 to +7
Copy link
Contributor

Choose a reason for hiding this comment

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

인터페이스는 인터페이스 폴더로 따로 작성해주시면 좋을 것 같습니다!


public final class DefaultUpdateBusStationListUseCase: UpdateBusStationListUseCase {
Copy link
Contributor

Choose a reason for hiding this comment

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

이 객체와 인터페이스도 SwiftConcurrency 형태로 수정 부탁드립니다!

@Injected private var versionRepository: BusStationVersionRepository
@Injected private var fileDownloadRepository: GithubFileDownloadRepository

public init() { }

public func execute() -> Observable<Void> {
return versionRepository.fetchRemoteVersion()
.do(onNext: { remoteVersion in
print("🚏 버스정류장 원격 버전 확인: \(remoteVersion.busStationVersion)")
}, onError: { error in
print("🚏 버스정류장 원격 버전 확인 중 에러 발생: \(error)")
})
.flatMap { [weak self] remoteVersion -> Observable<Void> in
guard let self = self else { return .error(RxError.unknown) }

let localVersion = self.versionRepository.fetchLocalVersion()
print("🚏 버스정류장 로컬 버전 확인: \(localVersion ?? "기존 파일 없음")")

let needsUpdate = localVersion != remoteVersion.busStationVersion
print("🚏 [버스정류장 버전 비교]")
print("로컬: \(localVersion ?? "파일 없음")")
print("원격: \(remoteVersion.busStationVersion)")

if needsUpdate {
return self.downloadAndSave(
newVersion: remoteVersion.busStationVersion
)
} else {
print("🚏 버스정류장 정보가 이미 최신 버전입니다.")
return .just(())
}
}
}
Comment on lines +15 to +42
Copy link
Contributor

Choose a reason for hiding this comment

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

디버깅용 print는 없애거나 형태를 고정하는 걸 같이 의논해보면 어떨까요? 지금도 로깅 자체가 많이 깔리고 있어서 프린트가 많이 찍히는게 고민이 됩니다! 그리고 이전 코멘트랑 동일하게 withUnretained를 사용하면 약한 참조 관련해서 보일러 플레이트가 줄 것 같습니다.


private func downloadAndSave(newVersion: String) -> Observable<Void> {
print("🚏 downloadAndSave 호출. bus_station_list.json 다운로드")
return fileDownloadRepository.downloadFile(
repo: "BusStationData",
filePath: "bus_station_list.json",
directoryName: "jsons",
fileName: "bus_station_list.json"
)
Comment on lines +46 to +51
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

하드코딩된 저장소와 파일 정보를 설정 가능하게 만드는 것을 고려해보세요.

repo: "BusStationData"와 파일 경로들이 하드코딩되어 있어 유연성이 떨어집니다.

구성 객체나 의존성 주입을 통해 이 값들을 외부에서 설정할 수 있도록 개선하는 것을 권장합니다:

public struct BusStationConfig {
    let repository: String
    let filePath: String
    let directoryName: String
    let fileName: String
}
🤖 Prompt for AI Agents
In Projects/Domain/Sources/UseCase/UpdateBusStationListUseCase.swift around
lines 46 to 51, the repository and file path parameters are hardcoded, reducing
flexibility. Refactor the code to accept these values via a configuration object
or dependency injection, such as a BusStationConfig struct containing
repository, filePath, directoryName, and fileName properties, and use this
config to provide the parameters to downloadFile instead of hardcoded strings.

.do(onNext: { _ in

self.versionRepository.save(version: newVersion)
print("🚏 로컬에 최신 버스정류장 정보 저장")
}, onError: { error in
print("🚏 파일 다운로드 중 에러 발생: \(error)")
})
}
}
9 changes: 9 additions & 0 deletions Projects/FileManagerService/Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import ProjectDescription
import ProjectDescriptionHelpers

let project = Project(name: "FileManagerService") {
FileManagerService {
Domain()
FrameworkInfoPlist(marketingVersion: .marketingVersion)
}
}
Loading
Loading