포스트

Final (29)

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

29일 새벽 커밋을 마지막으로, 프로젝트 끝까지 GitHub 계정이 일시적으로 정지되어 직접 push 를 하지 못했다. 그래서 그 뒤의 코드는 팀원(moremirim)이 7월 1일에 올려준 [Merge] 커밋에 한꺼번에 들어가 있다. 이 글부터 (31)까지는 그 커밋을 날짜별 메모에 맞춰 나눠서 정리했다.

29일에는 리젝 관련 부분을 반영해서 관리자에게 커뮤니티 권한을 주고, 신고가 쌓이면 메시지와 리뷰를 자동으로 가리도록 했다.


관리자 커뮤니티 권한

리젝 관련 부분을 반영해서, 관리자는 커뮤니티의 모든 채팅방에서 지역 제한 없이 메시지를 보낼 수 있게 했다. 관리자 계정은 닉네임이 “관리자” 인 계정으로 구분한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var isAdmin = false

private func checkNickname() {
    // 유저 모드일 때만 닉네임 확인
    if let user = currentUser {
        userManager.fetchUserData(uid: user.uid) { [self] error, snapshot in
            // ...
            currentName = (dictionary[db_nickName] as? String) ?? "Unknown"
            if currentName == ""  {
                showNameAlert(uid: user.uid)
                isValidate = false
            } else if currentName == "관리자" {
                isAdmin = true
            } else {
                isValidate = true
            }
        }
    }
}

채팅방에 들어갈 때 관리자면 지역과 관계없이 isLocation = true 로 넘긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if isAdmin {
    isLocation = true
} else {
    if currentAddress != channel.name {
        isLocation = false
    } else {
        isLocation = true
    }
}

if let user = currentUser {
    if isValidate || isAdmin {
        viewController = ChatVC(user: user, channel: channel)
        viewController?.isLocation = isLocation
    } else {
        // 닉네임 입력 Alert
    }
}

지금 보면 닉네임 문자열로 관리자를 구분하는 건 위험하다. 누군가 닉네임을 “관리자” 로 정하면 그 사람도 관리자가 된다. 중복 확인이 있어서 이미 관리자가 쓰고 있으면 막히긴 하지만, Realtime Database 에 role 같은 필드를 따로 두는 게 맞다.


커뮤니티 신고 메시지 바인딩

채팅 메시지 신고는 진문님이 만든 신고 화면(ChatReportViewController)으로 들어간다. 그런데 신고를 하면 신고 목록(report collection)의 카운트만 올라가고, 정작 신고된 메시지 자체에는 아무 변화가 없었다. 그래서 신고 카운트를 메시지 document 에도 반영하고, 3회 이상 신고되면 자동으로 block 처리되게 했다.


신고할 메시지 정보 만들기

메시지를 길게 눌러 신고하면 신고 데이터를 만든다. 사진 메시지는 내용이 없으니 이미지 URL 을 같이 넘겨서 나중에 그 메시지를 찾을 수 있게 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var reportContent = selectedMessage.content
var reportURL: String? = nil

switch selectedMessage.kind {
case .photo(_):
    reportContent = "이미지 신고"
    reportURL = selectedMessage.downloadURL?.absoluteString
case .text(let text):
    reportContent = text
    reportURL = nil
default:
    reportContent = "알 수 없는 종류의 메시지"
    reportURL = nil
}

let reportData = ReportUserData(
    title: "신고", // 원하는 신고 제목 설정
    // ... senderId, sentDate 등
    content: reportContent, // 메시지 내용
    reportCount: 1, // 초기 신고 수
    channel: channel.name,
    url: reportURL ?? "" // 이미지 URL 또는 nil
)

ReportUserData 에는 url property 를 추가했다.


메시지에 신고 횟수 올리기

신고 화면에서 신고를 저장한 뒤, 텍스트면 보낸 사람과 내용으로, 사진이면 보낸 사람과 URL 로 원래 메시지를 찾는다.

1
2
3
4
5
if userData.url != "" {
    await viewModel.fetchFilteredImages(forChannelName: userData.channel, senderId: userData.senderId, url: userData.url, isSexual: isSexual)
} else {
    await viewModel.fetchFilteredMessages(forChannelName: userData.channel, senderId: userData.senderId, content: userData.content, isSexual: isSexual)
}

