24.09.13 Today I Learned

2024. 9. 13. 19:21

오늘은 Firebase CRUD에서 Read를 공부해봤다.

 

우선 다음에 Update에서 수정할거리를 만들기 위해 닉네임도 추가로 넣었다.

import Foundation

struct User: Codable {
    
    let email: String
    let password: String
    let nickname: String
}

 

SignViewController에 닉네임 레이블 추가해서 가입할 때 닉네임도 적도록 구현

import UIKit

import SnapKit

class SignUpViewController: UIViewController {
    
    private let emailTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Email"
        textField.borderStyle = .roundedRect
        return textField
    }()
    
    private let passwordTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Password"
        textField.borderStyle = .roundedRect
        textField.isSecureTextEntry = true
        return textField
    }()
    
    private let nicknameTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "NickName"
        textField.borderStyle = . roundedRect
        return textField
    }()
    
    private lazy var signUpButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Sign Up", for: .normal)
        button.backgroundColor = .blue
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 8
        button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
        return button
    }()
    
    private let viewModel = SignUpViewModel() // viewModel을 SignUpViewModel클래스의 속한 모든 메서드와 속성에 접근할 수 있게 할 수 있는 객체
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        
        setupUI()
    }
    
    private func setupUI() {
        [emailTextField, passwordTextField, nicknameTextField, signUpButton].forEach { view.addSubview($0) }
        
        emailTextField.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide).offset(50)
            $0.leading.trailing.equalToSuperview().inset(20)
            $0.height.equalTo(40)
        }
        
        passwordTextField.snp.makeConstraints {
            $0.top.equalTo(emailTextField.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview().inset(20)
            $0.height.equalTo(40)
        }
        
        nicknameTextField.snp.makeConstraints {
            $0.top.equalTo(passwordTextField.snp.bottom).offset(20)
            $0.leading.equalToSuperview().inset(20)
            $0.height.equalTo(40)
        }
        
        signUpButton.snp.makeConstraints {
            $0.top.equalTo(nicknameTextField.snp.bottom).offset(30)
            $0.centerX.equalToSuperview()
            $0.width.equalTo(150)
            $0.height.equalTo(50)
        }
    }
    
    @objc private func buttonAction() {
        // email, password, nickname이 비어있을 때 비어있다고 알림.
        guard let email = emailTextField.text, !email.isEmpty,
              let password = passwordTextField.text, !password.isEmpty,
              let nickname = nicknameTextField.text, !nickname.isEmpty else {
            print("비어있음")
            return
        }
        
        viewModel.isSignUpSuccessful = { [weak self] isSuccess in
            if isSuccess {
                DispatchQueue.main.async {
                    let loginViewController = LoginViewController()
                    self?.navigationController?.pushViewController(loginViewController, animated: true)
                }
            } else {
                print("회원가입 실패")
            }
        }
        // ViewModel에 정의해둔 signUp()메서드를 사용함. -> 위에 "private let viewModel = SignUpViewModel()" 라고 정의해두었는데 SignUpViewModel의 signUp메서드에 접근한다 라는의미.
        viewModel.signUp(email: email, password: password, nickname: nickname)
    }
}
//        viewModel.email = emailTextField.text ?? ""  // ??쓰는이유 : 옵셔널 타입에 데이터가 nil일 때 default값을 정의해주기 위해서 사용하는 것

 

nickname이 새로 생겼으니 Firestore에도 nickname이 저장되도록 추가함.

import Foundation

import FirebaseFirestore

