From 586e30740dce3c38ba8a19b227176bf638ca99c1 Mon Sep 17 00:00:00 2001 From: snughnu Date: Thu, 13 Aug 2026 23:59:34 +0900 Subject: [PATCH 1/5] =?UTF-8?q?FEAT:=20=EC=9C=84=EC=B9=98=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=83=81=ED=83=9C=20Domain=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0(LocationPermissionStatus)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Entity/LocationPermissionStatus.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Domain/Entity/LocationPermissionStatus.swift 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 + +} From b2108af7af5ccfb5ac5f4f54ed7109ee196a09b5 Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 14 Aug 2026 00:01:56 +0900 Subject: [PATCH 2/5] =?UTF-8?q?FEAT:=20=EC=9C=84=EC=B9=98=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?Domain=20=ED=94=84=EB=A1=9C=ED=86=A0=EC=BD=9C(LocationPermissio?= =?UTF-8?q?nRepository)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LocationPermissionRepository.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Domain/Repository/LocationPermissionRepository.swift 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() + +} From 0c0fde8fbe1f554847ac163511fe007b9e9e23ad Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 14 Aug 2026 00:04:28 +0900 Subject: [PATCH 3/5] =?UTF-8?q?FEAT:=20CoreLocationRepository=EA=B0=80=20L?= =?UTF-8?q?ocationPermissionRepository=EB=A5=BC=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Data/CoreLocationRepository.swift | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) 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 + } + } + +} From 79868d1336fab020f392809ecc489e712178ac61 Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 14 Aug 2026 00:08:01 +0900 Subject: [PATCH 4/5] =?UTF-8?q?FEAT:=20=EC=9C=84=EC=B9=98=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=20=EC=A1=B0=ED=9A=8C/=EA=B5=AC=EB=8F=85/=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20UseCase(ObserveLocationPermissionUseCase)=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ObserveLocationPermissionUseCase.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 WhereAreYou/WhereAreYou/Domain/UseCase/ObserveLocationPermissionUseCase.swift 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() + } + +} From a71fc8642b1ac89a8e7cc93778c5b5b4602deef3 Mon Sep 17 00:00:00 2001 From: snughnu Date: Fri, 14 Aug 2026 00:24:31 +0900 Subject: [PATCH 5/5] =?UTF-8?q?REFACTOR:=20MyPageViewModel=EC=9D=98=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EA=B6=8C=ED=95=9C=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=EB=A5=BC=20CLLocationManager=20=EC=A7=81=EC=A0=91=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=97=90=EC=84=9C=20ObserveLocationPermissionUseCase?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MyPage/MyPageViewController.swift | 6 ++- .../Presentation/MyPage/MyPageViewModel.swift | 48 +++++++------------ .../LocationPermissionState.swift | 13 ++++- 3 files changed, 33 insertions(+), 34 deletions(-) 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 + } + } + }