diff --git a/WhereAreYou/WhereAreYou/Data/CoreLocationRepository.swift b/WhereAreYou/WhereAreYou/Data/CoreLocationRepository.swift index 89d758a..dab071d 100644 --- a/WhereAreYou/WhereAreYou/Data/CoreLocationRepository.swift +++ b/WhereAreYou/WhereAreYou/Data/CoreLocationRepository.swift @@ -5,6 +5,7 @@ // Created by 김성훈 on 7/28/26. // +import Combine import CoreLocation final class CoreLocationRepository: NSObject, LocationRepository { @@ -12,12 +13,17 @@ final class CoreLocationRepository: NSObject, LocationRepository { private let locationManager = CLLocationManager() private var completion: ((Result) -> Void)? + private let authorizationStatusSubject: CurrentValueSubject + enum LocationError: Error { case permissionDenied case unableToFetch } override init() { + authorizationStatusSubject = CurrentValueSubject( + LocationPermissionStatus(locationManager.authorizationStatus) + ) super.init() locationManager.delegate = self } @@ -41,9 +47,29 @@ final class CoreLocationRepository: NSObject, LocationRepository { } +// MARK: - LocationPermissionRepository + +extension CoreLocationRepository: LocationPermissionRepository { + + var authorizationStatus: LocationPermissionStatus { + LocationPermissionStatus(locationManager.authorizationStatus) + } + + var authorizationStatusPublisher: AnyPublisher { + authorizationStatusSubject.eraseToAnyPublisher() + } + + func requestAuthorization() { + locationManager.requestWhenInUseAuthorization() + } + +} + extension CoreLocationRepository: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + authorizationStatusSubject.send(LocationPermissionStatus(manager.authorizationStatus)) + switch manager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: manager.requestLocation() @@ -75,3 +101,20 @@ extension CoreLocationRepository: CLLocationManagerDelegate { } } + +// MARK: - CLAuthorizationStatus 변환 + +private extension LocationPermissionStatus { + + init(_ status: CLAuthorizationStatus) { + switch status { + case .notDetermined: self = .notDetermined + case .denied: self = .denied + case .restricted: self = .restricted + case .authorizedAlways: self = .authorizedAlways + case .authorizedWhenInUse: self = .authorizedWhenInUse + @unknown default: self = .notDetermined + } + } + +} diff --git a/WhereAreYou/WhereAreYou/Domain/Entity/LocationPermissionStatus.swift b/WhereAreYou/WhereAreYou/Domain/Entity/LocationPermissionStatus.swift new file mode 100644 index 0000000..4b73bd1 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/Entity/LocationPermissionStatus.swift @@ -0,0 +1,22 @@ +// +// LocationPermissionStatus.swift +// WhereAreYou +// +// Created by 김성훈 on 8/13/26. +// + +/// 위치 권한 상태 +enum LocationPermissionStatus { + + /// 사용자가 아직 위치 권한을 허용/거부하지 않은 상태 (최초 상태) + case notDetermined + /// 사용자가 위치 권한을 명시적으로 거부한 상태 (설정 앱에서 직접 허용 필요) + case denied + /// 자녀 보호 기능 등 기기 정책으로 인해 위치 권한 자체를 사용할 수 없는 상태 (앱에서 변경 불가) + case restricted + /// 앱을 사용하지 않을 때도 항상 위치 접근이 허용된 상태 + case authorizedAlways + /// 앱을 사용하는 동안에만 위치 접근이 허용된 상태 + case authorizedWhenInUse + +} diff --git a/WhereAreYou/WhereAreYou/Domain/Repository/LocationPermissionRepository.swift b/WhereAreYou/WhereAreYou/Domain/Repository/LocationPermissionRepository.swift new file mode 100644 index 0000000..c777263 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/Repository/LocationPermissionRepository.swift @@ -0,0 +1,20 @@ +// +// LocationPermissionRepository.swift +// WhereAreYou +// +// Created by 김성훈 on 8/14/26. +// + +import Combine + +/// 위치 권한 상태의 조회 및 구독을 담당 +protocol LocationPermissionRepository { + + /// 현재 위치 권한 상태 + var authorizationStatus: LocationPermissionStatus { get } + /// 위치 권한 상태가 변경될 때마다 발행되는 스트림 + var authorizationStatusPublisher: AnyPublisher { get } + /// 위치 권한을 요청 + func requestAuthorization() + +} diff --git a/WhereAreYou/WhereAreYou/Domain/UseCase/ObserveLocationPermissionUseCase.swift b/WhereAreYou/WhereAreYou/Domain/UseCase/ObserveLocationPermissionUseCase.swift new file mode 100644 index 0000000..1b0e557 --- /dev/null +++ b/WhereAreYou/WhereAreYou/Domain/UseCase/ObserveLocationPermissionUseCase.swift @@ -0,0 +1,34 @@ +// +// ObserveLocationPermissionUseCase.swift +// WhereAreYou +// +// Created by 김성훈 on 8/14/26. +// + +import Combine + +/// 위치 권한 상태의 조회, 구독, 요청을 담당 +final class ObserveLocationPermissionUseCase { + + private let repository: LocationPermissionRepository + + init(repository: LocationPermissionRepository) { + self.repository = repository + } + + /// 현재 위치 권한 상태 + var currentStatus: LocationPermissionStatus { + repository.authorizationStatus + } + + /// 위치 권한 상태가 변경될 때마다 발행되는 스트림 + var statusPublisher: AnyPublisher { + repository.authorizationStatusPublisher + } + + /// 위치 권한을 요청 + func requestAuthorization() { + repository.requestAuthorization() + } + +} diff --git a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewController.swift b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewController.swift index 409e910..b361262 100644 --- a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewController.swift +++ b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewController.swift @@ -11,7 +11,11 @@ final class MyPageViewController: UIViewController { private static let cardSpacing: CGFloat = 16 - private let viewModel = MyPageViewModel() + private let viewModel = MyPageViewModel( + observeLocationPermissionUseCase: ObserveLocationPermissionUseCase( + repository: CoreLocationRepository() + ) + ) private let logoImageView: UIImageView = { let imageView = UIImageView(image: .logo) diff --git a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift index 9ac04e2..561062f 100644 --- a/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift +++ b/WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift @@ -6,30 +6,31 @@ // import Combine -import CoreLocation +import Foundation // MARK: - 마이페이지 화면(탭)의 데이터 // - profile: 프로필 카드에 표시할 사용자 정보 // - appointmentNotifications: 약속별 알림 화면에서 쓸 약속 목록 -// - locationPermissionState: CLLocationManager의 실제 권한 상태를 Presentation 타입으로 변환해 발행 -// (지금은 이 ViewModel이 CLLocationManager와 변환 로직을 함께 들고 있지만, -// 추후 Data 계층(Repository)으로 위치 권한 조회를 옮길 때 이 변환도 함께 옮겨짐을 예상) +// - locationPermissionState: 위치 권한 상태를 표시용 타입으로 변환해 발행 +// (실제 CoreLocation 접근은 Data 계층이 담당하며, ObserveLocationPermissionUseCase를 통해 전달받음) -final class MyPageViewModel: NSObject { +final class MyPageViewModel { private(set) var profile: MyPageProfile private(set) var appointmentNotifications: [AppointmentListItem] = [] - @Published private(set) var locationPermissionState: LocationPermissionState = .notDetermined + @Published private(set) var locationPermissionState: LocationPermissionState - private let locationManager = CLLocationManager() + private let observeLocationPermissionUseCase: ObserveLocationPermissionUseCase + private var cancellables = Set() - init(profile: MyPageProfile? = nil) { + init(profile: MyPageProfile? = nil, observeLocationPermissionUseCase: ObserveLocationPermissionUseCase) { self.profile = profile ?? Self.makeDummyProfile() - super.init() - locationManager.delegate = self - locationPermissionState = Self.convert(locationManager.authorizationStatus) + self.observeLocationPermissionUseCase = observeLocationPermissionUseCase + self.locationPermissionState = LocationPermissionState(observeLocationPermissionUseCase.currentStatus) + loadDummyAppointmentNotifications() + bindLocationPermission() } } @@ -146,7 +147,7 @@ extension MyPageViewModel { } func requestLocationPermission() { - locationManager.requestWhenInUseAuthorization() + observeLocationPermissionUseCase.requestAuthorization() } func handlePermissionAction() -> LocationPermissionAction { @@ -157,25 +158,10 @@ extension MyPageViewModel { return .shouldOpenSettings } - static func convert(_ status: CLAuthorizationStatus) -> LocationPermissionState { - switch status { - case .notDetermined: return .notDetermined - case .denied: return .denied - case .restricted: return .restricted - case .authorizedAlways: return .authorizedAlways - case .authorizedWhenInUse: return .authorizedWhenInUse - @unknown default: return .notDetermined - } - } - -} - -// MARK: - CLLocationManagerDelegate - -extension MyPageViewModel: CLLocationManagerDelegate { - - func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { - locationPermissionState = Self.convert(manager.authorizationStatus) + private func bindLocationPermission() { + observeLocationPermissionUseCase.statusPublisher + .map(LocationPermissionState.init) + .assign(to: &$locationPermissionState) } } diff --git a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/LocationPermissionState.swift b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/LocationPermissionState.swift index 7838468..84d73f5 100644 --- a/WhereAreYou/WhereAreYou/Presentation/PresentationModel/LocationPermissionState.swift +++ b/WhereAreYou/WhereAreYou/Presentation/PresentationModel/LocationPermissionState.swift @@ -5,8 +5,7 @@ // Created by 김성훈 on 7/25/26. // -// MARK: - 위치 권한 화면에서 사용하는 표시 전용 모델 - +/// 위치 권한 화면에서 사용하는 표시 전용 모델 enum LocationPermissionState: Equatable { case notDetermined @@ -15,4 +14,14 @@ enum LocationPermissionState: Equatable { case authorizedAlways case authorizedWhenInUse + nonisolated init(_ status: LocationPermissionStatus) { + switch status { + case .notDetermined: self = .notDetermined + case .denied: self = .denied + case .restricted: self = .restricted + case .authorizedAlways: self = .authorizedAlways + case .authorizedWhenInUse: self = .authorizedWhenInUse + } + } + }