FireStore로 게시물 만들기

김재현·2024년 9월 1일

오늘은 Firebase의 FireStore로 게시물을 만드는 법을 알아보겠습니다.

  • Firebase 설정

    먼저 FireStore Storage를 생성해주면 되는데 위치를 설정해주고

    테스트 모드로 시작해줍니다.
    (프로덕션 모드에서 시작해서 읽기/쓰기 권한을 true로 변경해줘도 될 것 같습니다!)

  • 프로젝트 설정

    프로젝트에서 Tools를 통해 Cloud Firestore를 추가하셔도 되고

dependencies {
	implementation("com.google.firebase:firebase-firestore:25.0.0")
}

다음과 같이 의존성을 추가해도 됩니다.

  • 게시물 생성

UI layer

먼저 데이터를 받아줍니다.

private fun initData() {
        val auth = CurrentUser.userData
        val uid = auth?.uid.toString()
        val profileImage = auth?.profileImage
        val name = auth?.name.toString()
        val email = auth?.email.toString()
        val postText = binding.etMakeText.text.toString()
        val time = LocalDateTime.now()

        val data = PostDataModel(
            uid = uid,
            postId = UUID.randomUUID().toString(),
            profileImage = profileImage,
            name = name,
            email = email,
            imageList = imageList,
            postText = postText,
            mapData = MapDataModel(placeName, addressName, lat?.toDouble(), lng?.toDouble()),
            createdAt = dateFormat(time)
        )
}
  • 여기서 CurrnetUser는 MainFragment가 onCreated 될 때 값을 초기화 해준 object 입니다.
    (data class CurrentUserModel을 반환값으로 가짐)
  • 게시물 생성은 현재 유저의 uid, name, email, profileImage 등을 보여주므로 현재 유저의 정보를 담아줍니다.
  • postId는 uid와 마찬가지로 각 게시물마다 고유의 값이 존재해야 수정, 삭제 등에서 혼선이 없고 식별이 가능하기 때문에 randomUUID를 통해 값을 지정해주었습니다.
  • createdAt 즉, 생성 시기는 게시물을 생성된 시기에 따라 순차적으로 보여주기 위해 받았고 추후에 게시물 생성 몇 초전, 몇 분전과 같이 생성 시간에 따른 로직을 추가할 예정이기 때문에 넣어주었습니다.
    (여기서 dateFormat은 util로 빼준 함수입니다.)
fun dateFormat(time: LocalDateTime) : String {
    return time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
} 

- 시간만 넣으면 변환 (편안) -
  • mapData는 data class MapDataModel로 받고 있는데 장소 이름과 주소, 경도, 위도를 받고 있습니다.
    현재 장소 이름은 null이 아닐 경우에 게시물에 표시되고 있고 추후에 naver 지도를 통해 받은 경도, 위도를 입력해서 지도에 위치를 나타내줄 생각으로 경도, 위도까지 같이 받고 있습니다.

사실 mapData를 받는 과정에서 viewModel을 사용했는데 data가 들어오지않아 새로 배운 기술을 사용해 보았습니다.
(후에 알았지만 fragment 생명주기의 영향을 받는 by viewModels를 사용했을 때는 안되던 것이
by activityViewModels를 사용하니 받아지더라구요...이 부분도 공부해서 다음에 포스트 작성해야겠습니다.)

현재 다음과 같이 바텀시트를 이용해서 검색 결과를 보여주고 아이템 클릭 리스너를 통해 클릭 시 데이터를 bottomSheetFragment.popBackStack() 하면서 전달해주어야 하는데 viewModel을 사용하지 않고 적용하려하니 좀 어려웠던 것 같다 ㅠㅠ...

검색하면서 알아보다보니 Fragment Result API를 이용해서 값을 전달해 줄 수 있다는 것을 알았고 한번 사용해보았다!

Fragment 간 result를 전달 하기 위해 사용하는데 간단하다.
전달하고 싶은 데이터를 담아서 클릭 리스너에 연결시켜주었다.

MapBottomSheetFragment -> SendData

Recyclerview ClickListener ->


val mapAdapter = KakaoMapListAdapter { data ->
            sendMapData(data)
            findNavController().popBackStack()
        }
        with(binding.rvMap) {
            LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
            adapter = mapAdapter
        }
	}



