From 4069505e3ce05f4bfbfb9303339fc09aa71bb5cc Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Wed, 14 May 2025 15:49:23 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[#37]=20Firebase=20Function=20.gitignore?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6efbaaa..e25c661 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ Configuration/ ### Firebase ### GoogleService-Info*.plist +functions +.firebaserc +firebase.json ### macOS ### .DS_Store From e1982335999e7cb1150e51a5116172505c81e3e3 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Wed, 14 May 2025 19:44:52 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[#37]=20Firebase=20Function=20URL=20?= =?UTF-8?q?=ED=82=A4=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Resources/Info.plist | 4 ++++ Mark-In/Sources/App/Config.swift | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 Mark-In/Sources/App/Config.swift diff --git a/Mark-In/Resources/Info.plist b/Mark-In/Resources/Info.plist index a4b7330..9ab63ae 100644 --- a/Mark-In/Resources/Info.plist +++ b/Mark-In/Resources/Info.plist @@ -2,6 +2,10 @@ + RevokeTokenURL + $(REVOKE_TOKEN_URL) + GetRefreshTokenURL + $(GET_REFRESH_TOKEN_URL) GIDClientID $(GID_CLIENT) CFBundleURLTypes diff --git a/Mark-In/Sources/App/Config.swift b/Mark-In/Sources/App/Config.swift new file mode 100644 index 0000000..904a994 --- /dev/null +++ b/Mark-In/Sources/App/Config.swift @@ -0,0 +1,22 @@ +// +// Config.swift +// Mark-In +// +// Created by 이정동 on 5/14/25. +// + +import Foundation + +struct Config { + enum Key: String { + case getRefreshTokenURL = "GetRefreshTokenURL" + case revokeTokenURL = "RevokeTokenURL" + } + + static func value(forKey: Self.Key) -> String { + guard let value = Bundle.main.object(forInfoDictionaryKey: forKey.rawValue) as? String else { + fatalError("\(forKey.rawValue) not set") + } + return value + } +} From 97e8643b6bee7ccd508d1cffe80b0799092215db Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 14:55:04 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[#37]=20KeyChainStore=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/App/KeyChainStore.swift | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 Mark-In/Sources/App/KeyChainStore.swift diff --git a/Mark-In/Sources/App/KeyChainStore.swift b/Mark-In/Sources/App/KeyChainStore.swift new file mode 100644 index 0000000..156b022 --- /dev/null +++ b/Mark-In/Sources/App/KeyChainStore.swift @@ -0,0 +1,88 @@ +// +// KeyChainStore.swift +// Mark-In +// +// Created by 이정동 on 5/14/25. +// + + + + +// TODO: Core 모듈로 이동 + +import Foundation +import Security + +enum KeychainError: Error { + case encodingFailed + case decodingFailed + case unexpectedStatus(OSStatus) +} + +protocol KeychainStore { + func save(_ value: T, forKey key: String) throws + func load(forKey key: String) throws -> T? + func delete(forKey key: String) throws +} + +struct KeychainStoreImpl: KeychainStore { + + func save(_ value: T, forKey key: String) throws { + let data: Data + do { + data = try JSONEncoder().encode(value) + } catch { + throw KeychainError.encodingFailed + } + + // 기존 항목 삭제 후 저장 + try delete(forKey: key) + + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key, + kSecValueData as String: data + ] + + let status = SecItemAdd(query as CFDictionary, nil) + guard status == errSecSuccess else { + throw KeychainError.unexpectedStatus(status) + } + } + + func load(forKey key: String) throws -> T? { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key, + kSecReturnData as String: true, + kSecMatchLimit as String: kSecMatchLimitOne + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + + guard status == errSecSuccess || status == errSecItemNotFound else { + throw KeychainError.unexpectedStatus(status) + } + + guard let data = result as? Data else { return nil } + + do { + return try JSONDecoder().decode(T.self, from: data) + } catch { + throw KeychainError.decodingFailed + } + } + + func delete(forKey key: String) throws { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key + ] + + let status = SecItemDelete(query as CFDictionary) + guard status == errSecSuccess || status == errSecItemNotFound else { + throw KeychainError.unexpectedStatus(status) + } + } +} From 9a02d764f08a8fe2d4f125dd1bd1b91a90aa2bd2 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 16:02:20 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[#37]=20AuthUserManager=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/App/AuthUserManager.swift | 19 ++++++++++++++----- Mark-In/Sources/App/DIContainer.swift | 5 ++++- Shared/Util/Sources/OAuthProvider.swift | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Mark-In/Sources/App/AuthUserManager.swift b/Mark-In/Sources/App/AuthUserManager.swift index 871285c..ce639e2 100644 --- a/Mark-In/Sources/App/AuthUserManager.swift +++ b/Mark-In/Sources/App/AuthUserManager.swift @@ -9,10 +9,13 @@ import Foundation -import FirebaseAuth +import Util -struct AuthUser { +struct AuthUser: Codable { let id: String + let name: String + let email: String + let provider: SocialSignInProvider } protocol AuthUserManager { @@ -25,22 +28,28 @@ protocol AuthUserManager { @Observable final class AuthUserManagerImpl: AuthUserManager { + + private let keychainStore: KeychainStore + var user: AuthUser? - init() { + init(keychainStore: KeychainStore) { + self.keychainStore = keychainStore self.load() } func load() { - guard let currentUser = Auth.auth().currentUser else { return } - self.user = AuthUser(id: currentUser.uid) + let user: AuthUser? = try? keychainStore.load(forKey: "authUser") + self.user = user } func save(_ user: AuthUser) { + try? keychainStore.save(user, forKey: "authUser") self.user = user } func clear() { + try? keychainStore.delete(forKey: "authUser") user = nil } } diff --git a/Mark-In/Sources/App/DIContainer.swift b/Mark-In/Sources/App/DIContainer.swift index 801a09d..cf090e2 100644 --- a/Mark-In/Sources/App/DIContainer.swift +++ b/Mark-In/Sources/App/DIContainer.swift @@ -38,8 +38,11 @@ extension DIContainer { func registerDependencies() { /// Core + let keychainStore: KeychainStore = KeychainStoreImpl() let linkMetadataProvider: LinkMetadataProvider = LinkMetadataProviderImpl() - let authUserManager: AuthUserManager = AuthUserManagerImpl() + let authUserManager: AuthUserManager = AuthUserManagerImpl( + keychainStore: keychainStore + ) register(linkMetadataProvider) register(authUserManager) diff --git a/Shared/Util/Sources/OAuthProvider.swift b/Shared/Util/Sources/OAuthProvider.swift index c5cb681..5445bad 100644 --- a/Shared/Util/Sources/OAuthProvider.swift +++ b/Shared/Util/Sources/OAuthProvider.swift @@ -7,7 +7,7 @@ import Foundation -public enum SocialSignInProvider { +public enum SocialSignInProvider: Codable { case apple case google } From 8885ca4f4907e000b54e3efee3b0f8849c19c003 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 16:10:38 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[#37]=20SignInUseCase=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implements/SignInUseCaseImpl.swift | 94 +++++++++++++++++++ .../UseCases/Interfaces/SignInUseCase.swift | 32 +++++++ 2 files changed, 126 insertions(+) create mode 100644 Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift create mode 100644 Mark-In/Sources/Domain/UseCases/Interfaces/SignInUseCase.swift diff --git a/Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift new file mode 100644 index 0000000..43b5e00 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift @@ -0,0 +1,94 @@ +// +// SignInUseCaseImpl.swift +// Mark-In +// +// Created by 이정동 on 5/15/25. +// + +import Foundation + +import FirebaseAuth + +import Util + +struct SignInUseCaseImpl: SignInUseCase { + + private let keychainStore: KeychainStore + private let authUserManager: AuthUserManager + + init( + keychainStore: KeychainStore, + authUserManager: AuthUserManager + ) { + self.keychainStore = keychainStore + self.authUserManager = authUserManager + } + + func signIn(using info: AppleSignInInfo) async throws { + /// 애플 로그인 정보에 필요한 정보들이 누락되는 경우 + guard let nonce = info.nonce, + let appleIDToken = info.idCredential.identityToken, + let idTokenString = String(data: appleIDToken, encoding: .utf8), + let authorizationCode = info.idCredential.authorizationCode, + let codeString = String(data: authorizationCode, encoding: .utf8) else { + throw SignInError.missingData + } + + // TODO: 아래 코드들을 Core(Auth) 모듈에서 처리하도록 리팩토링 + /// 애플 서버에 Refresh Token 요청 + let urlString = "https://\(Config.value(forKey: .getRefreshTokenURL))/getRefreshToken?code=\(codeString)" + let url = URL(string: urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! + + let (data, _) = try await URLSession.shared.data(from: url) + let refreshToken = String(data: data, encoding: .utf8) ?? "" + + /// Refresh Token 저장 + try keychainStore.save(refreshToken, forKey: "refreshToken") + + /// Firebase 인증 요청을 위한 AuthCredential 생성 + let credential = OAuthProvider.appleCredential( + withIDToken: idTokenString, + rawNonce: nonce, + fullName: info.idCredential.fullName + ) + + /// Firebase 인증 요청 + let response = try await Auth.auth().signIn(with: credential) + + /// AuthUserManager에 유저 데이터 저장 + let authUser = AuthUser( + id: response.user.uid, + name: response.user.displayName ?? "-", + email: response.user.email ?? "-", + provider: .apple + ) + authUserManager.save(authUser) + } + + func signIn(using info: GoogleSignInInfo) async throws { + /// 로그인에 필요한 정보(IDToken)가 누락된 경우 + guard let idToken = info.user.idToken?.tokenString else { + throw SignInError.missingData + } + + // TODO: 아래 코드들을 Core(Auth) 모듈에서 처리하도록 리팩토링 + /// Firebase 인증 요청을 위한 AuthCredential 생성 + let credential = GoogleAuthProvider.credential( + withIDToken: idToken, + accessToken: info.user.accessToken.tokenString + ) + + /// Firebase 인증 요청 + let response = try await Auth.auth().signIn(with: credential) + + /// AuthUserManager에 유저 데이터 저장 + let authUser = AuthUser( + id: response.user.uid, + name: response.user.displayName ?? "-", + email: response.user.email ?? "-", + provider: .apple + ) + authUserManager.save(authUser) + } +} + diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/SignInUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/SignInUseCase.swift new file mode 100644 index 0000000..5ea9db5 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/SignInUseCase.swift @@ -0,0 +1,32 @@ +// +// SignInUseCase.swift +// Mark-In +// +// Created by 이정동 on 5/15/25. +// + +import AuthenticationServices +import Foundation + +import GoogleSignIn + +import Util + +enum SignInError: Error { + case missingData + case invalid +} + +struct AppleSignInInfo { + let nonce: String? + let idCredential: ASAuthorizationAppleIDCredential +} + +struct GoogleSignInInfo { + let user: GIDGoogleUser +} + +protocol SignInUseCase { + func signIn(using info: AppleSignInInfo) async throws + func signIn(using info: GoogleSignInInfo) async throws +} From e63d897519b1ece24bdb04ae0c5431d016713dac Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 16:11:04 +0900 Subject: [PATCH 06/13] =?UTF-8?q?[#37]=20SignInUseCase=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20=EB=B0=8F=20LoginView?= =?UTF-8?q?Model=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/App/DIContainer.swift | 5 ++ .../Feature/Login/LoginViewModel.swift | 83 ++++--------------- 2 files changed, 22 insertions(+), 66 deletions(-) diff --git a/Mark-In/Sources/App/DIContainer.swift b/Mark-In/Sources/App/DIContainer.swift index cf090e2..1a83c34 100644 --- a/Mark-In/Sources/App/DIContainer.swift +++ b/Mark-In/Sources/App/DIContainer.swift @@ -69,10 +69,15 @@ extension DIContainer { let generateFolderUseCase: GenerateFolderUseCase = GenerateFolderUseCaseImpl( folderRepository: folderRepository ) + let signInUseCase: SignInUseCase = SignInUseCaseImpl( + keychainStore: keychainStore, + authUserManager: authUserManager + ) register(fetchLinkListUseCase) register(fetchFolderListUseCase) register(generateLinkUseCase) register(generateFolderUseCase) + register(signInUseCase) } } diff --git a/Mark-In/Sources/Feature/Login/LoginViewModel.swift b/Mark-In/Sources/Feature/Login/LoginViewModel.swift index 9a2c7d4..d913d4e 100644 --- a/Mark-In/Sources/Feature/Login/LoginViewModel.swift +++ b/Mark-In/Sources/Feature/Login/LoginViewModel.swift @@ -24,19 +24,17 @@ final class LoginViewModel: Reducer { case appleLoginButtonTapped(AuthorizationController) case googleLoginButtonTapped - case signInError(SignInError) - - case firebaseAuthRequest(AuthCredential) - case firebaseAuthResponse(Result) + case signInError case empty } + private let signInUseCase: SignInUseCase + private(set) var state: State = .init() - private let authUserManager: AuthUserManager init() { - self.authUserManager = DIContainer.shared.resolve() + self.signInUseCase = DIContainer.shared.resolve() } func send(_ action: Action) { @@ -59,25 +57,18 @@ final class LoginViewModel: Reducer { guard let result = try? await authController.performRequest(request), case let .appleID(idCredential) = result else { return .empty } - /// 애플 로그인 정보에 필요한 정보들이 누락되는 경우 - guard let nonce, - let appleIDToken = idCredential.identityToken, - let idTokenString = String(data: appleIDToken, encoding: .utf8) else { - return .signInError(.missingData) - } - - /// Firebase 인증 요청을 위한 AuthCredential 생성 - let credential = OAuthProvider.appleCredential( - withIDToken: idTokenString, - rawNonce: nonce, - fullName: idCredential.fullName - ) + let appleSignInInfo = AppleSignInInfo(nonce: nonce, idCredential: idCredential) - return .firebaseAuthRequest(credential) + do { + try await self.signInUseCase.signIn(using: appleSignInInfo) + return .empty + } catch { + return .signInError + } } case .googleLoginButtonTapped: - + guard let windowScene = NSApplication.shared.windows.first else { return .none } @@ -87,50 +78,17 @@ final class LoginViewModel: Reducer { /// 구글 로그인 요청 let result = try await GIDSignIn.sharedInstance.signIn(withPresenting: windowScene) - /// 로그인에 필요한 정보(IDToken)가 누락된 경우 - guard let idToken = result.user.idToken?.tokenString else { - return .signInError(.missingData) - } + let googleSignInInfo = GoogleSignInInfo(user: result.user) + try await self.signInUseCase.signIn(using: googleSignInInfo) - /// Firebase 인증 요청을 위한 AuthCredential 생성 - let credential = GoogleAuthProvider.credential( - withIDToken: idToken, - accessToken: result.user.accessToken.tokenString - ) - - return .firebaseAuthRequest(credential) + return .empty } catch { - return .signInError(.invalid) + return .signInError } } // TODO: 에러 처리 필요 - case .signInError(_): - return .none - - // TODO: 구글 로그인까지 구현 후 디테일 수정 - case .firebaseAuthRequest(let credential): - return .run { - do { - /// Firebase 인증 요청 - let response = try await Auth.auth().signIn(with: credential) - - return .firebaseAuthResponse(.success(response.user.uid)) - } catch { - return .firebaseAuthResponse(.failure(error)) - } - } - - case .firebaseAuthResponse(let result): - switch result { - case .success(let id): - let user = AuthUser(id: id) - authUserManager.save(user) - case .failure(let error): - // TODO: 에러 처리 필요 - let _ = error as? AuthErrorCode - break - } + case .signInError: return .none case .empty: @@ -150,10 +108,3 @@ final class LoginViewModel: Reducer { } } } - -extension LoginViewModel { - enum SignInError: Error { - case missingData - case invalid - } -} From dc9905b474afa48e54cdeed0e0e2da22774280e7 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 16:18:01 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[#37]=20SignInUseCase=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift index 43b5e00..a83e8e3 100644 --- a/Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift +++ b/Mark-In/Sources/Domain/UseCases/Implements/SignInUseCaseImpl.swift @@ -86,7 +86,7 @@ struct SignInUseCaseImpl: SignInUseCase { id: response.user.uid, name: response.user.displayName ?? "-", email: response.user.email ?? "-", - provider: .apple + provider: .google ) authUserManager.save(authUser) } From 29a36647be112e65ab6cdb9e0675cf3a9b0debfc Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 16:35:42 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[#37]=20SignOutUseCase=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implements/SignOutUseCaseImpl.swift | 32 +++++++++++++++++++ .../UseCases/Interfaces/SignOutUseCase.swift | 12 +++++++ 2 files changed, 44 insertions(+) create mode 100644 Mark-In/Sources/Domain/UseCases/Implements/SignOutUseCaseImpl.swift create mode 100644 Mark-In/Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift diff --git a/Mark-In/Sources/Domain/UseCases/Implements/SignOutUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/SignOutUseCaseImpl.swift new file mode 100644 index 0000000..ef47926 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Implements/SignOutUseCaseImpl.swift @@ -0,0 +1,32 @@ +// +// SignOutUseCaseImpl.swift +// Mark-In +// +// Created by 이정동 on 5/15/25. +// + +import Foundation + +import FirebaseAuth +import GoogleSignIn + +struct SignOutUseCaseImpl: SignOutUseCase { + + private let authUserManager: AuthUserManager + + init(authUserManager: AuthUserManager) { + self.authUserManager = authUserManager + } + + func execute() throws { + // TODO: Core(Auth) 모듈에서 처리하도록 리팩토링 + try Auth.auth().signOut() + + /// 구글 사용자인 경우 + if authUserManager.user?.provider == .google { + GIDSignIn.sharedInstance.signOut() + } + + authUserManager.clear() + } +} diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift new file mode 100644 index 0000000..00a3ad6 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift @@ -0,0 +1,12 @@ +// +// SignOutUseCase.swift +// Mark-In +// +// Created by 이정동 on 5/15/25. +// + +import Foundation + +protocol SignOutUseCase { + func execute() throws +} From 588c9e6b03eb33484b997e5d5dcf4849df0dfb55 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 16:36:15 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[#37]=20SignOutUseCase=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EB=93=B1=EB=A1=9D=20=EB=B0=8F=20MyPageVie?= =?UTF-8?q?wModel=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/App/DIContainer.swift | 4 ++ .../Feature/MyPage/MyPageViewModel.swift | 57 +++++++++---------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Mark-In/Sources/App/DIContainer.swift b/Mark-In/Sources/App/DIContainer.swift index 1a83c34..39c0121 100644 --- a/Mark-In/Sources/App/DIContainer.swift +++ b/Mark-In/Sources/App/DIContainer.swift @@ -73,11 +73,15 @@ extension DIContainer { keychainStore: keychainStore, authUserManager: authUserManager ) + let signOutUseCase: SignOutUseCase = SignOutUseCaseImpl( + authUserManager: authUserManager + ) register(fetchLinkListUseCase) register(fetchFolderListUseCase) register(generateLinkUseCase) register(generateFolderUseCase) register(signInUseCase) + register(signOutUseCase) } } diff --git a/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift b/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift index 1611bb9..584885d 100644 --- a/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift +++ b/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift @@ -20,19 +20,20 @@ final class MyPageViewModel: Reducer { case logoutButtonTapped case withdrawalButtonTapped - case didSuccessLogout - case didFailLogout - case didSuccessWithdrawal case didFailWithdrawal } + private let signOutUseCase: SignOutUseCase private let authUserManager: AuthUserManager + private let tokenStore: KeychainStore private(set) var state: State = .init() init() { + self.signOutUseCase = DIContainer.shared.resolve() self.authUserManager = DIContainer.shared.resolve() + self.tokenStore = KeychainStoreImpl() } func send(_ action: Action) { @@ -43,47 +44,43 @@ final class MyPageViewModel: Reducer { func reduce(state: inout State, action: Action) -> Effect { switch action { case .logoutButtonTapped: - return .run { - // TODO: 이후 Auth 모듈로 분리하면서 코드 리팩토링 예정 - do { - try Auth.auth().signOut() - - if GIDSignIn.sharedInstance.currentUser != nil { - GIDSignIn.sharedInstance.signOut() - } - return .didSuccessLogout - } catch { - return .didFailLogout - } + do { + try signOutUseCase.execute() + } catch { + // TODO: 로그아웃 실패 처리 필요 } + return .none case .withdrawalButtonTapped: return .run { // TODO: 이후 Auth 모듈로 분리하면서 코드 리팩토링 예정 do { - // TODO: 재인증 과정 필요 - // try await Auth.auth().currentUser?.reauthenticate(with: credential) - try await Auth.auth().currentUser?.delete() - if GIDSignIn.sharedInstance.currentUser != nil { - try await GIDSignIn.sharedInstance.disconnect() - GIDSignIn.sharedInstance.signOut() - } + let token: String? = try? self.tokenStore.load(forKey: "refreshToken") + guard let token else { return .didFailWithdrawal } + let url = URL(string: "https://\(Config.value(forKey: .revokeTokenURL))/revokeToken?refresh_token=\(token)" + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! + + _ = try await URLSession.shared.data(from: url) + + try self.tokenStore.delete(forKey: "refreshToken") + +// // TODO: 재인증 과정 필요 +// // try await Auth.auth().currentUser?.reauthenticate(with: credential) +// try await Auth.auth().currentUser?.delete() +// +// if GIDSignIn.sharedInstance.currentUser != nil { +// try await GIDSignIn.sharedInstance.disconnect() +// GIDSignIn.sharedInstance.signOut() +// } +// return .didSuccessWithdrawal } catch { return .didFailWithdrawal } } - case .didSuccessLogout: - authUserManager.clear() - return .none - - // TODO: 로그아웃 실패에 대한 처리 필요 - case .didFailLogout: - return .none - case .didSuccessWithdrawal: authUserManager.clear() return .none From d47d05318aea68f79ecdcd8451b53cfd6f62a278 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 16:50:19 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[#37]=20WithdrawalUseCase=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implements/WithdrawalUseCaseImpl.swift | 54 +++++++++++++++++++ .../Interfaces/WithdrawalUseCase.swift | 16 ++++++ 2 files changed, 70 insertions(+) create mode 100644 Mark-In/Sources/Domain/UseCases/Implements/WithdrawalUseCaseImpl.swift create mode 100644 Mark-In/Sources/Domain/UseCases/Interfaces/WithdrawalUseCase.swift diff --git a/Mark-In/Sources/Domain/UseCases/Implements/WithdrawalUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/WithdrawalUseCaseImpl.swift new file mode 100644 index 0000000..7213c68 --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Implements/WithdrawalUseCaseImpl.swift @@ -0,0 +1,54 @@ +// +// WithdrawalUseCaseImpl.swift +// Mark-In +// +// Created by 이정동 on 5/15/25. +// + +import Foundation + +import FirebaseAuth +import GoogleSignIn + +struct WithdrawalUseCaseImpl: WithdrawalUseCase { + + private let keychainStore: KeychainStore + private let authUserManager: AuthUserManager + + init(keychainStore: KeychainStore, authUserManager: AuthUserManager) { + self.keychainStore = keychainStore + self.authUserManager = authUserManager + } + + func execute() async throws { + do { + try await Auth.auth().currentUser?.delete() + } catch { + throw WithdrawalError.credentialTooOld + } + + guard let provider = authUserManager.user?.provider else { return } + + switch provider { + case .apple: + let token: String? = try? keychainStore.load(forKey: "refreshToken") + guard let token else { return } + + let url = URL(string: "https://\(Config.value(forKey: .revokeTokenURL))/revokeToken?refresh_token=\(token)" + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! + + _ = try await URLSession.shared.data(from: url) + + try keychainStore.delete(forKey: "refreshToken") + + case .google: + try? await GIDSignIn.sharedInstance.disconnect() + GIDSignIn.sharedInstance.signOut() + + @unknown default: + fatalError("") + } + + authUserManager.clear() + } +} diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/WithdrawalUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/WithdrawalUseCase.swift new file mode 100644 index 0000000..52ad45c --- /dev/null +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/WithdrawalUseCase.swift @@ -0,0 +1,16 @@ +// +// WithdrawalUseCase.swift +// Mark-In +// +// Created by 이정동 on 5/15/25. +// + +import Foundation + +enum WithdrawalError: Error { + case credentialTooOld +} + +protocol WithdrawalUseCase { + func execute() async throws +} From e8b9b2f7d4ac33fea60b3d320024f07ab9acdbf0 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 16:50:47 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[#37]=20WithdrawalUseCase=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EB=93=B1=EB=A1=9D=20=EB=B0=8F=20MyPageVie?= =?UTF-8?q?wModel=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/App/DIContainer.swift | 5 +++ .../Feature/MyPage/MyPageViewModel.swift | 40 +++++-------------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/Mark-In/Sources/App/DIContainer.swift b/Mark-In/Sources/App/DIContainer.swift index 39c0121..f353f61 100644 --- a/Mark-In/Sources/App/DIContainer.swift +++ b/Mark-In/Sources/App/DIContainer.swift @@ -76,6 +76,10 @@ extension DIContainer { let signOutUseCase: SignOutUseCase = SignOutUseCaseImpl( authUserManager: authUserManager ) + let withdrawalUseCase: WithdrawalUseCase = WithdrawalUseCaseImpl( + keychainStore: keychainStore, + authUserManager: authUserManager + ) register(fetchLinkListUseCase) register(fetchFolderListUseCase) @@ -83,5 +87,6 @@ extension DIContainer { register(generateFolderUseCase) register(signInUseCase) register(signOutUseCase) + register(withdrawalUseCase) } } diff --git a/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift b/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift index 584885d..89322ce 100644 --- a/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift +++ b/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift @@ -20,20 +20,19 @@ final class MyPageViewModel: Reducer { case logoutButtonTapped case withdrawalButtonTapped - case didSuccessWithdrawal case didFailWithdrawal + + case empty } private let signOutUseCase: SignOutUseCase - private let authUserManager: AuthUserManager - private let tokenStore: KeychainStore + private let withdrawalUseCase: WithdrawalUseCase private(set) var state: State = .init() init() { self.signOutUseCase = DIContainer.shared.resolve() - self.authUserManager = DIContainer.shared.resolve() - self.tokenStore = KeychainStoreImpl() + self.withdrawalUseCase = DIContainer.shared.resolve() } func send(_ action: Action) { @@ -55,39 +54,20 @@ final class MyPageViewModel: Reducer { return .run { // TODO: 이후 Auth 모듈로 분리하면서 코드 리팩토링 예정 do { - - let token: String? = try? self.tokenStore.load(forKey: "refreshToken") - guard let token else { return .didFailWithdrawal } - - let url = URL(string: "https://\(Config.value(forKey: .revokeTokenURL))/revokeToken?refresh_token=\(token)" - .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! - - _ = try await URLSession.shared.data(from: url) - - try self.tokenStore.delete(forKey: "refreshToken") - -// // TODO: 재인증 과정 필요 -// // try await Auth.auth().currentUser?.reauthenticate(with: credential) -// try await Auth.auth().currentUser?.delete() -// -// if GIDSignIn.sharedInstance.currentUser != nil { -// try await GIDSignIn.sharedInstance.disconnect() -// GIDSignIn.sharedInstance.signOut() -// } -// - return .didSuccessWithdrawal + try await self.withdrawalUseCase.execute() + return .empty } catch { + print(error.localizedDescription) return .didFailWithdrawal } } - case .didSuccessWithdrawal: - authUserManager.clear() - return .none - // TODO: 회원탈퇴 실패에 대한 처리 필요 case .didFailWithdrawal: return .none + + case .empty: + return .none } } From 19cd407e90b6cefa8ab3aef80ebb4ed2cd57ef3b Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 17:50:14 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[#37]=20=EC=A3=BC=EC=84=9D=20=EB=B0=8F=20?= =?UTF-8?q?print=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mark-In/Sources/App/Mark_InApp.swift | 6 ++---- Mark-In/Sources/Feature/Login/LoginView.swift | 1 - Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Mark-In/Sources/App/Mark_InApp.swift b/Mark-In/Sources/App/Mark_InApp.swift index 6bbcf17..c22a66f 100644 --- a/Mark-In/Sources/App/Mark_InApp.swift +++ b/Mark-In/Sources/App/Mark_InApp.swift @@ -44,10 +44,8 @@ extension Mark_InApp { #endif guard let filePath = Bundle.main.path(forResource: resource, ofType: "plist"), - let options = FirebaseOptions(contentsOfFile: filePath) - else { - print("Firebase: \(resource) not found or invalid.") - return + let options = FirebaseOptions(contentsOfFile: filePath) else { + fatalError("Firebase: \(resource) not found or invalid.") } FirebaseApp.configure(options: options) diff --git a/Mark-In/Sources/Feature/Login/LoginView.swift b/Mark-In/Sources/Feature/Login/LoginView.swift index 5d39f4e..7c6b5e7 100644 --- a/Mark-In/Sources/Feature/Login/LoginView.swift +++ b/Mark-In/Sources/Feature/Login/LoginView.swift @@ -109,7 +109,6 @@ private struct SignInButtonList: View { } SignInButton(provider: .google) { - // TODO: 구글 로그인 로직 loginViewModel.send(.googleLoginButtonTapped) } } diff --git a/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift b/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift index 89322ce..1562a25 100644 --- a/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift +++ b/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift @@ -57,7 +57,6 @@ final class MyPageViewModel: Reducer { try await self.withdrawalUseCase.execute() return .empty } catch { - print(error.localizedDescription) return .didFailWithdrawal } } From 5fe9af431ece8e28c63df30bac82daa61d1c10b7 Mon Sep 17 00:00:00 2001 From: Jeong Dong Date: Thu, 15 May 2025 18:41:10 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[#37]=20SignOutUseCase=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Domain/UseCases/Implements/SignOutUseCaseImpl.swift | 4 ++-- .../Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift | 2 +- Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Mark-In/Sources/Domain/UseCases/Implements/SignOutUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/SignOutUseCaseImpl.swift index ef47926..ef37622 100644 --- a/Mark-In/Sources/Domain/UseCases/Implements/SignOutUseCaseImpl.swift +++ b/Mark-In/Sources/Domain/UseCases/Implements/SignOutUseCaseImpl.swift @@ -18,9 +18,9 @@ struct SignOutUseCaseImpl: SignOutUseCase { self.authUserManager = authUserManager } - func execute() throws { + func execute() { // TODO: Core(Auth) 모듈에서 처리하도록 리팩토링 - try Auth.auth().signOut() + try? Auth.auth().signOut() /// 구글 사용자인 경우 if authUserManager.user?.provider == .google { diff --git a/Mark-In/Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift b/Mark-In/Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift index 00a3ad6..59ca690 100644 --- a/Mark-In/Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift +++ b/Mark-In/Sources/Domain/UseCases/Interfaces/SignOutUseCase.swift @@ -8,5 +8,5 @@ import Foundation protocol SignOutUseCase { - func execute() throws + func execute() } diff --git a/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift b/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift index 1562a25..5d2fd0f 100644 --- a/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift +++ b/Mark-In/Sources/Feature/MyPage/MyPageViewModel.swift @@ -43,11 +43,7 @@ final class MyPageViewModel: Reducer { func reduce(state: inout State, action: Action) -> Effect { switch action { case .logoutButtonTapped: - do { - try signOutUseCase.execute() - } catch { - // TODO: 로그아웃 실패 처리 필요 - } + signOutUseCase.execute() return .none case .withdrawalButtonTapped: