From c3041249974bff87ab73c27d1fd9642b6edf6d34 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 12 Feb 2026 09:03:52 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=EB=B7=B0=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/AccountViewModel.swift | 126 ++++++++++++++++ DevLog/UI/Setting/AccountView.swift | 138 +++++++++--------- 2 files changed, 193 insertions(+), 71 deletions(-) create mode 100644 DevLog/Presentation/ViewModel/AccountViewModel.swift diff --git a/DevLog/Presentation/ViewModel/AccountViewModel.swift b/DevLog/Presentation/ViewModel/AccountViewModel.swift new file mode 100644 index 0000000..5f7d513 --- /dev/null +++ b/DevLog/Presentation/ViewModel/AccountViewModel.swift @@ -0,0 +1,126 @@ +// +// AccountViewModel.swift +// DevLog +// +// Created by 최윤진 on 2/12/26. +// + +import Foundation + +final class AccountViewModel: Store { + struct State { + var currentProvider: String = "" + var connectedProviders: [String] = [] + var disconnectedProviders: [String] = [] + var showAlert: Bool = false + var alertTitle: String = "" + var alertType: AlertType? + var alertMessage: String = "" + var showToast: Bool = false + var toastType: ToastType? + var toastMessage: String = "" + var isLoading: Bool = false + } + + enum Action { + case onAppear + case linkWithProvider(String) + case unlinkFromProvider(String) + case setAlert(isPresented: Bool, type: AlertType? = nil) + case setToast(isPresented: Bool, type: ToastType? = nil) + case setLoading(Bool) + } + + enum SideEffect { + case link(String) + case unlink(String) + } + + enum AlertType { + case error + } + + enum ToastType { + case linkSuccess + case unlinkSuccess + } + + @Published private(set) var state: State = .init() + + func reduce(with action: Action) -> [SideEffect] { + var state = self.state + + switch action { + case .onAppear: + + case .linkWithProvider(let value): + return [.link(value)] + case .unlinkFromProvider(let value): + return [.unlink(value)] + case .setAlert(let isPresented, let type): + setAlert(&state, isPresented: isPresented, type: type) + case .setToast(let isPresented, let type): + setToast(&state, isPresented: isPresented, type: type) + case .setLoading(let value): + state.isLoading = value + } + + self.state = state + return [] + } + + func run(_ effect: SideEffect) { + switch effect { + case .link(let provider): + Task { + do { + defer { send(.setLoading(false)) } + send(.setLoading(true)) + + send(.setToast(isPresented: true, type: .linkSuccess)) + } catch { + send(.setAlert(isPresented: true, type: .error)) + } + } + case .unlink(let provider): + Task { + do { + defer { send(.setLoading(false)) } + send(.setLoading(true)) + + send(.setToast(isPresented: true, type: .unlinkSuccess)) + } catch { + send(.setAlert(isPresented: true, type: .error)) + } + } + } + } +} + +private extension AccountViewModel { + func setAlert(_ state: inout State, isPresented: Bool, type: AlertType?) { + switch type { + case .error: + state.alertTitle = "오류" + state.alertMessage = "문제가 발생했습니다. 잠시 후 다시 시도해주세요." + case .none: + state.alertTitle = "" + state.alertMessage = "" + } + state.showAlert = isPresented + state.alertType = type + } + + func setToast(_ state: inout State, isPresented: Bool, type: ToastType?) { + switch type { + case .linkSuccess: + state.toastMessage = "계정이 성공적으로 연결되었습니다." + case .unlinkSuccess: + state.toastMessage = "계정 연결이 성공적으로 해제되었습니다." + case .none: + state.toastMessage = "" + } + state.showToast = isPresented + state.toastType = type + } +} diff --git a/DevLog/UI/Setting/AccountView.swift b/DevLog/UI/Setting/AccountView.swift index 3190206..f75f38f 100644 --- a/DevLog/UI/Setting/AccountView.swift +++ b/DevLog/UI/Setting/AccountView.swift @@ -9,53 +9,51 @@ import SwiftUI import FirebaseAuth struct AccountView: View { - @ObservedObject var viewModel: SettingViewModel - @State private var connectedProviders: [String] = [] - @State private var disconnectedProviders: [String] = [] - + @StateObject var viewModel: AccountViewModel + var body: some View { -// List { -// Section("현재 계정") { -// HStack { -// let provider = viewModel.currentProvider -// let formattedProvider = formattedProviderName(provider) -// Image(formattedProvider) -// .resizable() -// .scaledToFit() -// .frame(width: UIFont.labelFontSize) -// Text(formattedProvider) -// } -// } -// Section("연동된 계정") { -// ForEach(connectedProviders, id: \.self) { provider in -// HStack { -// let formattedProvider = formattedProviderName(provider) -// Image(formattedProvider) -// .resizable() -// .scaledToFit() -// .frame(width: UIFont.labelFontSize) -// Text(formattedProvider) -// } -// .swipeActions(edge: .trailing, allowsFullSwipe: true) { -// Button(role: .destructive, action: { -// Task { -//// await viewModel.unlinkFromProvider(provider: provider) -// } -// }) { -// Label("계정 삭제", systemImage: "trash") -// } -// } -// } -// } -// } -// .onAppear { + List { + Section("현재 계정") { + HStack { + let provider = viewModel.state.currentProvider + let formattedProvider = formattedProviderName(provider) + Image(formattedProvider) + .resizable() + .scaledToFit() + .frame(width: UIFont.labelFontSize) + Text(formattedProvider) + } + } + Section("연동된 계정") { + ForEach(viewModel.state.connectedProviders, id: \.self) { provider in + HStack { + let formattedProvider = formattedProviderName(provider) + Image(formattedProvider) + .resizable() + .scaledToFit() + .frame(width: UIFont.labelFontSize) + Text(formattedProvider) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button(role: .destructive, action: { + viewModel.send(.unlinkFromProvider(provider)) + }) { + Label("계정 삭제", systemImage: "trash") + } + } + } + } + } + .listStyle(.insetGrouped) + .onAppear { + viewModel.send(.onAppear) // connectedProviders = viewModel.providers.filter { provider in // provider != viewModel.currentProvider // } // disconnectedProviders = ["google.com", "github.com", "apple.com"].filter { provider in // !viewModel.providers.contains(provider) // } -// } + } // .onChange(of: viewModel.providers) { newProviders in // connectedProviders = newProviders.filter { provider in // provider != viewModel.currentProvider @@ -64,37 +62,35 @@ struct AccountView: View { // !newProviders.contains(provider) // } // } -// .listStyle(InsetGroupedListStyle()) -// .navigationTitle("계정 연동") -// .toolbar { -// ToolbarItem(placement: .navigationBarTrailing) { -// Menu("새 계정 연동", systemImage: "plus") { -// ForEach(disconnectedProviders, id: \.self) { provider in -// Button(action: { -// Task { -//// await viewModel.linkWithProvider(provider: provider) -// } -// }) { -// HStack { -// let formattedProvider = formattedProviderName(provider) -// Image(formattedProvider) -// .resizable() -// .scaledToFit() -// .frame(width: UIFont.systemFontSize, height: UIFont.systemFontSize) -// Text(formattedProvider) -// } -// } -// } -// } -// } -// } -// .alert("계정 연동 실패", isPresented: $viewModel.showAlert) { -// Button("확인", role: .cancel) { -// viewModel.showAlert = false -// } -// } message: { -// Text(viewModel.alertMsg) -// } + .navigationTitle("계정 연동") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Menu("새 계정 연동", systemImage: "plus") { + ForEach(viewModel.state.disconnectedProviders, id: \.self) { provider in + Button(action: { + viewModel.send(.linkWithProvider(provider)) + }) { + HStack { + let formattedProvider = formattedProviderName(provider) + Image(formattedProvider) + .resizable() + .scaledToFit() + .frame(width: UIFont.systemFontSize, height: UIFont.systemFontSize) + Text(formattedProvider) + } + } + } + } + } + } + .alert(viewModel.state.alertTitle, isPresented: Binding( + get: { viewModel.state.showAlert }, + set: { viewModel.send(.setAlert(isPresented: $0)) } + )) { + Button("확인", role: .cancel) { } + } message: { + Text(viewModel.state.alertMessage) + } } private func formattedProviderName(_ provider: String) -> String { From 583dddaa1c6439ff1cc620d0870abc1491c6bd6b Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 12 Feb 2026 09:47:16 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/DataAssembler.swift | 18 ++++ DevLog/Data/Common/Error+.swift | 2 + .../Repository/AuthDataRepositoryImpl.swift | 82 +++++++++++++++++++ .../Domain/Protocol/AuthDataRepository.swift | 14 +++- 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 DevLog/Data/Repository/AuthDataRepositoryImpl.swift diff --git a/DevLog/App/Assembler/DataAssembler.swift b/DevLog/App/Assembler/DataAssembler.swift index a918753..80c1f6e 100644 --- a/DevLog/App/Assembler/DataAssembler.swift +++ b/DevLog/App/Assembler/DataAssembler.swift @@ -37,6 +37,24 @@ final class DataAssembler: Assembler { AuthSessionRepositoryImpl(authService: container.resolve(AuthService.self)) } + container.register(AuthDataRepository.self) { + AuthDataRepositoryImpl( + authService: container.resolve(AuthService.self), + appleAuthService: container.resolve( + AuthenticationService.self, + name: "AppleAuthenticationService" + ), + githubAuthService: container.resolve( + AuthenticationService.self, + name: "GithubAuthenticationService" + ), + googleAuthService: container.resolve( + AuthenticationService.self, + name: "GoogleAuthenticationService" + ) + ) + } + container.register(UserDataRepository.self) { UserDataRepositoryImpl(userService: container.resolve(UserService.self)) } diff --git a/DevLog/Data/Common/Error+.swift b/DevLog/Data/Common/Error+.swift index 4405ded..33a4d0a 100644 --- a/DevLog/Data/Common/Error+.swift +++ b/DevLog/Data/Common/Error+.swift @@ -9,6 +9,8 @@ import Foundation enum AuthError: Error { case notAuthenticated + case failedToUnlinkLastProvider + case unsupportedProvider } enum FirestoreError: Error, LocalizedError { diff --git a/DevLog/Data/Repository/AuthDataRepositoryImpl.swift b/DevLog/Data/Repository/AuthDataRepositoryImpl.swift new file mode 100644 index 0000000..e580a04 --- /dev/null +++ b/DevLog/Data/Repository/AuthDataRepositoryImpl.swift @@ -0,0 +1,82 @@ +// +// AuthDataRepositoryImpl.swift +// DevLog +// +// Created by 최윤진 on 2/12/26. +// + +import FirebaseAuth + +final class AuthDataRepositoryImpl: AuthDataRepository { + private let authService: AuthService + private let appleAuthService: AuthenticationService + private let githubAuthService: AuthenticationService + private let googleAuthService: AuthenticationService + + init( + authService: AuthService, + appleAuthService: AuthenticationService, + githubAuthService: AuthenticationService, + googleAuthService: AuthenticationService + ) { + self.authService = authService + self.appleAuthService = appleAuthService + self.githubAuthService = githubAuthService + self.googleAuthService = googleAuthService + } + + func fetchCurrentProvider() async throws -> AuthProvider? { + guard let providerString = try await authService.getProviderID() else { + return nil + } + return AuthProvider(rawValue: providerString) + } + + func fetchAllProviders() async throws -> [AuthProvider] { + let providerStrings = authService.providerIDs ?? [] + return providerStrings.compactMap { AuthProvider(rawValue: $0) } + } + + func linkProvider(_ provider: AuthProvider) async throws { + guard let uid = authService.uid, + let user = Auth.auth().currentUser, + let email = user.email else { + throw AuthError.notAuthenticated + } + + let service: AuthenticationService + switch provider { + case .apple: + service = appleAuthService + case .google: + service = googleAuthService + case .github: + service = githubAuthService + } + + try await service.link(uid: uid, email: email) + } + + func unlinkProvider(_ provider: AuthProvider) async throws { + guard let uid = authService.uid, + let user = Auth.auth().currentUser else { + throw AuthError.notAuthenticated + } + + if user.providerData.count <= 1 { + throw AuthError.failedToUnlinkLastProvider + } + + let service: AuthenticationService + switch provider { + case .apple: + service = appleAuthService + case .google: + service = googleAuthService + case .github: + service = githubAuthService + } + + try await service.unlink(uid) + } +} diff --git a/DevLog/Domain/Protocol/AuthDataRepository.swift b/DevLog/Domain/Protocol/AuthDataRepository.swift index b36d473..ec869e3 100644 --- a/DevLog/Domain/Protocol/AuthDataRepository.swift +++ b/DevLog/Domain/Protocol/AuthDataRepository.swift @@ -5,4 +5,16 @@ // Created by 최윤진 on 1/5/26. // -import Foundation +protocol AuthDataRepository { + /// 현재 로그인한 프로바이더를 가져옵니다 + func fetchCurrentProvider() async throws -> AuthProvider? + + /// 연결된 모든 프로바이더 목록을 가져옵니다 + func fetchAllProviders() async throws -> [AuthProvider] + + /// 특정 프로바이더를 계정에 연결합니다 + func linkProvider(_ provider: AuthProvider) async throws + + /// 특정 프로바이더를 계정에서 해제합니다 + func unlinkProvider(_ provider: AuthProvider) async throws +} From c584e4f9360ba0ff0c22c2ab5177fc099f5ed407 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 12 Feb 2026 09:47:41 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/App/Assembler/DomainAssembler.swift | 12 +++++++++++ .../Provider/FetchAuthProvidersUseCase.swift | 10 +++++++++ .../FetchAuthProvidersUseCaseImpl.swift | 21 +++++++++++++++++++ .../Provider/LinkAuthProviderUseCase.swift | 10 +++++++++ .../LinkAuthProviderUseCaseImpl.swift | 18 ++++++++++++++++ .../Provider/UnlinkAuthProviderUseCase.swift | 10 +++++++++ .../UnlinkAuthProviderUseCaseImpl.swift | 18 ++++++++++++++++ 7 files changed, 99 insertions(+) create mode 100644 DevLog/Domain/UseCase/Auth/Provider/FetchAuthProvidersUseCase.swift create mode 100644 DevLog/Domain/UseCase/Auth/Provider/FetchAuthProvidersUseCaseImpl.swift create mode 100644 DevLog/Domain/UseCase/Auth/Provider/LinkAuthProviderUseCase.swift create mode 100644 DevLog/Domain/UseCase/Auth/Provider/LinkAuthProviderUseCaseImpl.swift create mode 100644 DevLog/Domain/UseCase/Auth/Provider/UnlinkAuthProviderUseCase.swift create mode 100644 DevLog/Domain/UseCase/Auth/Provider/UnlinkAuthProviderUseCaseImpl.swift diff --git a/DevLog/App/Assembler/DomainAssembler.swift b/DevLog/App/Assembler/DomainAssembler.swift index dad41ea..f99f662 100644 --- a/DevLog/App/Assembler/DomainAssembler.swift +++ b/DevLog/App/Assembler/DomainAssembler.swift @@ -70,5 +70,17 @@ final class DomainAssembler: Assembler { container.register(FetchPushNotificationsUseCase.self) { FetchPushNotificationsUseCaseImpl(container.resolve(PushNotificationRepository.self)) } + + container.register(FetchAuthProvidersUseCase.self) { + FetchAuthProvidersUseCaseImpl(container.resolve(AuthDataRepository.self)) + } + + container.register(LinkAuthProviderUseCase.self) { + LinkAuthProviderUseCaseImpl(container.resolve(AuthDataRepository.self)) + } + + container.register(UnlinkAuthProviderUseCase.self) { + UnlinkAuthProviderUseCaseImpl(container.resolve(AuthDataRepository.self)) + } } } diff --git a/DevLog/Domain/UseCase/Auth/Provider/FetchAuthProvidersUseCase.swift b/DevLog/Domain/UseCase/Auth/Provider/FetchAuthProvidersUseCase.swift new file mode 100644 index 0000000..11649d9 --- /dev/null +++ b/DevLog/Domain/UseCase/Auth/Provider/FetchAuthProvidersUseCase.swift @@ -0,0 +1,10 @@ +// +// FetchAuthProvidersUseCase.swift +// DevLog +// +// Created by 최윤진 on 2/12/26. +// + +protocol FetchAuthProvidersUseCase { + func execute() async throws -> (currentProvider: AuthProvider?, allProviders: [AuthProvider]) +} diff --git a/DevLog/Domain/UseCase/Auth/Provider/FetchAuthProvidersUseCaseImpl.swift b/DevLog/Domain/UseCase/Auth/Provider/FetchAuthProvidersUseCaseImpl.swift new file mode 100644 index 0000000..3e83239 --- /dev/null +++ b/DevLog/Domain/UseCase/Auth/Provider/FetchAuthProvidersUseCaseImpl.swift @@ -0,0 +1,21 @@ +// +// FetchAuthProvidersUseCaseImpl.swift +// DevLog +// +// Created by 최윤진 on 2/12/26. +// + +final class FetchAuthProvidersUseCaseImpl: FetchAuthProvidersUseCase { + private let repository: AuthDataRepository + + init(_ repository: AuthDataRepository) { + self.repository = repository + } + + func execute() async throws -> (currentProvider: AuthProvider?, allProviders: [AuthProvider]) { + async let currentProvider = try await repository.fetchCurrentProvider() + async let allProviders = try await repository.fetchAllProviders() + + return try await (currentProvider, allProviders) + } +} diff --git a/DevLog/Domain/UseCase/Auth/Provider/LinkAuthProviderUseCase.swift b/DevLog/Domain/UseCase/Auth/Provider/LinkAuthProviderUseCase.swift new file mode 100644 index 0000000..9ea4c0a --- /dev/null +++ b/DevLog/Domain/UseCase/Auth/Provider/LinkAuthProviderUseCase.swift @@ -0,0 +1,10 @@ +// +// LinkAuthProviderUseCase.swift +// DevLog +// +// Created by 최윤진 on 2/12/26. +// + +protocol LinkAuthProviderUseCase { + func execute(_ provider: AuthProvider) async throws +} diff --git a/DevLog/Domain/UseCase/Auth/Provider/LinkAuthProviderUseCaseImpl.swift b/DevLog/Domain/UseCase/Auth/Provider/LinkAuthProviderUseCaseImpl.swift new file mode 100644 index 0000000..fbdb25d --- /dev/null +++ b/DevLog/Domain/UseCase/Auth/Provider/LinkAuthProviderUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// LinkAuthProviderUseCaseImpl.swift +// DevLog +// +// Created by 최윤진 on 2/12/26. +// + +final class LinkAuthProviderUseCaseImpl: LinkAuthProviderUseCase { + private let repository: AuthDataRepository + + init(_ repository: AuthDataRepository) { + self.repository = repository + } + + func execute(_ provider: AuthProvider) async throws { + try await repository.linkProvider(provider) + } +} diff --git a/DevLog/Domain/UseCase/Auth/Provider/UnlinkAuthProviderUseCase.swift b/DevLog/Domain/UseCase/Auth/Provider/UnlinkAuthProviderUseCase.swift new file mode 100644 index 0000000..01dd39d --- /dev/null +++ b/DevLog/Domain/UseCase/Auth/Provider/UnlinkAuthProviderUseCase.swift @@ -0,0 +1,10 @@ +// +// UnlinkAuthProviderUseCase.swift +// DevLog +// +// Created by 최윤진 on 2/12/26. +// + +protocol UnlinkAuthProviderUseCase { + func execute(_ provider: AuthProvider) async throws +} diff --git a/DevLog/Domain/UseCase/Auth/Provider/UnlinkAuthProviderUseCaseImpl.swift b/DevLog/Domain/UseCase/Auth/Provider/UnlinkAuthProviderUseCaseImpl.swift new file mode 100644 index 0000000..2ddb12e --- /dev/null +++ b/DevLog/Domain/UseCase/Auth/Provider/UnlinkAuthProviderUseCaseImpl.swift @@ -0,0 +1,18 @@ +// +// UnlinkAuthProviderUseCaseImpl.swift +// DevLog +// +// Created by 최윤진 on 2/12/26. +// + +final class UnlinkAuthProviderUseCaseImpl: UnlinkAuthProviderUseCase { + private let repository: AuthDataRepository + + init(_ repository: AuthDataRepository) { + self.repository = repository + } + + func execute(_ provider: AuthProvider) async throws { + try await repository.unlinkProvider(provider) + } +} From cec0de5e5f09085e10ef8edf0cf4a89f6c6235d8 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 12 Feb 2026 09:47:58 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=ED=94=84=EB=A0=88=EC=A0=A0?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98,=20UI=20=EB=A0=88=EC=9D=B4=EC=96=B4?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/AccountViewModel.swift | 57 +++++++++++++++---- DevLog/Resource/Localizable.xcstrings | 12 ++++ DevLog/UI/Setting/AccountView.swift | 50 +++++++--------- DevLog/UI/Setting/SettingView.swift | 9 ++- 4 files changed, 87 insertions(+), 41 deletions(-) diff --git a/DevLog/Presentation/ViewModel/AccountViewModel.swift b/DevLog/Presentation/ViewModel/AccountViewModel.swift index 5f7d513..9aac266 100644 --- a/DevLog/Presentation/ViewModel/AccountViewModel.swift +++ b/DevLog/Presentation/ViewModel/AccountViewModel.swift @@ -9,9 +9,9 @@ import Foundation final class AccountViewModel: Store { struct State { - var currentProvider: String = "" - var connectedProviders: [String] = [] - var disconnectedProviders: [String] = [] + var currentProvider: AuthProvider? + var connectedProviders: [AuthProvider] = [] + var disconnectedProviders: [AuthProvider] = [] var showAlert: Bool = false var alertTitle: String = "" var alertType: AlertType? @@ -24,16 +24,18 @@ final class AccountViewModel: Store { enum Action { case onAppear - case linkWithProvider(String) - case unlinkFromProvider(String) + case linkWithProvider(AuthProvider) + case unlinkFromProvider(AuthProvider) case setAlert(isPresented: Bool, type: AlertType? = nil) case setToast(isPresented: Bool, type: ToastType? = nil) case setLoading(Bool) + case updateProviders(currentProvider: AuthProvider?, allProviders: [AuthProvider]) } enum SideEffect { - case link(String) - case unlink(String) + case fetch + case link(AuthProvider) + case unlink(AuthProvider) } enum AlertType { @@ -46,13 +48,26 @@ final class AccountViewModel: Store { } @Published private(set) var state: State = .init() + private let fetchProvidersUseCase: FetchAuthProvidersUseCase + private let linkProviderUseCase: LinkAuthProviderUseCase + private let unlinkProviderUseCase: UnlinkAuthProviderUseCase + + init( + fetchProvidersUseCase: FetchAuthProvidersUseCase, + linkProviderUseCase: LinkAuthProviderUseCase, + unlinkProviderUseCase: UnlinkAuthProviderUseCase + ) { + self.fetchProvidersUseCase = fetchProvidersUseCase + self.linkProviderUseCase = linkProviderUseCase + self.unlinkProviderUseCase = unlinkProviderUseCase + } func reduce(with action: Action) -> [SideEffect] { var state = self.state switch action { case .onAppear: - + return [.fetch] case .linkWithProvider(let value): return [.link(value)] case .unlinkFromProvider(let value): @@ -63,6 +78,11 @@ final class AccountViewModel: Store { setToast(&state, isPresented: isPresented, type: type) case .setLoading(let value): state.isLoading = value + case .updateProviders(let currentProvider, let allProviders): + state.currentProvider = currentProvider + state.connectedProviders = allProviders.filter { $0 != currentProvider } + state.disconnectedProviders = AuthProvider.allCases + .filter { !allProviders.contains($0) } } self.state = state @@ -71,13 +91,26 @@ final class AccountViewModel: Store { func run(_ effect: SideEffect) { switch effect { + case .fetch: + Task { + do { + let (currentProvider, allProviders) = try await fetchProvidersUseCase.execute() + send(.updateProviders(currentProvider: currentProvider, allProviders: allProviders)) + } catch { + send(.setAlert(isPresented: true, type: .error)) + } + } case .link(let provider): Task { do { defer { send(.setLoading(false)) } send(.setLoading(true)) - + + try await linkProviderUseCase.execute(provider) send(.setToast(isPresented: true, type: .linkSuccess)) + + let (currentProvider, allProviders) = try await fetchProvidersUseCase.execute() + send(.updateProviders(currentProvider: currentProvider, allProviders: allProviders)) } catch { send(.setAlert(isPresented: true, type: .error)) } @@ -87,8 +120,12 @@ final class AccountViewModel: Store { do { defer { send(.setLoading(false)) } send(.setLoading(true)) - + + try await unlinkProviderUseCase.execute(provider) send(.setToast(isPresented: true, type: .unlinkSuccess)) + + let (currentProvider, allProviders) = try await fetchProvidersUseCase.execute() + send(.updateProviders(currentProvider: currentProvider, allProviders: allProviders)) } catch { send(.setAlert(isPresented: true, type: .error)) } diff --git a/DevLog/Resource/Localizable.xcstrings b/DevLog/Resource/Localizable.xcstrings index 889b655..8e25a0a 100644 --- a/DevLog/Resource/Localizable.xcstrings +++ b/DevLog/Resource/Localizable.xcstrings @@ -253,6 +253,9 @@ }, "검색" : { + }, + "계정 삭제" : { + }, "계정 연동" : { @@ -292,6 +295,9 @@ }, "상태 설정" : { + }, + "새 계정 연동" : { + }, "생성" : { @@ -322,6 +328,9 @@ }, "어제" : { + }, + "연동된 계정" : { + }, "완료" : { @@ -388,6 +397,9 @@ }, "필터 옵션" : { + }, + "현재 계정" : { + }, "홈" : { diff --git a/DevLog/UI/Setting/AccountView.swift b/DevLog/UI/Setting/AccountView.swift index f75f38f..87ba501 100644 --- a/DevLog/UI/Setting/AccountView.swift +++ b/DevLog/UI/Setting/AccountView.swift @@ -15,13 +15,14 @@ struct AccountView: View { List { Section("현재 계정") { HStack { - let provider = viewModel.state.currentProvider - let formattedProvider = formattedProviderName(provider) - Image(formattedProvider) - .resizable() - .scaledToFit() - .frame(width: UIFont.labelFontSize) - Text(formattedProvider) + if let provider = viewModel.state.currentProvider { + let formattedProvider = formattedProviderName(provider) + Image(formattedProvider) + .resizable() + .scaledToFit() + .frame(width: UIFont.labelFontSize) + Text(formattedProvider) + } } } Section("연동된 계정") { @@ -45,23 +46,6 @@ struct AccountView: View { } } .listStyle(.insetGrouped) - .onAppear { - viewModel.send(.onAppear) -// connectedProviders = viewModel.providers.filter { provider in -// provider != viewModel.currentProvider -// } -// disconnectedProviders = ["google.com", "github.com", "apple.com"].filter { provider in -// !viewModel.providers.contains(provider) -// } - } -// .onChange(of: viewModel.providers) { newProviders in -// connectedProviders = newProviders.filter { provider in -// provider != viewModel.currentProvider -// } -// disconnectedProviders = ["google.com", "github.com", "apple.com"].filter { provider in -// !newProviders.contains(provider) -// } -// } .navigationTitle("계정 연동") .toolbar { ToolbarItem(placement: .topBarTrailing) { @@ -83,6 +67,9 @@ struct AccountView: View { } } } + .onAppear { + viewModel.send(.onAppear) + } .alert(viewModel.state.alertTitle, isPresented: Binding( get: { viewModel.state.showAlert }, set: { viewModel.send(.setAlert(isPresented: $0)) } @@ -91,12 +78,17 @@ struct AccountView: View { } message: { Text(viewModel.state.alertMessage) } + .overlay { + if viewModel.state.isLoading { + LoadingView() + } + } } - - private func formattedProviderName(_ provider: String) -> String { - // provider에서 첫번째 글자만 대문자로 바꾸고 .을 포함한 뒤는 다 제거 ex) google.com -> Google - let providerPrefix = provider.prefix(1).uppercased() - let providerSuffix = provider.dropFirst().prefix(while: { $0 != "." }) + + private func formattedProviderName(_ provider: AuthProvider) -> String { + let rawValue = provider.rawValue + let providerPrefix = rawValue.prefix(1).uppercased() + let providerSuffix = rawValue.dropFirst().prefix(while: { $0 != "." }) return providerPrefix + providerSuffix } } diff --git a/DevLog/UI/Setting/SettingView.swift b/DevLog/UI/Setting/SettingView.swift index ae33e8b..36f8931 100644 --- a/DevLog/UI/Setting/SettingView.swift +++ b/DevLog/UI/Setting/SettingView.swift @@ -115,8 +115,13 @@ struct SettingView: View { ) ) case .account: - TempView(text: "AccountView") -// AccountView(viewModel: viewModel) + AccountView( + viewModel: AccountViewModel( + fetchProvidersUseCase: container.resolve(FetchAuthProvidersUseCase.self), + linkProviderUseCase: container.resolve(LinkAuthProviderUseCase.self), + unlinkProviderUseCase: container.resolve(UnlinkAuthProviderUseCase.self) + ) + ) } } .alert("로그아웃", isPresented: Binding( From 2264fc197cb0663ef943ddaea3c95d717ba180c6 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 12 Feb 2026 10:08:35 +0900 Subject: [PATCH 5/6] =?UTF-8?q?chore:=20node.js=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Firebase/functions/package-lock.json | 30 +++++++++++++++++++++------- Firebase/functions/package.json | 1 + 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Firebase/functions/package-lock.json b/Firebase/functions/package-lock.json index caaba0f..10924b2 100644 --- a/Firebase/functions/package-lock.json +++ b/Firebase/functions/package-lock.json @@ -14,6 +14,7 @@ "jsonwebtoken": "^9.0.2" }, "devDependencies": { + "@types/node": "^25.2.3", "firebase-functions-test": "^3.1.0", "typescript": "^4.9.0" }, @@ -1736,12 +1737,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", - "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/qs": { @@ -3128,6 +3129,21 @@ "@google-cloud/storage": "^7.7.0" } }, + "node_modules/firebase-admin/node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/firebase-admin/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/firebase-functions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-7.0.0.tgz", @@ -6408,9 +6424,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "node_modules/unpipe": { diff --git a/Firebase/functions/package.json b/Firebase/functions/package.json index e4e3a47..ec65858 100644 --- a/Firebase/functions/package.json +++ b/Firebase/functions/package.json @@ -22,6 +22,7 @@ "jsonwebtoken": "^9.0.2" }, "devDependencies": { + "@types/node": "^25.2.3", "firebase-functions-test": "^3.1.0", "typescript": "^4.9.0" }, From 1ac58c35c6d310c452a8c7084aa9b1cd5d811838 Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 12 Feb 2026 10:25:30 +0900 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Setting/AccountView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DevLog/UI/Setting/AccountView.swift b/DevLog/UI/Setting/AccountView.swift index 87ba501..8e0b767 100644 --- a/DevLog/UI/Setting/AccountView.swift +++ b/DevLog/UI/Setting/AccountView.swift @@ -45,6 +45,7 @@ struct AccountView: View { } } } + .scrollDisabled(true) .listStyle(.insetGrouped) .navigationTitle("계정 연동") .toolbar {