class SignUpViewModel {

//    var errorMessage: String? -> 회원가입 중 오류 메세지를 저장하기 위한 문자열 옵셔널 변수임. 현재 오류메세지가 없어서 사용하지 않는중
    var isSignUpSuccessful: ((Bool) -> Void)? // 회원가입 성공 여부를 뷰컨에 전달하기 위한 콜백. 회원가입이 성공or실패 여부를 알기위해 bool값을 전달
    let db = Firestore.firestore() // Firebase 데이터베이스 객체, Firestore에 접근할 때 사용
    
 
    func signUp(email: String, password: String, nickname: String) { // 실제 회원가입을 처리하는 로직. 이메일, 비번, 닉네임을 Firestore에 저장함.
        db.collection("Users").document(email).setData([ // Firestore의 Users 컬렉션에 접근하여, 사용자의 이메일을 문서 ID로 지정. 이 문서에 이메일, 비밀번호, 닉네임 데이터를 저장함
            "email": email,
            "password": password,
            "nickname": nickname // 왼쪽의 key값은 필드의 이름 오른쪽이 실제 저장이 될 값 value
        ]) { error in
            if let error = error {
                print("Error saving user data: \(error)")
                self.isSignUpSuccessful?(false)
            } else {
                print("User data saved successfully.")
                self.isSignUpSuccessful?(true)
            }
        }
        // db에있는 "Users"라는 컬렉션에 접근, document는 각자의 객체로 회원가입 할떄마다 새로운 document가 생성되며 그 안에 정보가 들어감.
        // 예를들어 test@gmail.com 으로 가입하면 문서 부분에 test@gmail.com이라는 문서가 생기고 컬렉션에 그사람의 정보가 들어감.
    }
    
}

LoginViewController에서는 닉네임을 입력안해도 되기때문에 그대로 eamil과 password를 입력하여 로그인하도록 했음.

import UIKit

import FirebaseFirestore

class LoginViewController: UIViewController {
    
    let db = Firestore.firestore()
    
    private let emailTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Email"
        textField.borderStyle = .roundedRect
        return textField
    }()
    
    private let passwordTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Password"
        textField.borderStyle = .roundedRect
        textField.isSecureTextEntry = true
        return textField
    }()
    
    private lazy var loginButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Log In", for: .normal)
        button.backgroundColor = .blue
        button.setTitleColor(.white, for: .normal)
        button.layer.cornerRadius = 8
        button.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        setupUI()
    }
    
    private func setupUI() {
        [emailTextField, passwordTextField, loginButton].forEach { view.addSubview($0) }
        
        emailTextField.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide).offset(50)
            $0.leading.trailing.equalToSuperview().inset(20)
            $0.height.equalTo(40)
        }
        
        passwordTextField.snp.makeConstraints {
            $0.top.equalTo(emailTextField.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview().inset(20)
            $0.height.equalTo(40)
        }
        
        loginButton.snp.makeConstraints {
            $0.top.equalTo(passwordTextField.snp.bottom).offset(30)
            $0.centerX.equalToSuperview()
            $0.width.equalTo(150)
            $0.height.equalTo(50)
        }
    }
    
    @objc private func loginButtonTapped() {
        guard let email = emailTextField.text, !email.isEmpty,
              let password = passwordTextField.text, !password.isEmpty else {
            print("이메일과 비밀번호를 입력하세요.")
            return
        }
        
        let docRef = db.collection("Users").document(email)
        
        docRef.getDocument { (document, error) in
            if let document = document, document.exists {
                let data = document.data()
                let email = data?["email"] as? String ?? ""
                let password = data?["password"] as? String ?? ""
                let nickname = data?["nickname"] as? String ?? ""
                
                DispatchQueue.main.async {
                    let mainVC = MainViewController()
                    mainVC.configure(email: email, password: password, nickname: nickname)
                    self.navigationController?.pushViewController(mainVC, animated: true)
                }
            } else {
                print("사용자 정보를 찾을수 없음")
            }
        }
    }
}

로그인 버튼을 누르면, 이메일과 비밀번호가 비어있는지 비어있지 않은지 체크하고, 비어있으면 이메일과 비밀번호를 입력하라는 메세지가 콘솔창에 뜸. 그리고 전부 입력이 됐고 정보가 Firestore에 있으면 Firestore에서 해당 사용자의 데이터를 가져옴. 

db.collection("Users").document(email)은 Firestore의 Users 컬렉션에서 이메일을 기반으로 해당 문서를 참조함.

docRef.getDocument를 통해 Firestore에서 데이터를 가져옴.

