From 2a66aa8261f6d0a71b1431192819d915ae9e7594 Mon Sep 17 00:00:00 2001 From: SwainYun Date: Wed, 8 Apr 2026 20:28:31 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[Style]=20#204=20-=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=82=AC=EC=A7=84=20=EB=B3=80=EA=B2=BD=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=82=AC=EC=9A=A9=EC=84=B1=20=ED=99=95=EB=B3=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/View/AccountPreferenceView.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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) From 5286ff7bac63b6e279770b953d6a7b7849be9a2e Mon Sep 17 00:00:00 2001 From: SwainYun Date: Wed, 8 Apr 2026 21:07:07 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[Fix]=20#204=20-=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=ED=8E=B8=EC=A7=91=20=ED=9B=84=20=EC=95=84=EC=B9=B4?= =?UTF-8?q?=EC=9D=B4=EB=B9=99=20=ED=83=AD=EC=9D=B4=20=EB=82=98=ED=83=80?= =?UTF-8?q?=EB=82=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Neki-iOS/APP/Sources/Application/AppCoordinator.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Neki-iOS/APP/Sources/Application/AppCoordinator.swift b/Neki-iOS/APP/Sources/Application/AppCoordinator.swift index c741c2f9..c6e6d947 100644 --- a/Neki-iOS/APP/Sources/Application/AppCoordinator.swift +++ b/Neki-iOS/APP/Sources/Application/AppCoordinator.swift @@ -195,7 +195,8 @@ struct AppCoordinator { case let .signedIn(user): if case var .mainTab(mainTabState) = state.route { mainTabState.user = user - state.route = .mainTab(.init(user: user)) + mainTabState.myPage.root.user = user + state.route = .mainTab(mainTabState) return .none } state.route = .mainTab(.init(user: user)) From 4961d0cc5ac197ef191f9d4f5cc68aaab27003dc Mon Sep 17 00:00:00 2001 From: SwainYun Date: Wed, 8 Apr 2026 23:47:06 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[Chore]=20#204=20-=20=EB=8D=94=EB=AF=B8=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Auth/Sources/Domain/Sources/Entities/User.swift | 4 ++++ 1 file changed, 4 insertions(+) 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) } +} From c75da04a699f4c37a769254f8229bae6af1c08d6 Mon Sep 17 00:00:00 2001 From: SwainYun Date: Wed, 8 Apr 2026 23:47:40 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[Refactor]=20#204=20-=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=B8=EC=85=98=20=EC=A3=BC?= =?UTF-8?q?=EC=9E=85=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Application/AppCoordinator.swift | 30 ++++++------------- .../Sources/MainTab/MainTabCoordinator.swift | 20 +++++++------ .../Coordinator/MyPageCoordinator.swift | 17 ++++------- .../Feature/AccountPreferenceFeature.swift | 11 +++++-- .../Sources/Feature/MyPageFeature.swift | 7 ++++- .../Sources/Feature/ProfileEditFeature.swift | 21 ++++++++----- 6 files changed, 54 insertions(+), 52 deletions(-) diff --git a/Neki-iOS/APP/Sources/Application/AppCoordinator.swift b/Neki-iOS/APP/Sources/Application/AppCoordinator.swift index c6e6d947..f3a4dfbe 100644 --- a/Neki-iOS/APP/Sources/Application/AppCoordinator.swift +++ b/Neki-iOS/APP/Sources/Application/AppCoordinator.swift @@ -193,13 +193,8 @@ struct AppCoordinator { switch newStatus { case let .signedIn(user): - if case var .mainTab(mainTabState) = state.route { - mainTabState.user = user - mainTabState.myPage.root.user = user - state.route = .mainTab(mainTabState) - return .none - } - state.route = .mainTab(.init(user: user)) + if case .mainTab = state.route { return .none } + state.route = .mainTab(.init()) return .none case .signedOut: @@ -222,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 } @@ -254,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/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() }