diff --git a/Mark-In/Sources/App/DIContainer+.swift b/Mark-In/Sources/App/DIContainer+.swift index b2f26d6..f73f900 100644 --- a/Mark-In/Sources/App/DIContainer+.swift +++ b/Mark-In/Sources/App/DIContainer+.swift @@ -75,7 +75,9 @@ private extension DIContainer { ) let withdrawalUseCase: WithdrawalUseCase = WithdrawalUseCaseImpl( keychainStore: resolve(), - authUserManager: resolve() + authUserManager: resolve(), + linkRepository: resolve(), + folderRepsoitory: resolve() ) register(fetchLinkListUseCase) diff --git a/Mark-In/Sources/Data/Repositories/FolderRepositoryImpl.swift b/Mark-In/Sources/Data/Repositories/FolderRepositoryImpl.swift index 7c87bb8..a5db1b4 100644 --- a/Mark-In/Sources/Data/Repositories/FolderRepositoryImpl.swift +++ b/Mark-In/Sources/Data/Repositories/FolderRepositoryImpl.swift @@ -92,4 +92,24 @@ struct FolderRepositoryImpl: FolderRepository { /// 2. Folder 삭제 try await folderDocRef.delete() } + + func deleteAll(userID: String) async throws { + /// 1. Folders 컬렉션 참조 생성 + let path = FirebaseEndpoint.FirestoreDB.folders(userID: userID).path + let folderColRef = db.collection(path) + + /// 2. 컬렉션의 모든 문서 가져오기 + let snapshot = try await folderColRef.getDocuments() + + /// 3. 모든 데이터 병렬 작업으로 삭제 + try await withThrowingTaskGroup(of: Void.self) { group in + snapshot.documents.forEach { document in + group.addTask { + try await document.reference.delete() + } + } + + try await group.waitForAll() + } + } } diff --git a/Mark-In/Sources/Data/Repositories/LinkRepositoryImpl.swift b/Mark-In/Sources/Data/Repositories/LinkRepositoryImpl.swift index ca8e109..f752726 100644 --- a/Mark-In/Sources/Data/Repositories/LinkRepositoryImpl.swift +++ b/Mark-In/Sources/Data/Repositories/LinkRepositoryImpl.swift @@ -125,6 +125,33 @@ struct LinkRepositoryImpl: LinkRepository { /// 3. Link 삭제 try await linkDocRef.delete() } + + func deleteAll(userID: String) async throws { + /// 1. Links 컬렉션 참조 생성 + let path = FirebaseEndpoint.FirestoreDB.links(userID: userID).path + let linkColRef = db.collection(path) + + /// 2. 컬렉션의 모든 문서 가져오기 + let snapshot = try await linkColRef.getDocuments() + + /// 3. 병렬 작업으로 데이터 삭제 + try await withThrowingTaskGroup(of: Void.self) { group in + /// 모둔 문서에 순차 접근 + snapshot.documents.forEach { document in + /// Link 데이터 삭제 + group.addTask { + try await document.reference.delete() + } + + /// 이미지 데이터 삭제 + group.addTask { + try await deleteImageData(userID: userID, fileID: document.documentID) + } + } + + try await group.waitForAll() + } + } } private extension LinkRepositoryImpl { diff --git a/Mark-In/Sources/Domain/Interfaces/FolderRepository.swift b/Mark-In/Sources/Domain/Interfaces/FolderRepository.swift index f0f472e..c2152cd 100644 --- a/Mark-In/Sources/Domain/Interfaces/FolderRepository.swift +++ b/Mark-In/Sources/Domain/Interfaces/FolderRepository.swift @@ -12,4 +12,5 @@ protocol FolderRepository { func fetchAll(userID: String) async throws -> [Folder] func update(userID: String, folder: Folder) async throws func delete(userID: String, folder: Folder) async throws + func deleteAll(userID: String) async throws } diff --git a/Mark-In/Sources/Domain/Interfaces/LinkRepository.swift b/Mark-In/Sources/Domain/Interfaces/LinkRepository.swift index 32c665f..8238596 100644 --- a/Mark-In/Sources/Domain/Interfaces/LinkRepository.swift +++ b/Mark-In/Sources/Domain/Interfaces/LinkRepository.swift @@ -12,4 +12,5 @@ protocol LinkRepository { func fetchAll(userID: String) async throws -> [WebLink] func update(userID: String, link: WebLink) async throws func delete(userID: String, link: WebLink) async throws + func deleteAll(userID: String) async throws } diff --git a/Mark-In/Sources/Domain/UseCases/Implements/WithdrawalUseCaseImpl.swift b/Mark-In/Sources/Domain/UseCases/Implements/WithdrawalUseCaseImpl.swift index a98af7c..05335a4 100644 --- a/Mark-In/Sources/Domain/UseCases/Implements/WithdrawalUseCaseImpl.swift +++ b/Mark-In/Sources/Domain/UseCases/Implements/WithdrawalUseCaseImpl.swift @@ -15,40 +15,75 @@ struct WithdrawalUseCaseImpl: WithdrawalUseCase { private let keychainStore: KeychainStore private let authUserManager: AuthUserManager - init(keychainStore: KeychainStore, authUserManager: AuthUserManager) { + private let linkRepository: LinkRepository + private let folderRepsoitory: FolderRepository + + init( + keychainStore: KeychainStore, + authUserManager: AuthUserManager, + linkRepository: LinkRepository, + folderRepsoitory: FolderRepository + ) { self.keychainStore = keychainStore self.authUserManager = authUserManager + self.linkRepository = linkRepository + self.folderRepsoitory = folderRepsoitory } func execute() async throws { + /// 현재 로그인 된 유저 상태 확인 후 인증 정보 삭제 do { try await Auth.auth().currentUser?.delete() } catch { throw WithdrawalError.credentialTooOld } - guard let provider = authUserManager.user?.provider else { return } + guard let user = authUserManager.user else { return } - switch provider { - case .apple: - let token: String? = try? keychainStore.load(forKey: .refreshToken) - guard let token else { return } + /// 사용자의 모든 데이터(링크, 폴더) 삭제 + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + try await linkRepository.deleteAll(userID: user.id) + } - let url = URL(string: "https://\(Config.value(forKey: .revokeTokenURL))/revokeToken?refresh_token=\(token)" - .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)! + group.addTask { + try await folderRepsoitory.deleteAll(userID: user.id) + } - _ = try await URLSession.shared.data(from: url) - - try keychainStore.delete(forKey: .refreshToken) + try await group.waitForAll() + } + + /// OAuth 인증 해제 + switch user.provider { + case .apple: + try await revokeAppleAuthorization() case .google: - try? await GIDSignIn.sharedInstance.disconnect() - GIDSignIn.sharedInstance.signOut() + try await revokeGoogleAuthorization() @unknown default: - fatalError("") + fatalError("Do not implement revoke function for provider: \(user.provider)") } authUserManager.clear() } } + +private extension WithdrawalUseCaseImpl { + func revokeAppleAuthorization() async throws { + 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) + } + + func revokeGoogleAuthorization() async throws { + try? await GIDSignIn.sharedInstance.disconnect() + GIDSignIn.sharedInstance.signOut() + } +}