포스트

Final (27)

프로젝트 막바지에는 기능 구현에 쫓겨 블로그 정리를 제대로 하지 못했다. 그래서 이 글은 당시 GitHub 커밋 내역과, 프로젝트 기간에 직접 적어 둔 업무 내역 목록을 AI(Claude Code)에게 보여주고 그걸 바탕으로 내용을 다시 정리했다. 코드는 실제 커밋에 있던 것을 가져왔다.

게스트가 로그인이 필요한 기능을 눌렀을 때의 Alert 를 다듬고, 커뮤니티에서 처음 닉네임을 정할 때도 중복 확인을 하게 했다. 어제 만든 프로필 수정 중복 확인의 구멍도 같이 막았다. 다른 지역의 분식집 JSON 데이터도 이날 모두 추가했다.


게스트 모드 Alert 수정

게스트가 채팅 보내기, 리뷰 쓰기, 스크랩 같은 기능을 누르면 “로그인이 필요한 기능입니다.” Alert 만 뜨고 끝났다. 그러면 사용자는 로그인하려면 어디로 가야 하는지 스스로 찾아야 한다. 그래서 Alert 의 확인을 누르면 로그인 페이지로 이동하게 수정했다.

1
2
3
4
5
6
showMessage(title: "로그인이 필요한 기능입니다.", message: "확인을 클릭하시면 로그인 페이지로 이동합니다.") {
    let scene = UIApplication.shared.connectedScenes.first
    if let sd: SceneDelegate = (scene?.delegate as? SceneDelegate) {
        sd.switchToGreetingViewController()
    }
}

showMessage 는 예전에 만든 UIViewController extension 으로, Alert 의 확인을 누르면 completion 을 실행한다. 화면 전환은 SceneDelegate 가 가진 switchToGreetingViewController() 로 로그인 화면을 root 로 바꾼다. 이걸 ChatVC, MapViewController, DetailedReviewViewController, StoreViewController 에 적용했다.

마이페이지도 게스트일 때는 “로그아웃” 메뉴 대신 “로그인 하러가기” 가 보이도록 바꿨다. 누르면 로그아웃과 같은 경로로 로그인 화면에 간다.

1
2
3
4
5
6
7
8
9
var option = myPageVM.sections[indexPath.section].options[indexPath.item]
if let _ = Auth.auth().currentUser {
    // 로그인 유저는 그대로
} else {
    if option.title == "로그아웃" {
        option.title = "로그인 하러가기"
    }
    cell.configure(with: option)
}

취소 버튼 추가

그런데 써 보니 확인 버튼 하나뿐이라, 기능을 한 번 눌러 본 게스트를 무조건 로그인 페이지로 보내는 것 같았다. 둘러보던 중에 갑자기 로그인 화면으로 튕기면 불편하다. 그래서 취소도 할 수 있게 showMessage 를 showMessageWithCancel 로 바꿨다.

1
2
3
4
5
6
7
8
func showMessageWithCancel(title: String, message: String, completion: (() -> Void)? = nil) {
    let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "취소", style: .default))
    alert.addAction(UIAlertAction(title: "확인", style: .default, handler: { _ in
        completion?()
    }))
    present(alert, animated: true)
}
1
2
3
4
5
6
showMessageWithCancel(title: "로그인이 필요한 기능입니다.", message: "확인을 클릭하시면 로그인 페이지로 이동합니다.") {
    let scene = UIApplication.shared.connectedScenes.first
    if let sd: SceneDelegate = (scene?.delegate as? SceneDelegate) {
        sd.switchToGreetingViewController()
    }
}

지도 가게 정보창(PinStoreView)의 버튼도 게스트일 때 아무 반응 없이 return 하던 것을, 같은 Alert 를 띄우도록 바꿨다.


커뮤니티 닉네임 중복처리

커뮤니티에 처음 들어온 유저는 닉네임이 비어 있어서 닉네임 입력 Alert 가 뜬다. 여기에도 프로필 수정 페이지처럼 중복 확인을 넣었다.


getUsers 에 completion 추가

어제 프로필 수정에서는 @Published 를 sink 로 구독해서 비교했는데, 구독하는 순간 아직 서버에서 받기 전의 배열이 먼저 오는 문제가 있었다. 그래서 getUsers 에 completion 을 받는 버전을 추가했다. 전체 유저를 다 담은 뒤에 completion 을 부르니까, 그 안에서 비교하면 항상 최신 목록과 비교하게 된다.

1
2
3
4
5
6
7
8
func getUsers(completion: @escaping () -> Void) {
    manageManager.fetchUsers { [weak self] error, dataSnapshot in
        self?.userArray.removeAll()
        // ... userArray 채우기 (기존 getUsers 와 같음)
        self?.managePublisher.send(())
        completion()
    }
}

ChannelVC 에는 이걸 써서 중복이면 false, 아니면 true 를 돌려주는 함수를 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
private func validateNickname(nickName: String, completion: @escaping ((Bool) -> Void)) {
    let manageManager = ManageManager()
    self.viewModel = ManageViewModel(manageManager: manageManager)

    self.viewModel?.getUsers {
        if self.viewModel?.userArray.contains(where: { $0.nickName == nickName }) == false {
            completion(true)
        } else {
            completion(false)
        }
    }
}

중복이면 Alert 다시 띄우기