sendMapData -> 


private fun sendMapData(mapData: KakaoDocumentsModel) {
        val bundle = Bundle()
        bundle.putString("placeName", mapData.placeName)
        bundle.putString("addressName", mapData.addressName)
        bundle.putString("lat", mapData.lat)
        bundle.putString("lng", mapData.lng)
        setFragmentResult("data", bundleOf("mapData" to bundle))
    }

.
.
mapData를 번들로 담아서 recyclerview item 클릭 시 Fragment Result API로 넘겨주었다.

MakePostFragment -> RecieveData

private fun getMapData() {

        setFragmentResultListener("data") { data, bundle ->
            val mapData = bundle.getBundle("mapData")
            if (mapData != null) {
                binding.clMakeLocationText.visibility = View.VISIBLE
                binding.tvMakeLocationName.text = mapData.getString("placeName")
                binding.tvMakeLocationInfo.text = mapData.getString("addressName")

                placeName = mapData.getString("placeName")
                addressName = mapData.getString("addressName")
                lat = mapData.getString("lat")
                lng = mapData.getString("lng")

            } else {
                binding.clMakeLocationText.visibility = View.GONE
            }

            binding.ivMakeMapDelete.setOnClickListener {
                mapData?.clear()
                binding.clMakeLocationText.visibility = View.GONE
            }
        }
    }

데이터를 전달해줬으면 받는 쪽에서는 setFragmentResultListener를 통해 key값으로 data를 받을 수 있다.

지금은 해결방법을 알아서 viewModel을 통해서 데이터를 전달하고 사용하지만 Fragment Result API를 통해서 데이터를 전달하고 받는 과정도 공부해봐서 좋았던 것 같다.
.
.
.
이제 데이터가 준비됐다면 Firebase FireStore로 데이터를 올려줄 차례이다.
domain layer에서 먼저 작업을 해보자!


Domain layer

먼저 repository에서 함수를 작성해준다.

DataRepository

interface DataRepository {
	suspend fun uploadPost(postData: PostDataEntity): Flow<Boolean>
}

그 다음 useCase를 작성하고 data layer로 넘어가보자

UploadPostUseCase

class UploadPostUseCase @Inject constructor(private val dataRepository: DataRepository) {
    suspend operator fun invoke(postData: PostDataEntity) : Flow<Boolean> {
        return dataRepository.uploadPost(postData)
    }
}

(invoke에 관해서도 한번 공부할 필요가 있어보인다...)


Data layer

DataRpositoryImpl
impl에서 먼저 constructor로 auth, firestore, storage를 생성해준다.

class DataRepositoryImpl @Inject constructor(
    private val auth: FirebaseAuth,
    private val db: FirebaseFirestore,
    private val storage: FirebaseStorage
): DataRepository

storage는 따로 소개가 되지 않았지만 이미지는 firestore에 바로 올릴 수 없기 때문에 storage에 저장한 다음 downloadUrl을 통해 가져와야 부를 수 있습니다!

(storage도 firestore 처럼 종속성 추가하면 됩니다!! 과정이 같아서 생략...ㅎ)


그 다음 아까 작성해둔 uploadPost를 override하여 여기서 구현하면 된다.