가져온 문서가 존재한다면 email, password, nickname을 추출하고 이를 MainViewController로 전달함. 

전달하고 navigationController를 사용하여 MainViewController로 화면 이동.


MainViewController에서는 닉네임을 추가로 넣어주고, 수정페이지로 이동할 수 있는 네비게이션바에 오른쪽 버튼을 추가했음.

import UIKit
import SnapKit

class MainViewController: UIViewController {
    
    private let profileImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "person.circle")
        imageView.contentMode = .scaleAspectFit
        imageView.layer.cornerRadius = 50
        imageView.layer.masksToBounds = true
        return imageView
    }()
    
    private let emailLabel: UILabel = {
        let label = UILabel()
        label.text = "example@example.com"  // 이메일이 "Email: " 형태로 되어있으므로 수정해야 함
        label.textAlignment = .center
        return label
    }()
    
    private let passwordLabel: UILabel = {
        let label = UILabel()
        label.text = "Password: ••••••••"
        label.textAlignment = .center
        return label
    }()
    
    private let nicknameLabel: UILabel = {
        let label = UILabel()
        label.text = "Nickname"
        label.textAlignment = .center
        return label
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        
        // 네비게이션 바 우측 버튼 추가
        setupNavigationBar()
        
        setupUI()
    }
    
    // 네비게이션 바 우측 버튼 설정
    private func setupNavigationBar() {
        let editButton = UIBarButtonItem(title: "Edit", style: .plain, target: self, action: #selector(editButtonTapped))
        navigationItem.rightBarButtonItem = editButton
    }
    
    // 버튼 액션: EditViewController로 이동
    @objc private func editButtonTapped() {
        let email = emailLabel.text ?? ""
        print("전달된 이메일: \(email)")  // 이메일 출력해서 확인
        
        let editViewController = EditViewController()
        editViewController.userEmail = email  // EditViewController로 이메일 전달
        navigationController?.pushViewController(editViewController, animated: true)
    }
    
    private func setupUI() {
        [profileImageView, emailLabel, passwordLabel, nicknameLabel].forEach { view.addSubview($0) }
        
        profileImageView.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide).offset(100)
            $0.centerX.equalToSuperview()
            $0.width.height.equalTo(100)
        }
        
        emailLabel.snp.makeConstraints {
            $0.top.equalTo(profileImageView.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview().inset(20)
        }
        
        passwordLabel.snp.makeConstraints {
            $0.top.equalTo(emailLabel.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview().inset(20)
        }
        
        nicknameLabel.snp.makeConstraints {
            $0.top.equalTo(passwordLabel.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview().inset(20)
        }
    }
    
    // 사용자 정보 설정
    func configure(email: String, password: String, nickname: String) {
        emailLabel.text = "\(email)"
        passwordLabel.text = "Password: \(String(repeating: "•", count: password.count))"
        nicknameLabel.text = "NickName: \(nickname)"
    }
}

editButton을 누르게 되면 editViewController에 전달할 이메일을 출력해서 확인하고 그 이메일을 EditViewController에 전달함.

 

let email = emailLabel.text ?? "" print("전달된 이메일: \(email)") 주의깊게 볼 부분은 여긴데 edit버튼을 누르면 이메일 정보를 출력하여 해당 이메일을 EditViewController에 전달함. 여기서 처음에 계속 정보가 전달이 안됐었는데 내가 configure에 
emailLabel.text = "Email: \(email)"이라고 적었었는데 Email: 을 지우니까 제대로 전달이 됐었음. 전달해야하는 정보는 email뿐인데 emailLabel.text 에 내가 Email: 이라는 불필요한걸 넣어서 전달이 안됐던 것임.


마지막으로는 EditViewController인데, 

import UIKit
import FirebaseFirestore
import SnapKit

class EditViewController: UIViewController {
    
    let db = Firestore.firestore()
    var userEmail: String?
    
    private let profileImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "person.circle")
        imageView.contentMode = .scaleAspectFill
        imageView.layer.cornerRadius = 50
        imageView.layer.masksToBounds = true
        return imageView
    }()
    
    private let nickNameTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Enter Nickname"
        textField.borderStyle = .roundedRect
       return textField
    }()
    
    private let emailLabel: UILabel = {
        let label = UILabel()
        label.text = "Example@example.com"
        label.font = UIFont.systemFont(ofSize: 16)
        label.textColor = .black
        return label
    }()
    
    private let passwordTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "Enter Password"
        textField.borderStyle = .roundedRect
        return textField
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        setupNavigationBar()
        setupUI()
        fetchUserData()
    }
    
    // Firestore에서 데이터 가져오기
    private func fetchUserData() {
        guard let email = userEmail else {
            print("userEmail이 설정되지 않았습니다.")  // 이메일이 없는 경우
            return
        }
        
        let docRef = db.collection("Users").document(email)
        
        docRef.getDocument { [weak self] (document, error) in
            if let error = error {
                print("Firestore에서 데이터를 가져오는 중 오류 발생: \(error)")
                return
            }
            
            if let document = document, document.exists {
                let data = document.data()
                let nickname = data?["nickname"] as? String ?? ""
                let password = data?["password"] as? String ?? ""
                
                // 가져온 데이터를 UI에 반영
                DispatchQueue.main.async {
                    self?.nickNameTextField.text = nickname
                    self?.emailLabel.text = email
                    self?.passwordTextField.text = password
                }
            } else {
                print("해당 이메일의 문서가 존재하지 않습니다.")
            }
        }
    }
    
    private func setupNavigationBar() {
        let doneButton = UIBarButtonItem(title: "수정 완료", style: .plain, target: self, action: #selector(doneButtonTapped))
        navigationItem.rightBarButtonItem = doneButton
    }
    
    // UI 설정
    private func setupUI() {
        [profileImageView, nickNameTextField, emailLabel, passwordTextField].forEach { view.addSubview($0) }
        
        profileImageView.snp.makeConstraints {
            $0.top.equalTo(view.safeAreaLayoutGuide).offset(100)
            $0.centerX.equalToSuperview()
            $0.width.height.equalTo(100)
        }
        
        nickNameTextField.snp.makeConstraints {
            $0.top.equalTo(profileImageView.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview().inset(20)
            $0.height.equalTo(40)
        }
        
        emailLabel.snp.makeConstraints {
            $0.top.equalTo(nickNameTextField.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview().inset(20)
        }
        
        passwordTextField.snp.makeConstraints {
            $0.top.equalTo(emailLabel.snp.bottom).offset(20)
            $0.leading.trailing.equalToSuperview().inset(20)
            $0.height.equalTo(40)
        }
    }
    
    // "수정 완료" 버튼 클릭 시 호출되는 메서드
    @objc private func doneButtonTapped() {
        // MainViewController로 돌아가기
        navigationController?.popViewController(animated: true)
    }
}

여기서 맨 처음에 나오는 var userEmail: String?은 다른 뷰 컨트롤러에서 이메일을 전달받아서 사용자 데이터를 조회하는데 사용됨.

fetchUserData는 Firestore에서 사용자의 데이터를 가져오는 메서드인데, userEmail이 데이터를 받아오지 못했으면 오류메세지를 출력하고 반환함.

받아왔다면 받아온 Email을 기반으로 Firestore에서 해당 사용자의 nickname과 password를 가져옴.

가져온 데이터를 nicknameTextField와 passwordTextField에 반영하여 UI에 표시함.


아직 완벽하게 이해는 못했지만, 이해가 조금씩 되고있어서 재미 있음!

'Today I Learned > 2024' 카테고리의 다른 글

24.10.15 Today I Learned  (1) 2024.10.15
24.09.27 Today I Learned  (3) 2024.09.27
24.09.12 Today I Learned  (0) 2024.09.12
24.09.10 Today I Learned  (0) 2024.09.10
24.09.05 Today I Learned  (0) 2024.09.05

BELATED ARTICLES

more