본문 바로가기

Project/MovieClip

💾 Firestore 에 유저 정보 저장 및 이미지 업로드

 

 

🔷 StorageManager.swift

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> {
        
        return storage
            .reference()
            .child("images/\(userID)/profileImage/profileImage.jpg")
            .putData(image, metadata: metaData)
            .print()
            .eraseToAnyPublisher()
    }
    
    
    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()
    }
}


enum FireStorageError: Error {
    case invalidImageID
}

 

📌 func uploadProfilePhoto() 메서드 역할 

https://explorer89.tistory.com/371

 

📍 Firebase 의 Storage 에 사진 업로드하는 방법

✅ Firebase 의 Storage 에 저장하는 메서드images/{userID}/profileImage/profileImage_{userID}.jpg 경로를 사용해서 사용자별로 폴더를 구분나중에 리뷰 이미지 등 다른 카테고리를 추가할 확장성도 고려이 메서

explorer89.tistory.com

 

📌 getDownloadURL(for:) 메서드의 역할

이 메서드는 Firebase Storage에 저장된 이미지의 다운로드 URL을 가져오는 역할

  • id → 이미지가 저장된 Firebase Storage 내의 경로 (예: "images/user123/profile.jpg")
  • .downloadURL() → Firebase Storage에서 해당 경로의 이미지 다운로드 URL을 가져옴.
  • Fail(error: FireStorageError.invalidImageID).eraseToAnyPublisher() → id가 nil이면 즉시 실패하는 Publisher 반환.

즉, 업로드된 이미지의 경로를 받아서, 해당 이미지의 다운로드 URL을 반환하는 메서드

 

 

🔷 ProfileDataFormViewModel.swift

final class ProfileDataFormViewModel: ObservableObject {
    
    // MARK: - @Published
    @Published var username: String?
    @Published var bio: String?
    @Published var avatarPath: String?
    @Published var imageData: UIImage?
    @Published var isFormValid: Bool = false
    @Published var error: String = ""
    @Published var isOnboardingFinished: Bool = false
    
    private var cancelable: Set<AnyCancellable> = []

    
    // MARK: - Function
    /// 프로필 유효성 검사
    func validateUserProfileForm() {
        guard let username = username, username.count > 2,
              let bio = bio, bio.count > 2,
              imageData != nil else {
            isFormValid = false
            return
        }
        isFormValid = true
    }
    
    
    func uploadAvatar() {
        
        let userID = Auth.auth().currentUser?.uid ?? ""
        guard let imageData = imageData?.jpegData(compressionQuality: 0.5) else { return }
        let metaData = StorageMetadata()
        metaData.contentType = "image/jpeg"
        
        StorageManager.shared.uploadProfilePhoto(with: userID, image: imageData, metaData: metaData)
            .flatMap { metaData in
                StorageManager.shared.getDownloadURL(for: metaData.path)
            }
            .sink { [weak self] completion in
                switch completion {
                case .failure(let error):
                    print(error.localizedDescription)
                    self?.error = error.localizedDescription
                case .finished:
                    self?.updateUserData()
                }
            } receiveValue: { [weak self] url in
                self?.avatarPath = url.absoluteString
            }
            .store(in: &cancelable)
    }
    
    
    private func updateUserData() {
        guard let username,
              let bio,
              let avatarPath,
              let id = Auth.auth().currentUser?.uid else { return }
        
        let updateFields: [String: Any] = [
            "username": username,
            "bio": bio,
            "avatarPath": avatarPath,
            "isUserOnboarded": true
        ]
        
        DatabaseManager.shared.collectionUsers(updateFields: updateFields, for: id)
            .sink { [weak self] completion in
                if case .failure(let error) = completion {
                    print(error.localizedDescription)
                    self?.error = error.localizedDescription
                }
            } receiveValue: { [weak self] onboardingState in
                self?.isOnboardingFinished = onboardingState
            }
            .store(in: &cancelable)

    }
}

