✅ 회원 탈퇴 로직
- Firebase Storage에서 프로필 이미지 삭제
→ deleteProfilePhoto(for: userID)
→ 이미지가 없으면 실패해도 계속 진행 (catch { _ in Just(()) }) - Firestore에서 유저 데이터 삭제
→ collectionUsers(deleteUser: userID) - Firebase Authentication에서 유저 계정 삭제
→ deleteAccount()
1️⃣ Firebase Storage 에서 프로필 이미지 삭제
final class StorageManager {
// MARK: - Variable
static let shared = StorageManager()
let storage = Storage.storage()
// MARK: - Function
/// 프로필 이미지를 firestore에 저장하는 메서드
func uploadProfilePhoto(with userID: String, image: Data, metaData: StorageMetadata) -> AnyPublisher<StorageMetadata, Error> {
...
}
func getDownloadURL(for id: String?) -> AnyPublisher<URL, Error> {
guard let id = id else { return Fail(error: FireStorageError.invalidImageID).eraseToAnyPublisher() }
return storage
.reference(withPath: id)
.downloadURL()
.print()
.eraseToAnyPublisher()
}
/// ✅ 특정 유저 ID를 받아 해당 유저의 프로필 이미지를 Firebase Storage에서 삭제
func deleteProfilePhoto(for userID: String) -> AnyPublisher<Void, Error> {
return getDownloadURL(for: "images/\(userID)/profileImage/profileImage.jpg") // ✅ Firebase에서 이미지 URL 가져오기
.flatMap { imageURL in
let reference = Storage.storage().reference(forURL: imageURL.absoluteString)
return Future<Void, Error> { promise in
reference.delete { error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(())) // ✅ 성공 시 Void 반환
}
}
}
}
.catch { _ in Just(()).setFailureType(to: Error.self) } // ✅ 이미지가 없을 경우 삭제 실패해도 계속 진행
.eraseToAnyPublisher()
}
}
✅ flatMap을 사용하면 getDownloadURL()의 결과를 받은 후, 그 값을 사용하여 새로운 비동기 작업(Publisher)을 생성할 수 있다.
✅ Firebase에서 URL을 받아오는 작업과, 해당 URL을 사용한 삭제 작업을 순차적으로 실행할 수 있도록 보장한다.
✅ 이 방법이 없으면 reference.delete()를 바로 실행할 수 없고, 비동기 흐름이 깨지게 된다.
2️⃣ DatabaseManager - Firestore 에서 유저 데이터 삭제
class DatabaseManager {
// MARK: - Variable
static let shared = DatabaseManager()
let db = Firestore.firestore()
let userPath: String = "users"
// MARK: - Function
/// 회원정보를 fireStore에 저장하는 메서드
func collectionUsers(add user: User) -> AnyPublisher<Bool, Error> {
...
}
/// 회원정보를 반환하는 메서드
func collectionUsers(retrieve id: String) -> AnyPublisher<MovieClipUser, Error> {
...
}
func collectionUsers(updateFields: [String: Any], for id: String) -> AnyPublisher<Bool, Error> {
...
}
/// ✅ 특정 유저 ID를 받아 해당 유저의 Firestore 데이터를 삭제
func collectionUsers(deleteUser userID: String) -> AnyPublisher<Void, Error> {
return Future<Void, Error> { promise in
self.db.collection(self.userPath).document(userID).delete { error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(()))
}
}
}
.eraseToAnyPublisher()
}
}
3️⃣ AuthManager - Firebase Authentication에서 회원 탈퇴
class AuthManager {
// MARK: - Variable
static let shared = AuthManager()
private init() { }
// MARK: - Function
/// 이메일과 비밀번호로 회원가입 하는 함수 <Future는 필수가 아님>
func registerUser(email: String, password: String) -> AnyPublisher<User, Error> {
...
}
/// 이메일과 비밀번호로 로그인하는 함수
func loginUser(email: String, password: String) -> AnyPublisher<User, Error> {
...
}
/// ✅ Firebase Authentication에서 회원 탈퇴
func deleteUser() -> AnyPublisher<Void, Error> {
guard let user = Auth.auth().currentUser else {
return Fail(error: NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "사용자가 로그인되어 있지 않습니다."]))
.eraseToAnyPublisher()
}
return Future<Void, Error> { promise in
user.delete { error in
if let error = error {
promise(.failure(error))
} else {
promise(.success(()))
}
}
}
.eraseToAnyPublisher()
}
}
4️⃣ ProfileViewModel - 회원 탈퇴 기능 구현
final class ProfileViewModel: ObservableObject {
@Published var user: MovieClipUser // ✅ 유저 정보 (초기값 기본값으로 설정)
@Published var error: String?
@Published var isUserDeleted: Bool = false // ✅ 회원 탈퇴 완료 여부
private var cancellable: Set<AnyCancellable> = []
// ✅ 빈 유저로 초기화 후, Firebas에서 업데이트
init(user: MovieClipUser = MovieClipUser()) {
self.user = user
retreiveUser()
}
func retreiveUser() {
...
}
// ✅ 회원 탈퇴 기능 - Firebase Storage, Firestore, Authentication 순차적 삭제
func deleteUser() {
guard let userID = Auth.auth().currentUser?.uid else {
self.error = "유저 정보 없음"
return
}
StorageManager.shared.deleteProfilePhoto(for: userID)
.flatMap { DatabaseManager.shared.collectionUsers(deleteUser: userID) }
.flatMap { AuthManager.shared.deleteUser() }
.sink { [weak self] completion in
switch completion {
case .failure(let error):
self?.error = "회원 탈퇴 실패: \(error.localizedDescription)"
case .finished:
self?.isUserDeleted = true // 회원 탈퇴 완료 처리
}
} receiveValue: { }
.store(in: &cancellable)
}
}
5️⃣ ProfileViewController 에서 회원 탈퇴 실행
private func deleteUser() {
viewModel.deleteUser()
viewModel.$isUserDeleted
.sink { isDeleted in
if isDeleted {
print("회원 탈퇴")
// ✅ 기존의 모든 화면을 닫고, OnboardingViewController로 이동
DispatchQueue.main.async {
self.view.window?.rootViewController?.dismiss(animated: true, completion: {
if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate {
sceneDelegate.window?.rootViewController = UINavigationController(rootViewController: OnboardingViewController())
}
})
}
}
}
.store(in: &cancelable)
}
🔥 최종 결과
- Firebase Storage의 프로필 이미지 삭제
- Firestore 유저 데이터 삭제
- Firebase Authentication에서 회원 탈퇴
- 탈퇴 후 온보딩 화면으로 이동
'Project > MovieClip' 카테고리의 다른 글
❌ 컬렉션 뷰를 가로 스크롤할 때 다음 이미지가 살짝 보이는 문제 (0) | 2025.03.11 |
---|---|
❌ 컴파일 오류 발생... (0) | 2025.03.09 |
✅ 프로필 수정하기! (기존 프로필 입력 창 사용하기) (0) | 2025.03.04 |
❌ 문제 해결... ProfileItem이 Hashable 및 Equatable 프로토콜을 준수하지 않는다? (0) | 2025.03.01 |
💾 Firestore 에 유저 정보 저장 및 이미지 업로드 (0) | 2025.02.28 |