From 787b2cb2423f40665663726b3b9a05e78de0b5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Junnos=20=EF=A3=BF?= Date: Wed, 10 Jan 2024 20:32:59 +0900 Subject: [PATCH] =?UTF-8?q?:recycle:=20=EC=97=AC=EC=A0=95=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=20=ED=8C=8C=ED=8E=B8?= =?UTF-8?q?=ED=99=94=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 여러 곳에 파편화되어 있던 여정 시작 / 중단 / 재개 로직을 Home 부분에 통합 --- .../Presentation/HomeViewController.swift | 52 +++++------ .../Home/Presentation/HomeViewModel.swift | 88 ++++++++++++------- .../MapViewController+EventListener.swift | 25 +++--- .../Common/MapViewController.swift | 46 ++++------ .../NavigateMap/NavigateMapViewModel.swift | 31 ------- .../RecordJourneyViewModel.swift | 22 ----- .../Journey/DeleteJourneyResponseDTO.swift | 15 ++++ .../JourneyRepository.swift | 5 +- .../Repository/JourneyRepository.swift | 4 +- 9 files changed, 133 insertions(+), 155 deletions(-) diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift index 4eddc51..bc65e6b 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift @@ -116,12 +116,6 @@ public final class HomeViewController: HomeBottomSheetViewController { self.navigationController?.isNavigationBarHidden = true } - public override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - self.viewModel.trigger(.viewNeedsReloaded) - } - public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -132,10 +126,27 @@ public final class HomeViewController: HomeBottomSheetViewController { // MARK: - Combine Binding private func bind() { - self.viewModel.state.startedJourney + self.viewModel.state.journeyDidStarted .receive(on: DispatchQueue.main) .sink { [weak self] startedJourney in - self?.contentViewController.recordingShouldStart(startedJourney) + self?.contentViewController.clearOverlays() + self?.contentViewController.recordingDidStart(startedJourney) + } + .store(in: &self.cancellables) + + self.viewModel.state.journeyDidResumed + .receive(on: DispatchQueue.main) + .sink { [weak self] resumedJourney in + self?.contentViewController.clearOverlays() + self?.contentViewController.recordingDidResume(resumedJourney) + } + .store(in: &self.cancellables) + + self.viewModel.state.journeyDidCancelled + .receive(on: DispatchQueue.main) + .sink { [weak self] cancelledJourney in + self?.contentViewController.clearOverlays() + self?.contentViewController.recordingDidStop(cancelledJourney) } .store(in: &self.cancellables) @@ -155,7 +166,6 @@ public final class HomeViewController: HomeBottomSheetViewController { self?.hideBottomSheet() } else { self?.showBottomSheet() - self?.contentViewController.recordingShouldStop(isCancelling: false) } self?.updateButtonMode(isRecording: isRecording) } @@ -169,19 +179,15 @@ public final class HomeViewController: HomeBottomSheetViewController { .store(in: &self.cancellables) self.viewModel.state.isRefreshButtonHidden - .removeDuplicates(by: { $0 == $1 }) .combineLatest(self.viewModel.state.isRecording) .receive(on: DispatchQueue.main) .sink { [weak self] isHidden, isRecording in - self?.refreshButton.isHidden = (isHidden && !isRecording) - } - .store(in: &self.cancellables) - - self.viewModel.state.overlaysShouldBeCleared - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.contentViewController.clearOverlays() - self?.contentViewController.clearAnnotations() + guard let self = self else { return } + UIView.transition(with: self.refreshButton, + duration: 0.2, + options: .transitionCrossDissolve) { [weak self] in + self?.refreshButton.isHidden = (isHidden || isRecording) + } } .store(in: &self.cancellables) } @@ -189,14 +195,11 @@ public final class HomeViewController: HomeBottomSheetViewController { // MARK: - Functions private func updateButtonMode(isRecording: Bool) { - UIView.transition(with: startButton, duration: 0.5, + UIView.transition(with: self.view, + duration: 0.5, options: .transitionCrossDissolve, animations: { [weak self] in self?.startButton.isHidden = isRecording - }) - UIView.transition(with: recordJourneyButtonStackView, duration: 0.5, - options: .transitionCrossDissolve, - animations: { [weak self] in self?.recordJourneyButtonStackView.isHidden = !isRecording }) } @@ -236,7 +239,6 @@ extension HomeViewController: RecordJourneyButtonViewDelegate { guard self.viewModel.state.isRecording.value == true else { return } self.viewModel.trigger(.backButtonDidTap) - self.contentViewController.recordingShouldStop(isCancelling: true) } public func spotButtonDidTap(_ button: MSRectButton) { diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift index 0912b8d..86be724 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift @@ -20,18 +20,19 @@ public final class HomeViewModel { public enum Action { case viewNeedsLoaded - case viewNeedsReloaded case startButtonDidTap(Coordinate) case refreshButtonDidTap(visibleCoordinates: (minCoordinate: Coordinate, maxCoordinate: Coordinate)) case backButtonDidTap + case recordingStateDidChange(Bool) case mapViewDidChange } public struct State { // Passthrough - public var startedJourney = PassthroughSubject() + public var journeyDidStarted = PassthroughSubject() + public var journeyDidResumed = PassthroughSubject() + public var journeyDidCancelled = PassthroughSubject() public var visibleJourneys = PassthroughSubject<[Journey], Never>() - public var overlaysShouldBeCleared = PassthroughSubject() // CurrentValue public var isRecording = CurrentValueSubject(false) @@ -68,28 +69,23 @@ public final class HomeViewModel { #endif self.createNewUserWhenFirstLaunch() - case .viewNeedsReloaded: - let isRecording = self.journeyRepository.isRecording - #if DEBUG - MSLogger.make(category: .home).debug("여정 기록 중 여부: \(isRecording)") - #endif - if isRecording { - self.resumeJourney() - } - self.state.isRecording.send(isRecording) + + self.resumeJourneyIfNeeded() case .startButtonDidTap(let coordinate): #if DEBUG - MSLogger.make(category: .home).debug("Start 버튼 탭: \(coordinate)") + MSLogger.make(category: .home).debug("시작 버튼이 탭 되었습니다: \(coordinate)") #endif self.startJourney(at: coordinate) - self.state.isRefreshButtonHidden.send(true) case .refreshButtonDidTap(visibleCoordinates: (let minCoordinate, let maxCoordinate)): self.state.isRefreshButtonHidden.send(true) self.fetchJourneys(minCoordinate: minCoordinate, maxCoordinate: maxCoordinate) case .backButtonDidTap: - self.state.isRecording.send(false) - self.state.isRefreshButtonHidden.send(false) - self.state.overlaysShouldBeCleared.send(true) + #if DEBUG + MSLogger.make(category: .home).debug("취소 버튼이 탭 되었습니다.") + #endif + self.cancelJourney() + case .recordingStateDidChange(let isRecording): + self.state.isRecording.send(isRecording) case .mapViewDidChange: if self.state.isRecording.value == false { self.state.isRefreshButtonHidden.send(false) @@ -124,8 +120,26 @@ private extension HomeViewModel { } } + func fetchJourneys(minCoordinate: Coordinate, maxCoordinate: Coordinate) { + guard let userID = self.userRepository.fetchUUID() else { return } + + Task { + let result = await self.journeyRepository.fetchJourneyList(userID: userID, + minCoordinate: minCoordinate, + maxCoordinate: maxCoordinate) + switch result { + case .success(let journeys): + self.state.visibleJourneys.send(journeys) + case .failure(let error): + MSLogger.make(category: .home).error("\(error)") + } + } + } + func startJourney(at coordinate: Coordinate) { Task { + defer { self.syncRecordingState() } + self.state.isStartButtonLoading.send(true) defer { self.state.isStartButtonLoading.send(false) } @@ -134,36 +148,46 @@ private extension HomeViewModel { let result = await self.journeyRepository.startJourney(at: coordinate, userID: userID) switch result { case .success(let recordingJourney): - self.state.startedJourney.send(recordingJourney) - self.state.isRecording.send(true) + self.state.journeyDidStarted.send(recordingJourney) + self.state.isRefreshButtonHidden.send(true) case .failure(let error): MSLogger.make(category: .home).error("\(error)") } } } - func fetchJourneys(minCoordinate: Coordinate, maxCoordinate: Coordinate) { - guard let userID = self.userRepository.fetchUUID() else { return } + func resumeJourneyIfNeeded() { + defer { self.syncRecordingState() } + + guard let recordingJourney = self.journeyRepository.fetchRecordingJourney() else { + return + } + + self.state.journeyDidResumed.send(recordingJourney) + } + + func cancelJourney() { + guard let userID = self.userRepository.fetchUUID(), + let recordingJourney = self.journeyRepository.fetchRecordingJourney() else { + return + } Task { - let result = await self.journeyRepository.fetchJourneyList(userID: userID, - minCoordinate: minCoordinate, - maxCoordinate: maxCoordinate) + defer { self.syncRecordingState() } + + let result = await self.journeyRepository.deleteJourney(recordingJourney, userID: userID) switch result { - case .success(let journeys): - self.state.visibleJourneys.send(journeys) + case .success(let deletedJourney): + self.state.journeyDidCancelled.send(deletedJourney) case .failure(let error): MSLogger.make(category: .home).error("\(error)") } } } - func resumeJourney() { - guard let recordingJourney = self.journeyRepository.fetchRecordingJourney() else { - return - } - - MSLogger.make(category: .home).debug("Recording Journey: \(recordingJourney)") + func syncRecordingState() { + let isRecording = self.journeyRepository.isRecording + self.state.isRecording.send(isRecording) } } diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift index 3cfd8c4..36644e4 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController+EventListener.swift @@ -5,6 +5,7 @@ // Created by 이창준 on 2023.12.10. // +import CoreLocation import Foundation import MSData @@ -27,7 +28,7 @@ extension MapViewController { extension MapViewController { - public func recordingShouldStart(_ startedJourney: RecordingJourney) { + public func recordingDidStart(_ startedJourney: RecordingJourney) { guard self.viewModel is NavigateMapViewModel else { MSLogger.make(category: .home).error("여정이 시작되어야 하지만 이미 Map에서 RecordJourneyViewModel을 사용하고 있습니다.") return @@ -44,11 +45,11 @@ extension MapViewController { self.locationManager.allowsBackgroundLocationUpdates = true #if DEBUG - MSLogger.make(category: .home).debug("여정 기록이 시작되었습니다.") + MSLogger.make(category: .home).debug("여정 기록이 시작되었습니다: \(startedJourney)") #endif } - public func recordingShouldResume(_ recordedJourney: RecordingJourney) { + public func recordingDidResume(_ recordedJourney: RecordingJourney) { let userRepository = UserRepositoryImplementation() let journeyRepository = JourneyRepositoryImplementation() let recordJourneyViewModel = RecordJourneyViewModel(startedJourney: recordedJourney, @@ -56,24 +57,26 @@ extension MapViewController { journeyRepository: journeyRepository) self.swapViewModel(to: recordJourneyViewModel) + let coordinates = recordedJourney.coordinates.map { + CLLocationCoordinate2D(latitude: $0.latitude, + longitude: $0.longitude) + } + self.drawPolyline(using: coordinates) + self.locationManager.startUpdatingLocation() self.locationManager.allowsBackgroundLocationUpdates = true #if DEBUG - MSLogger.make(category: .home).debug("여정 기록이 재개되었습니다.") + MSLogger.make(category: .home).debug("여정 기록이 재개되었습니다: \(recordedJourney)") #endif } - public func recordingShouldStop(isCancelling: Bool) { - guard let viewModel = self.viewModel as? RecordJourneyViewModel else { + public func recordingDidStop(_ stoppedJourney: RecordingJourney) { + guard self.viewModel is RecordJourneyViewModel else { MSLogger.make(category: .home).error("여정이 종료되어야 하지만 이미 Map에서 NavigateMapViewModel을 사용하고 있습니다.") return } - if isCancelling { - viewModel.trigger(.recordingDidCancelled) - } - let journeyRepository = JourneyRepositoryImplementation() let navigateMapViewModel = NavigateMapViewModel(repository: journeyRepository) self.swapViewModel(to: navigateMapViewModel) @@ -82,7 +85,7 @@ extension MapViewController { self.locationManager.allowsBackgroundLocationUpdates = false #if DEBUG - MSLogger.make(category: .home).debug("여정 기록이 종료되었습니다.") + MSLogger.make(category: .home).debug("여정 기록이 종료되었습니다: \(stoppedJourney)") #endif } diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift index ae5def2..a7bce5b 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift @@ -135,39 +135,26 @@ public final class MapViewController: UIViewController { if let navigateMapViewModel = viewModel as? NavigateMapViewModel { self.bind(navigateMapViewModel) - #if DEBUG MSLogger.make(category: .home).debug("Map에 NavigateMapViewModel을 바인딩 했습니다.") - #endif - - navigateMapViewModel.trigger(.viewNeedsLoaded) + return } if let recordJourneyViewModel = viewModel as? RecordJourneyViewModel { self.bind(recordJourneyViewModel) - #if DEBUG MSLogger.make(category: .home).debug("Map에 RecordJourneyViewModel을 바인딩 했습니다.") - #endif + return } + + MSLogger.make(category: .home).warning("Map에 ViewModel을 바인딩하지 못했습니다.") } private func bind(_ viewModel: NavigateMapViewModel) { viewModel.state.visibleJourneys .receive(on: DispatchQueue.main) .sink { [weak self] journeys in - self?.clearAnnotations() + self?.clearOverlays() self?.addAnnotations(with: journeys) - self?.drawPolyLinesToMap(with: journeys) - } - .store(in: &self.cancellables) - - viewModel.state.recordingJourneyShouldResume - .sink { [weak self] recordingJourney in - self?.recordingShouldResume(recordingJourney) - - let coordinates = recordingJourney.coordinates.map { - CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) - } - self?.drawPolylineToMap(using: coordinates) + self?.drawJourneyListPolylines(with: journeys) } .store(in: &self.cancellables) } @@ -185,7 +172,7 @@ public final class MapViewController: UIViewController { .receive(on: DispatchQueue.main) .sink { [weak self] previousCoordinate, currentCoordinate in let points = [previousCoordinate, currentCoordinate] - self?.drawPolylineToMap(using: points) + self?.drawPolyline(using: points) } .store(in: &self.cancellables) } @@ -237,7 +224,7 @@ public final class MapViewController: UIViewController { // MARK: - Functions: Polyline - private func drawPolyLinesToMap(with journeys: [Journey]) { + func drawJourneyListPolylines(with journeys: [Journey]) { Task { await withTaskGroup(of: Void.self) { group in for journey in journeys { @@ -246,31 +233,30 @@ public final class MapViewController: UIViewController { CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) } - await self.drawPolylineToMap(using: coordinates) + await self.drawPolyline(using: coordinates) } } } } } - private func drawPolylineToMap(using coordinates: [CLLocationCoordinate2D]) { - let polyline = MKPolyline(coordinates: coordinates, - count: coordinates.count) + func drawPolyline(using coordinates: [CLLocationCoordinate2D]) { + let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count) self.mapView.addOverlay(polyline) } // MARK: - Functions - public func clearOverlays() { - let overlays = self.mapView.overlays - self.mapView.removeOverlays(overlays) - } - public func clearAnnotations() { let annotations = self.mapView.annotations self.mapView.removeAnnotations(annotations) } + public func clearOverlays() { + let overlays = self.mapView.overlays + self.mapView.removeOverlays(overlays) + } + } // MARK: - UI Configuration diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift index 5b74670..26d2859 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/NavigateMap/NavigateMapViewModel.swift @@ -6,26 +6,18 @@ // import Combine -import CoreLocation import Foundation -import MSConstants -import MSData import MSDomain import MSLogger -import MSUserDefaults public final class NavigateMapViewModel: MapViewModel { public enum Action { - case viewNeedsLoaded case visibleJourneysDidUpdated(_ visibleJourneys: [Journey]) } public struct State { - // Passthrough - public var recordingJourneyShouldResume = PassthroughSubject() - // CurrentValue public var visibleJourneys = CurrentValueSubject<[Journey], Never>([]) } @@ -46,32 +38,9 @@ public final class NavigateMapViewModel: MapViewModel { public func trigger(_ action: Action) { switch action { - case .viewNeedsLoaded: - if let recordingJourney = self.fetchRecordingJourneyIfNeeded() { - #if DEBUG - MSLogger.make(category: .navigateMap) - .debug("기록중이던 여정이 발견되었습니다: \(recordingJourney)") - #endif - self.state.recordingJourneyShouldResume.send(recordingJourney) - } case .visibleJourneysDidUpdated(let visibleJourneys): self.state.visibleJourneys.send(visibleJourneys) } } } - -// MARK: - Private Functions - -private extension NavigateMapViewModel { - - /// 앱 종료 전 진행중이던 여정 기록이 남아있는지 확인합니다. - /// 진행 중이던 여정 기록이 있다면 해당 데이터를 불러옵니다. - /// - Returns: 진행 중이던 여정 기록. 없다면 `nil`을 반환합니다. - func fetchRecordingJourneyIfNeeded() -> RecordingJourney? { - guard self.journeyRepository.isRecording else { return nil } - - return self.journeyRepository.fetchRecordingJourney() - } - -} diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift index 1d4e0f7..34b5039 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift @@ -9,17 +9,14 @@ import Combine import CoreLocation import Foundation -import MSData import MSDomain import MSLogger public final class RecordJourneyViewModel: MapViewModel { public enum Action { - case viewNeedsLoaded case locationDidUpdated(CLLocationCoordinate2D) case locationsShouldRecorded([CLLocationCoordinate2D]) - case recordingDidCancelled } public struct State { @@ -50,10 +47,6 @@ public final class RecordJourneyViewModel: MapViewModel { public func trigger(_ action: Action) { switch action { - case .viewNeedsLoaded: - #if DEBUG - MSLogger.make(category: .home).debug("View Did load.") - #endif case .locationDidUpdated(let coordinate): let previousCoordinate = self.state.currentCoordinate.value self.state.previousCoordinate.send(previousCoordinate) @@ -71,21 +64,6 @@ public final class RecordJourneyViewModel: MapViewModel { MSLogger.make(category: .home).error("\(error)") } } - case .recordingDidCancelled: - Task { - guard let userID = self.userRepository.fetchUUID() else { return } - - let recordingJourney = self.state.recordingJourney.value - let result = await self.journeyRepository.deleteJourney(recordingJourney, userID: userID) - switch result { - case .success(let cancelledJourney): - #if DEBUG - MSLogger.make(category: .home).debug("여정이 취소 되었습니다: \(cancelledJourney)") - #endif - case .failure(let error): - MSLogger.make(category: .home).error("\(error)") - } - } } } diff --git a/iOS/MSData/Sources/MSData/DTO/Response/Journey/DeleteJourneyResponseDTO.swift b/iOS/MSData/Sources/MSData/DTO/Response/Journey/DeleteJourneyResponseDTO.swift index 52d942f..aee910e 100644 --- a/iOS/MSData/Sources/MSData/DTO/Response/Journey/DeleteJourneyResponseDTO.swift +++ b/iOS/MSData/Sources/MSData/DTO/Response/Journey/DeleteJourneyResponseDTO.swift @@ -42,3 +42,18 @@ extension DeleteJourneyResponseDTO: Decodable { } } + +// MARK: - Domain Mapping + +import MSDomain + +extension DeleteJourneyResponseDTO { + + public func toDomain() -> RecordingJourney { + return RecordingJourney(id: self.id, + startTimestamp: self.metadata.startTimestamp, + spots: self.spots.map { $0.toDomain() }, + coordinates: self.coordinates.map { $0.toDomain() }) + } + +} diff --git a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift index 7d3c8c3..ffe90ef 100644 --- a/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift +++ b/iOS/MSData/Sources/MSData/RepositoryImplementation/JourneyRepository.swift @@ -149,7 +149,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { } public mutating func deleteJourney(_ recordingJourney: RecordingJourney, - userID: UUID) async -> Result { + userID: UUID) async -> Result { let requestDTO = DeleteJourneyRequestDTO(userID: userID, journeyID: recordingJourney.id) let router = JourneyRouter.deleteJourney(dto: requestDTO) let result = await self.networking.request(DeleteJourneyResponseDTO.self, router: router) @@ -157,7 +157,8 @@ public struct JourneyRepositoryImplementation: JourneyRepository { case .success(let responseDTO): do { try self.recordingJourney.finish() - return .success(responseDTO.id) + let deletedJourney = responseDTO.toDomain() + return .success(deletedJourney) } catch { return .failure(error) } diff --git a/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift b/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift index 8399375..a5b037a 100644 --- a/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift +++ b/iOS/MSDomain/Sources/MSDomain/Repository/JourneyRepository.swift @@ -17,8 +17,8 @@ public protocol JourneyRepository { maxCoordinate: Coordinate) async -> Result<[Journey], Error> func fetchRecordingJourney() -> RecordingJourney? mutating func startJourney(at coordinate: Coordinate, userID: UUID) async -> Result - mutating func endJourney(_ journey: Journey) async -> Result func recordJourney(journeyID: String, at coordinates: [Coordinate]) async -> Result - mutating func deleteJourney(_ journey: RecordingJourney, userID: UUID) async -> Result + mutating func endJourney(_ journey: Journey) async -> Result + mutating func deleteJourney(_ journey: RecordingJourney, userID: UUID) async -> Result }