diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift index 1fe20e4..f1e1d12 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewController.swift @@ -116,6 +116,12 @@ public final class HomeViewController: HomeBottomSheetViewController { self.navigationController?.isNavigationBarHidden = true } + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.viewModel.trigger(.viewNeedsReloaded) + } + // MARK: - Combine Binding private func bind() { diff --git a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift index 3d4c266..9518e06 100644 --- a/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift +++ b/iOS/Features/Home/Sources/Home/Presentation/HomeViewModel.swift @@ -22,6 +22,7 @@ public final class HomeViewModel { public enum Action { case viewNeedsLoaded + case viewNeedsReloaded case startButtonDidTap(Coordinate) case refreshButtonDidTap(visibleCoordinates: (minCoordinate: Coordinate, maxCoordinate: Coordinate)) case backButtonDidTap @@ -68,15 +69,14 @@ public final class HomeViewModel { switch action { case .viewNeedsLoaded: #if DEBUG - self.isFirstLaunch = true - try? self.keychain.deleteAll() - #endif let firstLaunchMessage = self.isFirstLaunch ? "앱이 처음 실행되었습니다." : "앱 첫 실행이 아닙니다." - MSLogger.make(category: .userDefaults).log("\(firstLaunchMessage)") + MSLogger.make(category: .userDefaults).debug("\(firstLaunchMessage)") + #endif - if self.isFirstLaunch { - self.createNewUser() - } + self.createNewUserWhenFirstLaunch() + case .viewNeedsReloaded: + let isRecording = self.journeyRepository.fetchIsRecording() + self.state.isRecording.send(isRecording) case .startButtonDidTap(let coordinate): #if DEBUG MSLogger.make(category: .home).debug("Start 버튼 탭: \(coordinate)") @@ -103,7 +103,7 @@ public final class HomeViewModel { private extension HomeViewModel { - func createNewUser() { + func createNewUserWhenFirstLaunch() { guard self.isFirstLaunch else { return } Task { @@ -125,10 +125,8 @@ private extension HomeViewModel { self.state.isStartButtonLoading.send(true) defer { self.state.isStartButtonLoading.send(false) } - let userID = try self.userRepository.fetchUUID() - #if DEBUG - MSLogger.make(category: .home).debug("유저 ID 조회 성공: \(userID)") - #endif + guard let userID = self.userRepository.fetchUUID() else { return } + let result = await self.journeyRepository.startJourney(at: coordinate, userID: userID) switch result { case .success(let recordingJourney): @@ -141,7 +139,7 @@ private extension HomeViewModel { } func fetchJourneys(minCoordinate: Coordinate, maxCoordinate: Coordinate) { - guard let userID = try? self.userRepository.fetchUUID() else { return } + guard let userID = self.userRepository.fetchUUID() else { return } Task { let result = await self.journeyRepository.fetchJourneyList(userID: userID, diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift index 7d54e54..26ea827 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/Common/MapViewController.swift @@ -317,7 +317,9 @@ extension MapViewController: CLLocationManagerDelegate { let coordinate2D = CLLocationCoordinate2D(latitude: newCurrentLocation.coordinate.latitude, longitude: newCurrentLocation.coordinate.longitude) + recordJourneyViewModel.trigger(.locationDidUpdated(coordinate2D)) + recordJourneyViewModel.trigger(.locationsShouldRecorded([coordinate2D])) } private func handleAuthorizationChange(_ manager: CLLocationManager) { diff --git a/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift b/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift index 54d5ebc..1d4e0f7 100644 --- a/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift +++ b/iOS/Features/Home/Sources/NavigateMap/Presentation/RecordJourney/RecordJourneyViewModel.swift @@ -73,7 +73,8 @@ public final class RecordJourneyViewModel: MapViewModel { } case .recordingDidCancelled: Task { - let userID = try self.userRepository.fetchUUID() + 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 { diff --git a/iOS/Features/JourneyList/Sources/JourneyList/Presentation/VIew/MSCollectionView.swift b/iOS/Features/JourneyList/Sources/JourneyList/Presentation/VIew/MSCollectionView.swift index f3dbe88..20b5798 100644 --- a/iOS/Features/JourneyList/Sources/JourneyList/Presentation/VIew/MSCollectionView.swift +++ b/iOS/Features/JourneyList/Sources/JourneyList/Presentation/VIew/MSCollectionView.swift @@ -11,9 +11,12 @@ final class MSCollectionView: UICollectionView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // Cell + let headers = self.visibleSupplementaryViews(ofKind: UICollectionView.elementKindSectionHeader) let cells = self.visibleCells for cell in cells where cell.frame.contains(point) { - return super.hitTest(point, with: event) + for header in headers where !header.frame.contains(point) { + return super.hitTest(point, with: event) + } } return nil diff --git a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift index 437a2d5..e6b21c2 100644 --- a/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift +++ b/iOS/Features/SaveJourney/Sources/SaveJourney/Presentation/SaveJourneyViewModel.swift @@ -111,6 +111,7 @@ private extension SaveJourneyViewModel { #if DEBUG MSLogger.make(category: .saveJourney).log("\(journeyID)가 저장되었습니다.") #endif + self.state.endJourneySucceed.send(journey) case .failure(let error): MSLogger.make(category: .saveJourney).error("\(error)") } diff --git a/iOS/MSCoreKit/Sources/MSKeychainStorage/MSKeychainStorage.swift b/iOS/MSCoreKit/Sources/MSKeychainStorage/MSKeychainStorage.swift index 70eda35..619c1fb 100644 --- a/iOS/MSCoreKit/Sources/MSKeychainStorage/MSKeychainStorage.swift +++ b/iOS/MSCoreKit/Sources/MSKeychainStorage/MSKeychainStorage.swift @@ -68,7 +68,7 @@ public struct MSKeychainStorage { /// Keychain에 저장된 모든 데이터를 삭제합니다. public func deleteAll() throws { - for account in Accounts.allCases { + for account in Accounts.allCases where try self.exists(account: account.rawValue) { try self.delete(account: account.rawValue) } } diff --git a/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift b/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift index 63165fb..4c54136 100644 --- a/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift +++ b/iOS/MSCoreKit/Sources/MSPersistentStorage/FileManager/FileManagerStorage.swift @@ -146,15 +146,15 @@ extension FileManagerStorage { func storageURL(create: Bool = false) -> URL? { let directoryURL: URL? if #available(iOS 16.0, *) { - let storageDirectoryURL = try? self.fileManager.url(for: .applicationDirectory, + let storageDirectoryURL = try? self.fileManager.url(for: .cachesDirectory, in: .userDomainMask, - appropriateFor: .applicationDirectory, + appropriateFor: .cachesDirectory, create: false) directoryURL = storageDirectoryURL? .appending(path: Constants.appBundleIdentifier, directoryHint: .isDirectory) } else { let cacheDirectoryURL = self.fileManager - .urls(for: .applicationDirectory, in: .userDomainMask) + .urls(for: .cachesDirectory, in: .userDomainMask) .first directoryURL = cacheDirectoryURL? .appendingPathComponent(Constants.appBundleIdentifier, isDirectory: true) diff --git a/iOS/MSData/Sources/MSData/DTO/Response/Journey/RecordSpotResponseDTO.swift b/iOS/MSData/Sources/MSData/DTO/Response/Journey/RecordJourneyResponseDTO.swift similarity index 77% rename from iOS/MSData/Sources/MSData/DTO/Response/Journey/RecordSpotResponseDTO.swift rename to iOS/MSData/Sources/MSData/DTO/Response/Journey/RecordJourneyResponseDTO.swift index 359b2a4..00de500 100644 --- a/iOS/MSData/Sources/MSData/DTO/Response/Journey/RecordSpotResponseDTO.swift +++ b/iOS/MSData/Sources/MSData/DTO/Response/Journey/RecordJourneyResponseDTO.swift @@ -1,5 +1,5 @@ // -// RecordSpotResponseDTO.swift +// RecordJourneyResponseDTO.swift // MSData // // Created by 이창준 on 2023.12.06. @@ -7,7 +7,7 @@ import Foundation -public struct RecordSpotResponseDTO: Decodable { +public struct RecordJourneyResponseDTO: Decodable { // MARK: - Properties diff --git a/iOS/MSData/Sources/MSData/Repository/JourneyRepository.swift b/iOS/MSData/Sources/MSData/Repository/JourneyRepository.swift index cc4003e..9d6b389 100644 --- a/iOS/MSData/Sources/MSData/Repository/JourneyRepository.swift +++ b/iOS/MSData/Sources/MSData/Repository/JourneyRepository.swift @@ -17,6 +17,8 @@ import MSUserDefaults public protocol JourneyRepository: Persistable { + func fetchIsRecording() -> Bool + mutating func updateIsRecording(_ isRecording: Bool) -> Bool func fetchRecordingJourneyID() -> String? func fetchRecordingJourney(forID id: String) -> RecordingJourney? func fetchJourneyList(userID: UUID, @@ -36,6 +38,9 @@ public struct JourneyRepositoryImplementation: JourneyRepository { private let networking: MSNetworking public let storage: MSPersistentStorage + @UserDefaultsWrapped(UserDefaultsKey.isRecording, defaultValue: false) + private var isRecording: Bool + @UserDefaultsWrapped(UserDefaultsKey.recordingJourneyID, defaultValue: nil) private var recordingJourneyID: String? @@ -49,6 +54,16 @@ public struct JourneyRepositoryImplementation: JourneyRepository { // MARK: - Functions + public func fetchIsRecording() -> Bool { + return self.isRecording + } + + @discardableResult + public mutating func updateIsRecording(_ isRecording: Bool) -> Bool { + self.isRecording = isRecording + return self.isRecording + } + public func fetchRecordingJourneyID() -> String? { guard let recordingJourneyID = self.recordingJourneyID else { MSLogger.make(category: .userDefaults).error("기록 중인 여정 정보를 가져오는데 실패했습니다.") @@ -118,6 +133,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { self.saveToLocal(value: recordingJourney.id) self.saveToLocal(value: recordingJourney.startTimestamp) + self.isRecording = true self.recordingJourneyID = recordingJourney.id #if DEBUG @@ -140,11 +156,11 @@ public struct JourneyRepositoryImplementation: JourneyRepository { let coordinatesDTO = coordinates.map { CoordinateDTO($0) } let requestDTO = RecordCoordinateRequestDTO(journeyID: journeyID, coordinates: coordinatesDTO) let router = JourneyRouter.recordCoordinate(dto: requestDTO) - let result = await self.networking.request(RecordCoordinateRequestDTO.self, router: router) + let result = await self.networking.request(RecordJourneyResponseDTO.self, router: router) switch result { case .success(let responseDTO): let coordinates = responseDTO.coordinates.map { $0.toDomain() } - let recordingJourney = RecordingJourney(id: responseDTO.journeyID, + let recordingJourney = RecordingJourney(id: journeyID, startTimestamp: Date(), spots: [], coordinates: coordinates) @@ -168,6 +184,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { switch result { case .success(let responseDTO): self.recordingJourneyID = nil + self.isRecording = false return .success(responseDTO.id) case .failure(let error): return .failure(error) @@ -182,6 +199,7 @@ public struct JourneyRepositoryImplementation: JourneyRepository { switch result { case .success(let responseDTO): self.recordingJourneyID = nil + self.isRecording = false return .success(responseDTO.id) case .failure(let error): return .failure(error) diff --git a/iOS/MSData/Sources/MSData/Repository/UserRepository.swift b/iOS/MSData/Sources/MSData/Repository/UserRepository.swift index eea4802..cb7d6fc 100644 --- a/iOS/MSData/Sources/MSData/Repository/UserRepository.swift +++ b/iOS/MSData/Sources/MSData/Repository/UserRepository.swift @@ -8,12 +8,13 @@ import Foundation import MSKeychainStorage +import MSLogger import MSNetworking public protocol UserRepository { func createUser() async -> Result - func fetchUUID() throws -> UUID + func fetchUUID() -> UUID? } @@ -35,8 +36,12 @@ public struct UserRepositoryImplementation: UserRepository { // MARK: - Functions public func createUser() async -> Result { - guard let userID = try? self.fetchUUID() else { - return .failure(MSKeychainStorage.KeychainError.transactionError) + // Keychain에 UserID가 저장되어 있는 지 확인하고 아니라면 새로 생성 + let userID: UUID + if let existingUserID = self.fetchUUID() { + userID = existingUserID + } else { + userID = UUID() } let requestDTO = UserRequestDTO(userID: userID) @@ -52,16 +57,17 @@ public struct UserRepositoryImplementation: UserRepository { } /// UUID가 이미 키체인에 등록되어 있다면 가져옵니다. - /// 그렇지 않다면 새로 생성하고, 키체인에 등록합니다. - public func fetchUUID() throws -> UUID { + public func fetchUUID() -> UUID? { let account = MSKeychainStorage.Accounts.userID.rawValue - if let userID = try? self.keychain.get(UUID.self, account: account) { - return userID + guard let userID = try? self.keychain.get(UUID.self, account: account) else { + MSLogger.make(category: .keychain).error("Keychain에서 UserID를 조회하는 것에 실패했습니다.") + return nil } - let newUserID = UUID() - try self.keychain.set(value: newUserID, account: account) - return newUserID + #if DEBUG + MSLogger.make(category: .keychain).debug("Keychain에서 UserID를 조회했습니다: \(userID)") + #endif + return userID } } diff --git a/iOS/MSUIKit/Sources/MSUIKit/Cells/JourneyCell/JourneyCell.swift b/iOS/MSUIKit/Sources/MSUIKit/Cells/JourneyCell/JourneyCell.swift index d95b160..4b5b148 100644 --- a/iOS/MSUIKit/Sources/MSUIKit/Cells/JourneyCell/JourneyCell.swift +++ b/iOS/MSUIKit/Sources/MSUIKit/Cells/JourneyCell/JourneyCell.swift @@ -80,6 +80,8 @@ public final class JourneyCell: UICollectionViewCell { @MainActor public func addImageView(count: Int) { + guard count != .zero else { return } + (1...count).forEach { _ in let imageView = SpotPhotoImageView() self.spotImageStack.addArrangedSubview(imageView) diff --git a/iOS/MusicSpot/MusicSpot/SceneDelegate.swift b/iOS/MusicSpot/MusicSpot/SceneDelegate.swift index 42e8d6d..748633e 100644 --- a/iOS/MusicSpot/MusicSpot/SceneDelegate.swift +++ b/iOS/MusicSpot/MusicSpot/SceneDelegate.swift @@ -8,6 +8,7 @@ import UIKit import JourneyList +import MSConstants import MSData import MSDesignSystem @@ -18,6 +19,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? private var appCoordinator: Coordinator! + #if DEBUG + @UserDefaultsWrapped(UserDefaultsKey.recordingJourneyID, defaultValue: nil) + var recordingJourneyID: String? + @UserDefaultsWrapped(UserDefaultsKey.isFirstLaunch, defaultValue: false) + var isFirstLaunch: Bool + var keychain = MSKeychainStorage() + #endif + // MARK: - Functions func scene(_ scene: UIScene, @@ -51,3 +60,28 @@ private extension SceneDelegate { } } + + +// MARK: - Debug + +#if DEBUG + +import MSKeychainStorage +import MSLogger +import MSUserDefaults + +private extension SceneDelegate { + + func prepareToDebug() { + self.isFirstLaunch = true + self.recordingJourneyID = nil + do { + try self.keychain.deleteAll() + } catch { + MSLogger.make(category: .keychain).error("키체인 초기화 실패") + } + } + +} + +#endif