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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ xcodebuild -workspace Atcha.xcworkspace -scheme AtchaV2 -configuration Debug \
# 모듈 테스트 (Swift Testing 기반)
xcodebuild -workspace Atcha.xcworkspace -scheme HomeFeature \
-destination 'platform=iOS Simulator,name=iPhone 17' test
# 단일 테스트: -only-testing:HomeFeatureTests/HomeViewModelTests/viewDidLoad_success_transitionsLoadingToLoaded
# 단일 테스트: -only-testing:HomeFeatureTests/HomeViewModelTests/viewDidLoad_locationSuccess_showsReverseGeocodedName

# 의존 그래프 확인 (graph.dot 생성, gitignore됨)
tuist graph --format dot --no-open
Expand All @@ -47,8 +47,10 @@ xcodebuild -workspace Atcha.xcworkspace -scheme Atcha-Dev -configuration Debug \
## 아키텍처 (AtchaV2 — uFeatures + 클린아키텍처)

```
AtchaV2(앱, 조합 루트) ─► HomeFeature ─► {HomeFeatureInterface, Domain, DesignSystem, CoreCoordinator, SnapKit}
└─► AtchaData ─► {Domain, CoreNetwork}
AtchaV2(앱, 조합 루트: 어댑터·스플래시) ─► HomeFeature ─► {HomeFeatureInterface, SearchFeatureInterface, Domain, DesignSystem, CoreCoordinator, SnapKit}
├─► SearchFeature ─► {SearchFeatureInterface, Domain, DesignSystem, CoreCoordinator, SnapKit}
├─► AtchaData ─► {Domain, CoreNetwork, CoreStorage}
└─► CoreAuth ─► {CoreNetwork, CoreStorage}
```

