-
Notifications
You must be signed in to change notification settings - Fork 0
[Feature] 회원 탈퇴 로직 구현 #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4069505
e198233
97e8643
9a02d76
8885ca4
e63d897
dc9905b
29a3664
588c9e6
d47d053
e8b9b2f
19cd407
5fe9af4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<T: Codable>(_ value: T, forKey key: String) throws | ||
| func load<T: Codable>(forKey key: String) throws -> T? | ||
| func delete(forKey key: String) throws | ||
| } | ||
|
|
||
| struct KeychainStoreImpl: KeychainStore { | ||
|
|
||
| func save<T: Codable>(_ 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<T: Codable>(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) | ||
| } | ||
| } | ||
| } |
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 외부 시스템과 통신하는 코드까지 유스케이스에 하드코딩이 되어있는 상태입니다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. execute |
||
| /// 애플 로그인 정보에 필요한 정보들이 누락되는 경우 | ||
| 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)!)! | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 안전하게 처리 필요 |
||
|
|
||
| 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: .google | ||
| ) | ||
| authUserManager.save(authUser) | ||
| } | ||
| } | ||
|
|
||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Auth 모듈 분리 후 리팩토링 예정 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() { | ||
| // TODO: Core(Auth) 모듈에서 처리하도록 리팩토링 | ||
| try? Auth.auth().signOut() | ||
|
|
||
| /// 구글 사용자인 경우 | ||
| if authUserManager.user?.provider == .google { | ||
| GIDSignIn.sharedInstance.signOut() | ||
| } | ||
|
Comment on lines
+26
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. switch apple: break // 애플 로그아웃 API 제공하지 않음 |
||
|
|
||
| authUserManager.clear() | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
keychain에 저장하기 위해 Codable 프로토콜을 채택했습니다.