override suspend fun uploadPost(postData: PostDataEntity): Flow<Boolean> {
        return flow {
            try {
                val currentUser = auth.currentUser?.uid
                val imageList = arrayListOf<ImageDataEntity>()
                if (currentUser != null) {
                    val data = hashMapOf(
                        "postId" to postData.postId,
                        "uid" to currentUser,
                        "profileImage" to postData.profileImage,
                        "name" to postData.name,
                        "email" to postData.email,
                        "postText" to postData.postText,
                        "mapData" to postData.mapData,
                        "createdAt" to postData.createdAt
                    )
                    db.collection("post").document(postData.postId).set(data).await()

                    if (postData.imageList != null) {
                        for (i in 0 until postData.imageList.count()) {
                            val imageToUri = postData.imageList[i].imageUri.toUri()
                            val storageRef =
                                storage.getReference("image")
                                    .child("${currentUser}/${postData.postId}/${postData.createdAt}_${i}")
                            if (imageToUri.pathSegments?.contains("video") == true) {
                                storageRef.putFile(imageToUri).addOnSuccessListener {
                                    storageRef.downloadUrl.addOnSuccessListener { downloadUri ->
                                        imageList.add(
                                            ImageDataEntity(
                                                downloadUri.toString(),
                                                "video"
                                            )
                                        )
                                        val imageData =
                                            mapOf("imageList" to imageList.sortedBy { it.imageUri })
                                        db.collection("post").document(postData.postId)
                                            .update(imageData)
                                    }
                                }
                            } else {
                                storageRef.putFile(imageToUri).addOnSuccessListener {
                                    storageRef.downloadUrl.addOnSuccessListener { downloadUri ->
                                        imageList.add(
                                            ImageDataEntity(
                                                downloadUri.toString(),
                                                "image"
                                            )
                                        )
                                        val imageData =
                                            mapOf("imageList" to imageList.sortedBy { it.imageUri })
                                        db.collection("post").document(postData.postId)
                                            .update(imageData)
                                    }
                                }
                            }
                        }
                    }
                    emit(true)
                }

            } catch (e: Exception) {
                emit(false)
            }
        }
    }

코드가 좀 길긴한데 코드를 더럽게 못짜서 그런가 싶다...하나씩 살펴보자

먼저 아래 코드로 데이터를 firestore에 올려준다.
db.collection("post").document(postData.postId).set(data).await()

여기서! 굳이 데이터를 hashMap의 형태로 바꿔줄 필요는 없고 나는 image 데이터를 따로 올려주려고 하다보니 한번 다시 하나하나 직접 바꿔주었다. data class 형태 그대로 올려도 무방하다.

그리고 image 데이터가 사실 거의 메인인데...
for 문을 통해 0 부터 imageList.count() or imageList.size까지 imageData의 downloadUrl을 해당 post에 updata 시켜준다. storage에 있는 이미지들을 한번에 변환시켜줄 수 있는 방법이 있는 지는 모르겠지만...내가 알아본 정보로는 없어서...그렇다 ㅠㅠ

다음 구문을 통해 내가 저장하고 싶은 image의 이름과 경로를 지정해준다.

storage.getReference("image").child("${currentUser}/${postData.postId}/${postData.createdAt}_${i}")

그 다음 나는 video와 image 두 가지 타입을 구분해줘야 했기 때문에 다음 구문을 작성했다.

  if (imageToUri.pathSegments?.contains("video") == true) {
                                storageRef.putFile(imageToUri).addOnSuccessListener {
                                    storageRef.downloadUrl.addOnSuccessListener { downloadUri ->
                                        imageList.add(
                                            ImageDataEntity(
                                                downloadUri.toString(),
                                                "video"
                                            )
                                        )
                                        val imageData =
                                            mapOf("imageList" to imageList.sortedBy { it.imageUri })
                                        db.collection("post").document(postData.postId)
                                            .update(imageData)
                                    }
                                }

if 문을 통해 만약 imageUri에 "video"가 포함되어 있다면
(이 부분은 Log로 직접 image와 video의 다른 점을 찾아야 합니다! 제가 사용한 imagePicker는 달라서...)

putFile 후 addOnSuccessListener로 putFile에 성공했을 때 storageRef.downloadUrl에서 downloadUri를 가져옵니다. (이 친구가 중요합니다!)

가져온 uri를 imageList에 담아줍니다. 저는 추후에 구분을 편하게 하기 위해 type: String과 Uri: String으로 값을 받아줬습니다.

이제 imageData를 아까 다른 postData를 set해준 경로에 update 시켜주면 됩니다.
(이미지도 같은 방법으로 pathSegments에 contain("image")일 경우에 다음과 같이 진행해주면 됩니다!)

결과화면


다음과 같이 데이터가 잘 들어오는 모습을 볼 수 있습니다.
.
.
.
만약 hilt를 사용하고 계신다면 di module을 통해 firebase @Provides 해야합니다!
repository는 @Binds!
.
.
.

참고문헌


https://moon-i.tistory.com/entry/Fragment-Result-API


https://rkdrkd-history.tistory.com/3


https://velog.io/@simsubeen/Android-Kotlin-Firebase-Storage-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C%EC%82%AD%EC%A0%9C


0개의 댓글