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
43 changes: 43 additions & 0 deletions WhereAreYou/WhereAreYou/Data/CoreLocationRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
// Created by 김성훈 on 7/28/26.
//

import Combine
import CoreLocation

final class CoreLocationRepository: NSObject, LocationRepository {
Comment thread
snughnu marked this conversation as resolved.

private let locationManager = CLLocationManager()
private var completion: ((Result<Coordinate, Error>) -> Void)?

private let authorizationStatusSubject: CurrentValueSubject<LocationPermissionStatus, Never>

enum LocationError: Error {
case permissionDenied
case unableToFetch
}

override init() {
authorizationStatusSubject = CurrentValueSubject(
LocationPermissionStatus(locationManager.authorizationStatus)
)
super.init()
locationManager.delegate = self
}
Expand All @@ -41,9 +47,29 @@ final class CoreLocationRepository: NSObject, LocationRepository {

}

// MARK: - LocationPermissionRepository

extension CoreLocationRepository: LocationPermissionRepository {

var authorizationStatus: LocationPermissionStatus {
LocationPermissionStatus(locationManager.authorizationStatus)
}

var authorizationStatusPublisher: AnyPublisher<LocationPermissionStatus, Never> {
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()
Expand Down Expand Up @@ -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
}
}

}
Original file line number Diff line number Diff line change
@@ -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

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// LocationPermissionRepository.swift
// WhereAreYou
//
// Created by 김성훈 on 8/14/26.
//

import Combine

/// 위치 권한 상태의 조회 및 구독을 담당
protocol LocationPermissionRepository {

/// 현재 위치 권한 상태
var authorizationStatus: LocationPermissionStatus { get }
/// 위치 권한 상태가 변경될 때마다 발행되는 스트림
var authorizationStatusPublisher: AnyPublisher<LocationPermissionStatus, Never> { get }
/// 위치 권한을 요청
func requestAuthorization()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// ObserveLocationPermissionUseCase.swift
// WhereAreYou
//
// Created by 김성훈 on 8/14/26.
//

import Combine

/// 위치 권한 상태의 조회, 구독, 요청을 담당
final class ObserveLocationPermissionUseCase {
Comment thread
snughnu marked this conversation as resolved.

private let repository: LocationPermissionRepository

init(repository: LocationPermissionRepository) {
self.repository = repository
}

/// 현재 위치 권한 상태
var currentStatus: LocationPermissionStatus {
repository.authorizationStatus
}

/// 위치 권한 상태가 변경될 때마다 발행되는 스트림
var statusPublisher: AnyPublisher<LocationPermissionStatus, Never> {
repository.authorizationStatusPublisher
}

/// 위치 권한을 요청
func requestAuthorization() {
repository.requestAuthorization()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ final class MyPageViewController: UIViewController {

private static let cardSpacing: CGFloat = 16

private let viewModel = MyPageViewModel()
private let viewModel = MyPageViewModel(
Comment thread
snughnu marked this conversation as resolved.
observeLocationPermissionUseCase: ObserveLocationPermissionUseCase(
repository: CoreLocationRepository()
)
)

private let logoImageView: UIImageView = {
let imageView = UIImageView(image: .logo)
Expand Down
48 changes: 17 additions & 31 deletions WhereAreYou/WhereAreYou/Presentation/MyPage/MyPageViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyCancellable>()

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()
}

}
Expand Down Expand Up @@ -146,7 +147,7 @@ extension MyPageViewModel {
}

func requestLocationPermission() {
locationManager.requestWhenInUseAuthorization()
observeLocationPermissionUseCase.requestAuthorization()
}

func handlePermissionAction() -> LocationPermissionAction {
Expand All @@ -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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
// Created by 김성훈 on 7/25/26.
//

// MARK: - 위치 권한 화면에서 사용하는 표시 전용 모델

/// 위치 권한 화면에서 사용하는 표시 전용 모델
enum LocationPermissionState: Equatable {

case notDetermined
Expand All @@ -15,4 +14,14 @@ enum LocationPermissionState: Equatable {
case authorizedAlways
case authorizedWhenInUse

nonisolated init(_ status: LocationPermissionStatus) {
Comment thread
snughnu marked this conversation as resolved.
switch status {
case .notDetermined: self = .notDetermined
case .denied: self = .denied
case .restricted: self = .restricted
case .authorizedAlways: self = .authorizedAlways
case .authorizedWhenInUse: self = .authorizedWhenInUse
}
}

}
Loading