닉네임 입력 Alert 에서 OK 를 누르면 중복 확인을 하고, 중복이면 안내 후에 입력 Alert 를 다시 띄운다. 쓸 수 있는 닉네임을 넣을 때까지 반복된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private func showNameAlert(uid: String) {
    let alertController = UIAlertController(title: "Enter Name", message: "Please enter your name.", preferredStyle: .alert)

    alertController.addTextField { (textField) in
        textField.placeholder = "Name"
    }

    let confirmAction = UIAlertAction(title: "OK", style: .default) { [weak self] (_) in
        if let textField = alertController.textFields?.first, let newName = textField.text, !newName.isEmpty {
            self?.validateNickname(nickName: newName) { result in
                switch result {
                case true:
                    self?.updateUserName(uid: uid, newName: newName)
                    self?.isValidate = true
                case false:
                    self?.isValidate = false
                    self?.showMessage(title: "중복 확인", message: "현재 닉네임은 이미 존재합니다.") {
                        self?.showNameAlert(uid: uid) // 이름이 유효하지 않으면 알림을 다시 표시
                    }
                }
            }
        }
    }
    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)

    alertController.addAction(confirmAction)
    alertController.addAction(cancelAction)

    self.present(alertController, animated: true, completion: nil)
}

checkNickname() 은 viewDidLoad 에서 viewWillAppear 로 옮겼다. 탭을 옮겨 다니다 다시 와도 닉네임이 비어 있으면 다시 묻게 하려는 것이다. 닉네임이 있으면 isValidate = true, 비어 있으면 false 로 둔다.


Cancel 했을 때의 예외처리

Alert 에서 Cancel 을 누르면 닉네임이 빈 채로 채팅방 목록에 남는다. 이 상태에서 채팅방에 들어가면 이름 없는 사람이 생긴다. 그래서 채팅방을 누를 때 isValidate 를 확인해서, 아직 닉네임이 없으면 들어가지 않고 입력 Alert 를 다시 띄운다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let channel = channels[indexPath.row]
    var viewController: ChatVC? // viewController를 옵셔널로 선언

    if currentAddress != channel.name {
        isLocation = false
    } else {
        isLocation = true
    }

    if let user = currentUser {
        if isValidate {
            viewController = ChatVC(user: user, channel: channel)
            viewController?.isLocation = isLocation
        } else {
            guard let uid = Auth.auth().currentUser?.uid else { return }
            showNameAlert(uid: uid)
            return // 이름 확인이 완료될 때까지 반환하여 viewController 초기화를 기다림
        }
    } else if let customUser = customUser {
        viewController = ChatVC(customUser: customUser, channel: channel)
    } else {
        fatalError("No valid user found.")
    }

    if let viewController = viewController {
        navigationController?.pushViewController(viewController, animated: true)
    }
    tableView.deselectRow(at: indexPath, animated: true)
}

중간에 return 하는 경우가 생겨서 viewController 를 옵셔널로 바꿨다. 이 수정을 하면서 25일에 넣은 “테스트” 채널 예외 분기가 빠졌다.


프로필 수정 중복 확인 보완

프로필 수정 페이지의 중복 확인도 sink 대신 completion 버전의 getUsers 를 쓰도록 바꿨다.

1
2
3
4
5
6
7
8
9
10
11
self.viewModel?.getUsers { [weak self] in
    if self?.viewModel?.userArray.contains(where: { $0.nickName == nickName }) == false {
        self?.isValidate = true
        self?.validateLabel.textColor = .blue
        self?.validateLabel.text = "입력하신 닉네임은 사용 가능합니다."
    } else {
        self?.isValidate = false
        self?.validateLabel.textColor = .red
        self?.validateLabel.text = "이미 닉네임이 존재합니다."
    }
}

그리고 구멍이 하나 더 있었다. 쓸 수 있는 닉네임으로 중복확인을 통과한 뒤에 닉네임을 다른 걸로 고쳐도 isValidate 가 true 로 남아 있어서, 확인하지 않은 닉네임이 저장됐다. 그래서 입력칸의 글자가 바뀔 때마다 isValidate 를 false 로 되돌렸다.

1
2
3
4
5
6
7
extension PersonalInfoViewController: UITextFieldDelegate {

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        isValidate = false
        return true
    }
}

textField(_:shouldChangeCharactersIn:replacementString:) 은 글자가 바뀌기 직전마다 불리는 delegate 메서드다. true 를 돌려주면 입력은 그대로 반영된다. 화면에 다시 들어올 때(viewWillAppear)도 isValidate 를 false 로 초기화했다. 저장 버튼은 NavigationBar 오른쪽으로 옮겼다.


지도에서 지역 채팅방 바로 가기

지도 가게 정보창에는 그 가게가 있는 지역의 채팅방으로 바로 가는 “친구찾기” 버튼이 있다. 지금까지는 지역 이름만으로 Channel 을 만들어서 ChatVC 를 띄웠는데, 채팅 메시지를 읽고 쓰려면 Firestore 의 채널 document id 가 필요했다. 그래서 지역 이름과 document id 를 같이 돌려주도록 바꿨다.

1
2
3
4
5
6
7
8
9
10
11
private func getChannelInfo(address: String) -> (id: String, name: String) {
    switch address {
    case "서울특별시":
        return ("<서울 채널 document id>", "서울특별시")
    case "경기도":
        return ("<경기 채널 document id>", "경기도")
    // ... 다른 지역 생략
    default:
        return ("defaultID", "default")
    }
}
1
2
3
let channelInfo = getChannelInfo(address: region)
guard let user = Auth.auth().currentUser else { return }
let chatVC = ChatVC(user: user, channel: Channel(id: channelInfo.id, name: channelInfo.name))

Auth.auth().currentUser! 로 강제 언래핑하던 것도 guard let 으로 바꿨다. 위치 선택 화면의 Send, Cancel 버튼은 “보내기”, “취소” 로 바꾸고, 검색창 자리에 “보내고 싶은 지역을 지도에서 길게 누르세요.” 안내를 넣었다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.