찾은 메시지의 reportCount 를 1 올리고, 이미 2번 신고된 메시지(이번이 3번째)이거나 선정적인 내용으로 신고되면 isActive 를 false 로 바꾼다.

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
31
func fetchFilteredMessages(forChannelName channelName: String, senderId: String, content: String, isSexual: Bool) async {
    do {
        let channelSnapshot = try await channelCollection.whereField(db_channelName, isEqualTo: channelName).getDocuments()

        for channelDocument in channelSnapshot.documents {
            let channelId = channelDocument.documentID

            let threadSnapshot = try await channelCollection.document(channelId).collection(db_thread)
                .whereField(db_senderId, isEqualTo: senderId)
                .whereField(db_content, isEqualTo: content)
                .getDocuments()

            for threadDocument in threadSnapshot.documents {
                let data = threadDocument.data()
                let id = threadDocument.documentID
                if let reportCount = data[db_chatReportCount] as? Int, let isActive = data[db_isActive] as? Bool {
                    var dict = [String: Any]()

                    if reportCount >= 2 || isSexual {
                        dict = [db_chatReportCount: reportCount + 1, db_isActive: false]
                    } else {
                        dict = [db_chatReportCount: reportCount + 1]
                    }
                    try await channelCollection.document(channelId).collection(db_thread).document(id).setData(dict, merge: true)
                }
            }
        }
    } catch {
        print("Error fetching channels or threads: \(error)")
    }
}

사진용 fetchFilteredImages 도 content 대신 url 로 찾는다는 것만 다르고 나머지는 같다. setData(_:merge: true) 는 document 전체를 덮어쓰지 않고 넘긴 필드만 바꾼다.


block 된 메시지 가리기

채팅방은 isActive 가 true 인 메시지만 구독하도록 바꿨다. 그래서 신고로 isActive 가 false 가 되는 순간 그 메시지는 채팅방에서 사라진다.

1
2
3
4
listener = collectionListener?.whereField(db_isActive, isEqualTo: true)
    .addSnapshotListener { snapshot, error in
        // ...
    }

신고 화면에도 리뷰 신고처럼 기타 사유 예외처리를 넣었다.

1
2
3
4
if isEtc == false && !textView.text.isEmpty {
    showMessage(title: "오류", message: "기타 사유를 선택한 경우에만 내용을 신고할 수 있습니다.")
    return
}

리뷰 신고 기능 보완

리뷰 신고도 커뮤니티 신고처럼 3회 이상 신고되거나 선정적인 내용이면 자동으로 block 되게 했다. 기존에는 reportCount 만 1 올렸다.

1
2
3
4
5
6
7
8
9
10
func addReportCount(uid: String, storeAddress: String, title: String, isSexual: Bool) async throws {
    // ... 신고된 리뷰 찾기
    var dict = [String: Any]()
    if reportCount >= 2 || isSexual {
        dict = [db_reportCount: reportCount + 1, db_isActive: false]
    } else {
        dict = [db_reportCount: reportCount + 1]
    }
    try await reviewCollection.document(id).setData(dict, merge: true)
}

신고 화면에서 선정성 사유를 체크했는지(isSexual)도 같이 넘기도록 함수 인자를 늘렸다.

1
await viewModel.addReportAndIncreaseCount(uid: userData!.uid, storeAddress: userData!.storeAddress, title: userData!.title, isSexual: isSexual, reportData: reportData)

게스트 커뮤니티 예외처리

정지 전 마지막 커밋에서는 게스트로 들어갔다가 커뮤니티 탭을 열 때의 예외를 하나 더 막았다. 로그인 상태가 바뀐 뒤에도 ChannelVC 가 이전 유저 정보를 들고 있을 수 있어서, 화면에 들어올 때 로그인한 유저가 없으면 게스트로 다시 맞춘다.

1
2
3
4
if Auth.auth().currentUser == nil {
    currentUser = nil
    customUser = CustomUser(guestUID: "guest")
}

SceneDelegate 도 게스트 로그인 버튼을 누를 때 customUser 를 property 에 저장해 두도록 바꿨다.

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