📌 uploadAvatar() 메서드 역할 

이 함수는 사용자의 프로필 사진을 Firebase Storage에 업로드하고, 업로드된 이미지의 다운로드 URL을 가져와 저장하는 역할을 한다.

 

 

https://explorer89.tistory.com/372

 

🔥 Firebase Storage에 이미지 업로드, 이미지 주소 URL 가져오기

func uploadAvatar() { let userID = Auth.auth().currentUser?.uid ?? "" // ✅ 유저 ID 가져오기 guard let imageData = imageData?.jpegData(compressionQuality: 0.5) else { return } // ✅ 이미지 데이터 변환 let metaData = StorageMetadata() // Fir

explorer89.tistory.com

 

 

🔷 DatabaseManager.swift

class DatabaseManager {
    
    
    // MARK: - Variable
    static let shared = DatabaseManager()
    
    let db = Firestore.firestore()
    let userPath: String = "users"
    
    ... 
    func collectionUsers(updateFields: [String: Any], for id: String) -> AnyPublisher<Bool, Error> {
        Future<Bool, Error> { promise in
            self.db.collection(self.userPath).document(id).updateData(updateFields) { error in
                if let error = error {
                    promise(.failure(error))
                } else {
                    promise(.success(true))
                }
            }
        }
        .eraseToAnyPublisher()
    }
}

 

📌 collectionUsers(updateFields: [String: Any], for id: String) 메서드 역할 

 

  • Firestore의 특정 문서(사용자 데이터)를 업데이트하는 역할을 한다.
  • 입력값
    • updateFields: [String: Any] → 업데이트할 데이터 (예: ["username": "newName"])
    • id: String → 업데이트할 사용자의 Firebase UID
  • 출력값
    • AnyPublisher<Bool, Error> → 성공하면 true, 실패하면 Error를 반환하는 Combine Publisher

 

 

✅ 전체 흐름 

1. ProfileDataFormViewController() 에서 프로필 작성 완료 후 "작성 완료" 버튼을 누른다. 

@objc private func didTapSubmit() {
    viewModel.uploadAvatar()
}

 

 

2. uploadAvatar() 메서드 실행하면, uploadProfilePhoto() 호출 후 업로드된 이미지의 다운로드 URL 요청

flatMap을 사용해 getDownloadURL() 호출한 뒤, sink에서 결과처리 

실패 -> 에러 / 성공 -> 다운로드 URL을 avatarPath에 저장, updateUserData() 실행 

 

3. collectionUsers(updateFields: [String: Any], for id: String)를 통해 사용자 정보 업데이트 진행 

 

4. 사용자 정보 업데이트 완료 후 "isUserOnboarded" : true 가 됨 

 

5. ProfileDataFormViewController 내에서 아래 bindView를 통해 MainTabBarController로 이동 

private func bindViews() {
 ...
    viewModel.$isOnboardingFinished
        .sink { [weak self] success in
            if success {
                // ✅ 기존의 모든 화면을 닫고, OnboardingViewController로 이동
                self?.view.window?.rootViewController?.dismiss(animated: true, completion: {
                    if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate {
                        sceneDelegate.window?.rootViewController = MainTabBarController()
                    }
                })
            }
        }
        .store(in: &cancelable)
}

 

6. MainTabBarController로 와서는 isUserOnboarded 값에 따라 화면 이동 결정 

private func bindView() {

    // ✅ 회원정보가 수정이 안되었다면 ➡️ ProfileDataFormViewController 로 이동
    viewModel.$user
        .sink { [weak self] user in
            guard let user = user else { return }
            if !user.isUserOnboarded{
                dump(user.isUserOnboarded)
                self?.completeUserOnboarding()
            }
        }
        .store(in: &cancelable)
}

func completeUserOnboarding() {
    let profileDataFormVC = ProfileDataFormViewController()
    present(profileDataFormVC, animated: true)
}