diff --git a/Neki-iOS/APP/Sources/Application/AppCoordinator.swift b/Neki-iOS/APP/Sources/Application/AppCoordinator.swift index c741c2f9..f3a4dfbe 100644 --- a/Neki-iOS/APP/Sources/Application/AppCoordinator.swift +++ b/Neki-iOS/APP/Sources/Application/AppCoordinator.swift @@ -193,12 +193,8 @@ struct AppCoordinator { switch newStatus { case let .signedIn(user): - if case var .mainTab(mainTabState) = state.route { - mainTabState.user = user - state.route = .mainTab(.init(user: user)) - return .none - } - state.route = .mainTab(.init(user: user)) + if case .mainTab = state.route { return .none } + state.route = .mainTab(.init()) return .none case .signedOut: @@ -221,24 +217,17 @@ struct AppCoordinator { case let .route(.auth(.delegate(.moveToMainTab(user)))): state.$userSessionStatus.withLock { $0 = .signedIn(user) } - state.route = .mainTab(.init(user: user)) + state.route = .mainTab(.init()) return .send(.executePendingShareExtensionIfNeeded) - case .route(.mainTab(.delegate(.signedOut))): - state.$userSessionStatus.withLock { $0 = .signedOut } - state.route = .auth(.init()) - return .none - - case .route(.mainTab(.delegate(.withdraw))): + case .route(.mainTab(.delegate(.signedOut))), .route(.mainTab(.delegate(.withdraw))): state.$userSessionStatus.withLock { $0 = .signedOut } - state.initializeUserDefaults() + if case .route(.mainTab(.delegate(.withdraw))) = action { + state.initializeUserDefaults() + } state.route = .auth(.init()) return .none - case let .route(.mainTab(.delegate(.profileUpdated(user)))): - state.$userSessionStatus.withLock { $0 = .signedIn(user) } - return .none - case .binding(\.isAlertPresented): guard state.isAlertPresented == false else { return .none } guard let pendingSessionStatus = state.pendingSessionStatus else { return .none } @@ -253,8 +242,8 @@ struct AppCoordinator { private func navigateToNextScreen(state: inout State, sessionStatus: UserSessionStatus) -> Effect { switch sessionStatus { - case .signedIn(let user): - state.route = .mainTab(.init(user: user)) + case .signedIn: + state.route = .mainTab(.init()) return .send(.executePendingShareExtensionIfNeeded) case .signedOut, .expired: diff --git a/Neki-iOS/APP/Sources/MainTab/MainTabCoordinator.swift b/Neki-iOS/APP/Sources/MainTab/MainTabCoordinator.swift index 6f5d3d22..ceaf04da 100644 --- a/Neki-iOS/APP/Sources/MainTab/MainTabCoordinator.swift +++ b/Neki-iOS/APP/Sources/MainTab/MainTabCoordinator.swift @@ -16,14 +16,14 @@ struct MainTabCoordinator { @ObservableState struct State { - var user: User + @Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut var selectedTab: NekiTab = .archive // 하위 코디네이터들의 State를 보유 var pose = PoseCoordinator.State() var archive = ArchiveCoordinator.State() var map = MapCoordinator.State() - var myPage: MyPageCoordinator.State + var myPage = MyPageCoordinator.State() var imagePicker = ImagePickerFeature.State(mediaType: .photoBooth, autoUpload: false) @@ -37,9 +37,15 @@ struct MainTabCoordinator { var toast: NekiToastItem? = nil var isPermissionAlertPresented: Bool = false - init(user: User) { - self.user = user - myPage = MyPageCoordinator.State(user: user) + var user: User { + get { + guard case let .signedIn(user) = userSessionStatus else { return .dummy } + return user + } + + set { + $userSessionStatus.withLock { $0 = .signedIn(newValue) } + } } } @@ -72,7 +78,6 @@ struct MainTabCoordinator { enum Delegate { case signedOut case withdraw - case profileUpdated(User) } } @@ -170,9 +175,6 @@ struct MainTabCoordinator { await send(.delegate(.withdraw)) } - case let .myPage(.delegate(.profileUpdated(user))): - return .send(.delegate(.profileUpdated(user))) - case let .imagePicker(.delegate(.imagesConverted(entities))): state.isPhotoPickerPresented = false state.isLoading = false diff --git a/Neki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Entities/User.swift b/Neki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Entities/User.swift index b7e42ba3..c6464f69 100644 --- a/Neki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Entities/User.swift +++ b/Neki-iOS/Core/Sources/Auth/Sources/Domain/Sources/Entities/User.swift @@ -16,3 +16,7 @@ public struct User: Sendable, Equatable, Codable { let providerType: ProviderType var allRequiredTermsAgreed: Bool } + +extension User { + static var dummy: Self { User(id: -1, nickname: "-", email: nil, profileImageURL: nil, providerType: .local, allRequiredTermsAgreed: true) } +} diff --git a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Coordinator/MyPageCoordinator.swift b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Coordinator/MyPageCoordinator.swift index c1c625ab..38ea31de 100644 --- a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Coordinator/MyPageCoordinator.swift +++ b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Coordinator/MyPageCoordinator.swift @@ -12,12 +12,10 @@ import ComposableArchitecture struct MyPageCoordinator { @ObservableState struct State { - var root: MyPageFeature.State - var path = StackState() + @Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut - init(user: User) { - root = MyPageFeature.State(user: user) - } + var root = MyPageFeature.State() + var path = StackState() } enum Action { @@ -28,7 +26,6 @@ struct MyPageCoordinator { enum Delegate { case didLogout case didWithdraw - case profileUpdated(User) } } @@ -43,11 +40,12 @@ struct MyPageCoordinator { return routeMyPageCellTapped(state: &state, cellItem) case .root(.profileTapped): - state.path.append(.accountPreference(.init(user: state.root.user))) + state.path.append(.accountPreference(.init())) return .none case .path(.element(id: _, action: .accountPreference(.editProfileButtonTapped))): - state.path.append(.profileEdit(.init(user: state.root.user))) + guard case let .signedIn(user) = state.userSessionStatus else { return .none } + state.path.append(.profileEdit(.init(user: user))) return .none case .path(.element(id: _, action: .accountPreference(.didSignOut))): @@ -56,9 +54,6 @@ struct MyPageCoordinator { case .path(.element(id: _, action: .accountPreference(.didWithdraw))): return .send(.delegate(.didWithdraw)) - case let .path(.element(id: _, action: .profileEdit(.profileUpdated(user)))): - return .send(.delegate(.profileUpdated(user))) - default: return .none } diff --git a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/AccountPreferenceFeature.swift b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/AccountPreferenceFeature.swift index 0fc54b0d..0aa8a746 100644 --- a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/AccountPreferenceFeature.swift +++ b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/AccountPreferenceFeature.swift @@ -13,11 +13,16 @@ import os struct AccountPreferenceFeature { @ObservableState struct State { - var user: User + @Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut var isLogoutAlertPresented: Bool = false var isUnregisterAlertPresented: Bool = false var isLoading: Bool = false + + var user: User { + guard case let .signedIn(user) = userSessionStatus else { return .dummy } + return user + } } enum Action: BindableAction { @@ -71,9 +76,9 @@ struct AccountPreferenceFeature { state.isUnregisterAlertPresented = false state.isLoading = true - return .run { [userId = state.user.id] send in + return .run { [userID = state.user.id] send in try await authClient.withdraw() - UserDefaults.standard.removeObject(forKey: "TermsAgreed_\(userId)") + UserDefaults.standard.removeObject(forKey: "TermsAgreed_\(userID)") await send(.didWithdraw) } catch: { error, send in Logger.presentation.error("회원탈퇴 과정 중 에러 발생: \(error)") diff --git a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/MyPageFeature.swift b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/MyPageFeature.swift index b04c278e..1d4857eb 100644 --- a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/MyPageFeature.swift +++ b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/MyPageFeature.swift @@ -12,8 +12,13 @@ import ComposableArchitecture struct MyPageFeature { @ObservableState struct State { - var user: User + @Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut var appVersion: AppVersion = AppVersion(major: 0, minor: 0, revision: 0) + + var user: User { + guard case let .signedIn(user) = userSessionStatus else { return .dummy } + return user + } } enum Action { diff --git a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/ProfileEditFeature.swift b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/ProfileEditFeature.swift index 4949c515..260a4019 100644 --- a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/ProfileEditFeature.swift +++ b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/Feature/ProfileEditFeature.swift @@ -14,7 +14,7 @@ import os struct ProfileEditFeature { @ObservableState struct State { - @ObservationStateIgnored let user: User + @Shared(.appStorage(AppStorageKey.userSessionStatus)) var userSessionStatus: UserSessionStatus = .signedOut var nickname: String var currentProfileImageURL: URL? var selectedProfileImage: UIImage? @@ -29,8 +29,18 @@ struct ProfileEditFeature { var isProfileSelectionAlertPresented: Bool = false + var user: User { + get { + guard case let .signedIn(user) = userSessionStatus else { return .dummy } + return user + } + + set { + $userSessionStatus.withLock { $0 = .signedIn(newValue) } + } + } + init(user: User) { - self.user = user nickname = user.nickname currentProfileImageURL = user.profileImageURL } @@ -50,9 +60,6 @@ struct ProfileEditFeature { // Binding Actions case binding(BindingAction) - - // Delegate Actions - case profileUpdated(User) } private enum CancelID { case imageLoad } @@ -134,8 +141,8 @@ struct ProfileEditFeature { case let .updateProfileResponse(.success(user)): state.isLoading = false - return .run { send in - await send(.profileUpdated(user)) + state.user = user + return .run { _ in await dismiss() } diff --git a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift index 83c464a4..a8c56013 100644 --- a/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift +++ b/Neki-iOS/Features/MyPage/Sources/Presentation/Sources/View/AccountPreferenceView.swift @@ -69,12 +69,20 @@ private extension AccountPreferenceView { var profileArea: some View { VStack(spacing: 16) { - KFImage(store.user.profileImageURL) - .resizable() - .onFailureImage(.iconDefaultProfile) - .scaledToFill() - .frame(width: 142, height: 142) - .clipShape(.circle) + ZStack(alignment: .bottomTrailing) { + KFImage(store.user.profileImageURL) + .resizable() + .onFailureImage(.iconDefaultProfile) + .scaledToFill() + .frame(width: 142, height: 142) + .clipShape(.circle) + + Button { + store.send(.editProfileButtonTapped) + } label: { + Image(.iconProfileCamera) + } + } HStack(spacing: 9) { Text(store.user.nickname)