의존 규칙(위반 금지, `tuist graph`로 검증 가능):
Expand Down
3 changes: 3 additions & 0 deletions Projects/App/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ let appTarget = Target.target(
"CFBundleDisplayName": "앗차",
"UILaunchScreen": [:],
"NSAlarmKitUsageDescription": "막차 시간에 맞춰 알람을 울리기 위해 권한이 필요합니다.",
"NSLocationWhenInUseUsageDescription": "현재 위치를 출발지로 사용하기 위해 위치 정보 접근 권한이 필요합니다.",
"UIApplicationSceneManifest": [
"UIApplicationSupportsMultipleScenes": false,
"UISceneConfigurations": [
Expand All @@ -30,6 +31,8 @@ let appTarget = Target.target(
dependencies: [
.project(target: "HomeFeature", path: "../Feature/Home"),
.project(target: "HomeFeatureInterface", path: "../Feature/Home"),
.project(target: "SearchFeature", path: "../Feature/Search"),
.project(target: "SearchFeatureInterface", path: "../Feature/Search"),
.project(target: "Domain", path: "../Domain"),
.project(target: "AtchaData", path: "../Data"),
.project(target: "CoreNetwork", path: "../Core/Network"),
Expand Down
34 changes: 34 additions & 0 deletions Projects/App/Sources/Adapters/CoreLocationServiceAdapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import CoreLocation
import Domain

/// CoreLocation → Domain `LocationService` 어댑터. CoreLocation을 아는 곳은 여기뿐.
///
/// `CLLocationUpdate.liveUpdates()`는 권한이 notDetermined이면 WhenInUse 요청을
/// 스스로 띄우므로(plist 키 필요) delegate/continuation 없이 one-shot 조회가 된다.
/// App 모듈 기본 격리가 MainActor라 이 클래스는 암시적 Sendable — 프로토콜의
/// nonisolated async 요구사항은 격리 witness로 충족된다.
final class CoreLocationServiceAdapter: LocationService {
func currentLocation() async throws -> Coordinate {
do {
for try await update in CLLocationUpdate.liveUpdates() {
if update.authorizationDenied
|| update.authorizationDeniedGlobally
|| update.authorizationRestricted {
throw LocationError.permissionDenied
}
if let location = update.location {
return Coordinate(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude
)
}
// 권한 요청 진행 중 / 일시적 위치 불가 → 다음 업데이트를 기다린다.
}
} catch let error as LocationError {
throw error
} catch {
throw LocationError.unavailable
}
throw LocationError.unavailable
}
}
9 changes: 9 additions & 0 deletions Projects/App/Sources/Adapters/NoopAlarmScheduler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Domain
import Foundation

/// Phase 6 임시 어댑터: 서버 알람 등록은 실동작, 로컬 스케줄은 no-op.
/// Phase 7에서 CoreAlarm(AlarmKit) 기반 어댑터로 교체된다.
struct NoopAlarmScheduler: AlarmScheduler {
func replaceAlarm(id: String, fireDate: Date, title: String) async throws {}
func cancelAlarm() async {}
}
31 changes: 28 additions & 3 deletions Projects/App/Sources/AppDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import CoreStorage
import Domain
import HomeFeature
import HomeFeatureInterface
import SearchFeature
import SearchFeatureInterface

/// Composition root — the only place that sees concrete Data/Network types.
/// Presentation modules depend on Domain protocols only.
Expand Down Expand Up @@ -33,8 +35,31 @@ final class AppDIContainer {
}

func makeHomeDIContainer() -> any HomeCoordinatorBuildable {
let repository: any HomeRepository = HomeRepositoryImpl(networkClient: networkClient)
let fetchHome: any FetchHomeUseCase = DefaultFetchHomeUseCase(repository: repository)
return HomeDIContainer(fetchHomeUseCase: fetchHome)
let placeRepository = PlaceRepositoryImpl(networkClient: networkClient)
let lastRouteRepository = LastRouteRepositoryImpl(networkClient: networkClient)
let alarmRepository = AlarmRepositoryImpl(networkClient: networkClient)
let recentSearchRepository = RecentSearchRepositoryImpl(store: UserDefaultsKeyValueStore())

// 디바이스 포트 어댑터. AlarmScheduler는 Phase 7에서 CoreAlarm 기반으로 교체.
let locationService = CoreLocationServiceAdapter()
let getCurrentLocation: any GetCurrentLocationUseCase =
DefaultGetCurrentLocationUseCase(locationService: locationService)

let searchContainer = SearchDIContainer(
searchPlacesUseCase: DefaultSearchPlacesUseCase(repository: placeRepository),
searchLastRoutesUseCase: DefaultSearchLastRoutesUseCase(repository: lastRouteRepository),
recentSearchesUseCase: DefaultRecentSearchesUseCase(repository: recentSearchRepository),
getCurrentLocationUseCase: getCurrentLocation
)

return HomeDIContainer(
getCurrentLocationUseCase: getCurrentLocation,
reverseGeocodeUseCase: DefaultReverseGeocodeUseCase(repository: placeRepository),
registerAlarmUseCase: DefaultRegisterAlarmUseCase(
repository: alarmRepository,
scheduler: NoopAlarmScheduler()
),
searchCoordinatorBuildable: searchContainer
)
}
}
10 changes: 5 additions & 5 deletions Projects/App/Sources/SplashViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,17 @@ final class SplashViewController: UIViewController {
}

private func configureUI() {
view.backgroundColor = DSColor.background
view.backgroundColor = DSColor.Background.base

logoLabel.text = "앗차"
logoLabel.font = DSFont.title(34)
logoLabel.textColor = DSColor.accent
logoLabel.font = DSFont.pretendard(.bold, size: 34)
logoLabel.textColor = DSColor.Accent.default

activityIndicator.hidesWhenStopped = true

messageLabel.text = "네트워크 연결을 확인해주세요"
messageLabel.font = DSFont.body()
messageLabel.textColor = DSColor.textPrimary
messageLabel.font = DSTypography.body1.font
messageLabel.textColor = DSColor.Text.primary
messageLabel.textAlignment = .center

retryButton.addAction(
Expand Down
11 changes: 0 additions & 11 deletions Projects/Data/Sources/DTO/HomeSummaryRequestDTO.swift

This file was deleted.

11 changes: 0 additions & 11 deletions Projects/Data/Sources/DTO/HomeSummaryResponseDTO.swift

This file was deleted.

25 changes: 0 additions & 25 deletions Projects/Data/Sources/Network/HomeEndpoint.swift

This file was deleted.

17 changes: 0 additions & 17 deletions Projects/Data/Sources/Repositories/HomeRepositoryImpl.swift

This file was deleted.

13 changes: 0 additions & 13 deletions Projects/Data/Tests/HomeSummaryResponseDTOTests.swift

This file was deleted.

9 changes: 0 additions & 9 deletions Projects/DesignSystem/Sources/Foundation/DSColor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,4 @@ public enum DSColor {
public static var danger: UIColor { DSPalette.red400 }
public static var urgent: UIColor { DSPalette.red600 }
}

// Flat aliases kept so HomeFeature compiles untouched until the Phase 6
// home overhaul migrates it to the semantic tokens.
@available(*, deprecated, renamed: "Accent.default")
public static var accent: UIColor { Accent.default }
@available(*, deprecated, renamed: "Background.base")
public static var background: UIColor { Background.base }
@available(*, deprecated, renamed: "Text.primary")
public static var textPrimary: UIColor { Text.primary }
}
15 changes: 0 additions & 15 deletions Projects/DesignSystem/Sources/Foundation/DSFont.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,19 +52,4 @@ public enum DSFont {

private static var didAttemptRegistration = false
private static var registrationSucceeded = false

@available(*, deprecated, message: "Use DSTypography presets")
public static func title(_ size: CGFloat = 22) -> UIFont {
pretendard(.bold, size: size)
}

@available(*, deprecated, message: "Use DSTypography presets")
public static func body(_ size: CGFloat = 16) -> UIFont {
pretendard(.regular, size: size)
}

@available(*, deprecated, message: "Use DSTypography presets")
public static func caption(_ size: CGFloat = 12) -> UIFont {
pretendard(.medium, size: size)
}
}
7 changes: 0 additions & 7 deletions Projects/DesignSystem/Tests/DSFontTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,4 @@ struct DSFontTests {
let resolvable = UIFont(name: "Pretendard-Regular", size: 17) != nil
#expect(registered == resolvable)
}

@Test
func deprecatedHelpersKeepDefaultSizes() {
#expect(DSFont.title().pointSize == 22)
#expect(DSFont.body().pointSize == 16)
#expect(DSFont.caption().pointSize == 12)
}
}
7 changes: 0 additions & 7 deletions Projects/DesignSystem/Tests/DesignTokenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,4 @@ struct DesignTokenTests {
#expect(colorsMatch(DSColor.State.danger, DSPalette.red400))
#expect(colorsMatch(DSColor.State.urgent, DSPalette.red600))
}

@Test
func flatAliasesForwardToSemanticTokens() {
#expect(colorsMatch(DSColor.accent, DSColor.Accent.default))
#expect(colorsMatch(DSColor.background, DSColor.Background.base))
#expect(colorsMatch(DSColor.textPrimary, DSColor.Text.primary))
}
}
11 changes: 0 additions & 11 deletions Projects/Domain/Sources/Entities/HomeSummary.swift

This file was deleted.

5 changes: 5 additions & 0 deletions Projects/Domain/Sources/Entities/LocationError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// 위치 조회 실패 사유 — 권한 거부를 구분해야 Feature가 "검색 유도 + 설정 이동" UX로 분기할 수 있다.
public enum LocationError: Error, Equatable, Sendable {
case permissionDenied
case unavailable
}
3 changes: 0 additions & 3 deletions Projects/Domain/Sources/Interfaces/HomeRepository.swift

This file was deleted.

15 changes: 0 additions & 15 deletions Projects/Domain/Sources/UseCases/FetchHomeUseCase.swift

This file was deleted.

15 changes: 15 additions & 0 deletions Projects/Domain/Sources/UseCases/ReverseGeocodeUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
public protocol ReverseGeocodeUseCase: Sendable {
func execute(coordinate: Coordinate) async throws -> Place
}

public struct DefaultReverseGeocodeUseCase: ReverseGeocodeUseCase {
private let repository: any PlaceRepository

public init(repository: any PlaceRepository) {
self.repository = repository
}

public func execute(coordinate: Coordinate) async throws -> Place {
try await repository.reverseGeocode(coordinate)
}
}
17 changes: 0 additions & 17 deletions Projects/Domain/Tests/DefaultFetchHomeUseCaseTests.swift

This file was deleted.

Loading
Loading