| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 알고리즘 조건
- 원형 연결 구조 연결된 큐
- 운영체제 기능
- auto 키워드
- getline()함수
- C언어 덱
- r-value참조자
- const l-value참조자
- C언어 스택 연산
- 범위 기반 for문
- 백준 파이썬
- 네트워크 결합
- 문제해결 단계
- 괄호 검사 프로그램
- 문자형 배열
- 입출력 관리자
- IPv4 주소체계
- string유형
- C언어 계산기 프로그램
- 논리 연산
- 프로그래머스 푸드 파이트 대회
- c언어 괄호검사
- 프로그래머스 배열만들기4
- 주기억장치
- LAN의 분류
- l-value참조자
- 유형 변환
- 값/참조/주소에 의한 전달
- const화
- 회전 및 자리 이동 연산
- Today
- Total
chyam
[Android/Kotlin] - 사용자 참여형 장소 제보 및 승인 시스템(Firebase) 본문
기획 배경
공공데이터로 CCTV와 보안등 API를 사용하였는데, 학교와 같은 시설들은 누락된 경우가 많아서, 앱에서 사용자가 직접 제보를하면 검토 후 승인하여 모든 사용자의 지도에 즉각 반영되는 시스템을 개발하고자하였습니다.
시스템 흐름
1. 위치 선택 : 사용자가 앱에서 해당 위치를 길게 터치합니다.
2. 정보 입력 및 사진 촬영 : 시트에 CCTV인지 보안등인지 선택한 뒤, 카메라를 켜서 현장사진을 필수로 촬영하도록 합니다. (장난으로 하는 것 방지)
3. 업로드 : 사진은 Firebase Storage에, 좌표 및 상태값은 Firestore에 업로드 됩니다.
4. 관리자 승인 : 관리자가 Firebase console에서 사진을 확인한 후 상태값을 "APPROVED"로 변경합니다.
5. 지도 반영 : 앱에서 "APPROVED" 상태인 데이터만 실시간으로 불러와 지도에 새로운 마커로 렌더링합니다.
Firebase 세팅
기본적으로 Firebase는 만든상태라고 가정하겠습니다!

먼저 firestore을 들어가줍니다. 데이터베이스 만들기를 누른 뒤 Standard 버전으로 선택합니다.

위치를 Seoul로 해준뒤,

테스트모드에서 시작으로 합니다.

만들기를 한뒤, "규칙"에 들어가서 아래처럼 입력한 뒤 게시해줍니다.
파이어베이스 프로젝트를 처음 생성하면 외부의 접근이 완전히 차단되어 있어 앱에서 업로드 시 Permission Denied 에러가 발생합니다. 초기 개발 및 테스트를 위해 Firestore와 Storage의 보안 규칙을 임시로 개방해 주어야 합니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true; // 테스트용으로 일단 모두 열기
}
}
}
Storage도 동일하게 진행하는데, 이건 버전을 업그레이드해야해서 업그레이드를해줍니다. 사용량에 따라 돈이 부과된다고합니다. 규칙에 아래처럼 추가해줍니다.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if true; // 테스트용으로 일단 모두 열기
}
}
}
이벤트 및 카메라 연동
앱에서 해당 위치를 길게 누르면 임시 마커를 찍고, 제보용 바텀 시트를 띄웁니다.
// 지도 길게 누르기 감지
naverMap.setOnMapLongClickListener { _, latLng ->
tempReportMarker?.map = null
tempReportMarker = Marker().apply {
position = latLng; map = naverMap
iconTintColor = Color.parseColor("#FF6600")
captionText = "제보 위치"
}
showReportBottomSheet(latLng) // 바텀 시트 오픈
}
// 카메라 호출 및 임시 파일(Uri) 생성
private fun dispatchTakePictureIntent() {
val file = createImageFile()
photoUri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", file)
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
}
takePhotoLauncher.launch(intent)
}
촬영한 뒤 제보하기를 누르면됩니다.


Firebase 데이터 업로드
촬영된 사진을 Storage에 올리고, 다운로드 URL을 받아 Firestore에 위치 정보를 함께 저장합니다.
private fun uploadReport(latLng: LatLng, type: String) {
val storageRef = FirebaseStorage.getInstance().reference
val firestore = FirebaseFirestore.getInstance()
val imageRef = storageRef.child("reports/${System.currentTimeMillis()}.jpg")
// 1. Storage에 이미지 업로드
imageRef.putFile(photoUri!!).addOnSuccessListener {
imageRef.downloadUrl.addOnSuccessListener { downloadUrl ->
// 2. Firestore에 저장할 데이터 구성
val reportData = hashMapOf(
"latitude" to latLng.latitude,
"longitude" to latLng.longitude,
"type" to type,
"imageUrl" to downloadUrl.toString(),
"status" to "PENDING", // 최초 업로드 시 상태는 '대기 중'
"timestamp" to com.google.firebase.Timestamp.now()
)
// 3. Firestore에 문서 생성
firestore.collection("reports").add(reportData)
.addOnSuccessListener {
Toast.makeText(this, "제보 완료! 관리자 검토 후 반영됩니다.", Toast.LENGTH_SHORT).show()
}
}
}
}


승인된(APPROVED) 제보 데이터를 지도에 표시하기
관리자가 제보 사진을 확인하고 상태를 "APPROVED"로 변경하면 앱에서 이를 감지하고 지도에 마커로 그려줍니다. 이때는 아래에 '사용자 제보'라고 표시됩니다.
private fun updateMarkers(type: String) {
val bounds = naverMap.contentBounds
// 로컬 DB(공공데이터) 마커 그리기 로직 (생략) ...
// Firebase에서 승인된 사용자 제보 데이터만 불러오기!!
FirebaseFirestore.getInstance().collection("reports")
.whereEqualTo("status", "APPROVED") // 상태가 승인된 것만 필터링!
.whereEqualTo("type", type)
.get()
.addOnSuccessListener { documents ->
for (document in documents) {
val lat = document.getDouble("latitude") ?: continue
val lng = document.getDouble("longitude") ?: continue
// 현재 사용자가 보고 있는 지도 화면(bounds) 안에 있는 마커만 생성
if (lat in bounds.southWest.latitude..bounds.northEast.latitude &&
lng in bounds.southWest.longitude..bounds.northEast.longitude) {
val marker = Marker().apply {
position = LatLng(lat, lng)
map = naverMap
icon = OverlayImage.fromResource(R.drawable.custom_marker_icon)
// 사용자 제보 데이터임을 명시
captionText = "사용자 제보"
captionColor = Color.parseColor("#3D6BF5")
captionMinZoom = 15.0
}
activeMarkers.add(marker)
}
}
}
}

'Android' 카테고리의 다른 글
| [GeoCoding] 주소를 위/경도 변환하기(Geoservice) (2) | 2026.06.08 |
|---|---|
| [flutter] - 이미지 자동으로 넘기기(PageController) (0) | 2026.04.29 |
| [Android] - 레이아웃이 상태바와 겹칠 때 (fitsSystemWindows) (0) | 2026.03.23 |
| [Android] - Naver Map API 연동하기 (0) | 2026.03.23 |
