[Firebase] Realtime DB & Storage 로 게시물 업로드 기능 구현하기

LeeEunJae·2022년 7월 25일
2

Firebase

목록 보기
2/3

📌 실행화면

📌 ArticleModel.kt

Realtime database에 ArticleModel 객체로 저장하기 위해 필요한 data class

data class ArticleModel (
    val sellerId: String,
    val title: String,
    val createdAt: Long,
    val price: String,
    val imageUrl: String
){
    constructor(): this("","",0,"","")
}

📌 HomeFragment.kt

import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.dldmswo1209.usedgoods.R
import com.dldmswo1209.usedgoods.databinding.FragmentHomeBinding
import com.dldmswo1209.usedgoods.mypage.DBKey.Companion.DB_ARTICLES
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.ChildEventListener
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase

class HomeFragment: Fragment(R.layout.fragment_home) {
    private var mBinding: FragmentHomeBinding? = null
    private val binding get() = mBinding!!
    private lateinit var articleAdapter: ArticleAdapter
    private val auth: FirebaseAuth by lazy {
        Firebase.auth
    }
    private val articleList = mutableListOf<ArticleModel>()
    private val listener = object: ChildEventListener{
        override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
            val articleModel = snapshot.getValue(ArticleModel::class.java) // 데이터베이스에서 가져오는데 ArticleModel 객체로 가져옴
            articleModel ?: return // null 처리

            articleList.add(articleModel) // 리스트에 추가
            articleAdapter.submitList(articleList)
        }
        override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {}
        override fun onChildRemoved(snapshot: DataSnapshot) {}
        override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
        override fun onCancelled(error: DatabaseError) {}
    }
    private lateinit var articleDB: DatabaseReference

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        mBinding = FragmentHomeBinding.bind(view)

        articleList.clear() // 게시물 리스트 초기화(중복 되서 생성되는 경우를 방지)
        articleDB = Firebase.database.reference.child(DB_ARTICLES) // Realtime DB reference 생성
        articleAdapter = ArticleAdapter() // 어답터 생성

        binding.articleRecyclerView.layoutManager = LinearLayoutManager(context)
        binding.articleRecyclerView.adapter = articleAdapter // 리사이클러뷰 어답터 연결

        articleDB.addChildEventListener(listener) // Realtime DB 에서 게시물 리스트를 가져옴

        binding.addFloatingButton.setOnClickListener { // 게시물 작성 버튼 클릭 이벤트
//            if(auth.currentUser == null){
//                Snackbar.make(view, "로그인 후 사용해주세요.", Snackbar.LENGTH_LONG).show()
//                return@setOnClickListener
//            }
            val intent = Intent(requireContext(), AddArticleActivity::class.java)
            startActivity(intent)
        }
    }

    override fun onResume() {
        super.onResume()

        articleAdapter.notifyDataSetChanged() // 게시물 업데이트
    }
    override fun onDestroy() {
        super.onDestroy()

        articleDB.removeEventListener(listener)
    }

}

articleDB = Firebase.database.reference.child(DB_ARTICLES) 로 데이터베이스 reference 를 생성하고, articleDB.addChildEventListener(listener) 로 이벤트 리스너로 listener를 등록한다.

게시물 작성 버튼은 로그인 후에 사용 할 수 있도록 이후에 구현하도록 하겠다.
지금은 데이터베이스에 정보를 저장/불러오기가 잘 되는지 확인 하기 위해서 주석 처리를 해놓았다.

📌 AddArticleActivity.kt

package com.dldmswo1209.usedgoods.home

import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.dldmswo1209.usedgoods.databinding.ActivityArticleAddBinding
import com.dldmswo1209.usedgoods.mypage.DBKey.Companion.DB_ARTICLES
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.ktx.storage

class AddArticleActivity : AppCompatActivity() {
    private var mBinding : ActivityArticleAddBinding? = null
    private val binding get() = mBinding!!
    private var selectedUri: Uri? = null
    private val auth: FirebaseAuth by lazy {
        Firebase.auth
    }
    private val storage: FirebaseStorage by lazy {
        Firebase.storage
    }
    private val articleDB: DatabaseReference by lazy {
        Firebase.database.reference.child(DB_ARTICLES)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityArticleAddBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.imageAddButton.setOnClickListener { // 이미지 등록 버튼 클릭 이벤트
            when{
                ContextCompat.checkSelfPermission(
                    this,
                    android.Manifest.permission.READ_EXTERNAL_STORAGE
                ) == PackageManager.PERMISSION_GRANTED -> {
                    // 권한이 부여된 경우
                    startContentProvider()
                }
                shouldShowRequestPermissionRationale(android.Manifest.permission.READ_EXTERNAL_STORAGE) -> {
                    // 사용자가 권한 요청을 명시적으로 거부한 경우
                    showPermissionContextPopup() // 팝업 요청
                }
                else -> {
                    // 그 외
                    requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE), 1010)
                }
            }
        }
        binding.submitButton.setOnClickListener { // 게시물 등록 버튼 클릭 이벤트
            val title = binding.titleEditText.text.toString()
            val price = binding.priceEditText.text.toString()
            val sellerId = auth.currentUser?.uid.orEmpty()

            showProgress() // 지연시간 동안 progressbar 를 보여줌
            // 중간에 이미지가 있으면 업로드 과정을 추가
            if(selectedUri != null){
                val photoUri = selectedUri ?: return@setOnClickListener
                uploadPhoto(photoUri,
                    successHandler = { uri->
                        uploadArticle(sellerId, title, price, uri)
                    },
                    errorHandler = {
                        Toast.makeText(this, "사진 업로드에 실패했습니다.", Toast.LENGTH_SHORT).show()
                        hideProgress()
                    })
            }else{
                // 이미지가 없는 경우 이미지 제외하고 등록
                uploadArticle(sellerId,title,price,"")
            }
        }
    }
    private fun uploadPhoto(uri: Uri, successHandler: (String) -> Unit, errorHandler: () -> Unit){
        val fileName = "${System.currentTimeMillis()}.png" // 이미지 파일 이름
        storage.reference.child("article/photo").child(fileName) // storage 에 article/photo/fileName 경로로 저장
            .putFile(uri)
            .addOnCompleteListener{
                if(it.isSuccessful){
                    // 업로드 성공시
                    storage.reference.child("article/photo").child(fileName).downloadUrl
                        .addOnSuccessListener { uri ->
                            // 다운로드 성공시
                            successHandler(uri.toString()) // successHandler 실행
                        }
                        .addOnFailureListener {
                            // 다운로드 실패시
                            errorHandler() // errorHandler 실행
                        }
                }else{
                    // 업로드 실패시
                    errorHandler()
                }
            }
    }
    private fun uploadArticle(sellerId: String, title: String, price: String, imageUrl: String){
        val model = ArticleModel(sellerId, title, System.currentTimeMillis(), "$price 원", imageUrl) // ArticleModel 생성
        articleDB.push().setValue(model) // Realtime DB에 저장

        hideProgress() // progressbar 를 숨김
        finish()
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when(requestCode){
            1010->{
                if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    startContentProvider()
                } else{
                  Toast.makeText(this, "권한을 거부하셨습니다.", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
    private fun startContentProvider(){
        val intent = Intent(Intent.ACTION_GET_CONTENT)
        intent.type = "image/*"
        startActivityForResult(intent, 2020)
    }
    private fun showProgress(){
        binding.progressbar.isVisible = true
    }
    private fun hideProgress(){
        binding.progressbar.isVisible = false
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if(resultCode != Activity.RESULT_OK){
            return
        }
        when(requestCode){
            2020 -> {
                val uri = data?.data
                if(uri != null){
                    binding.photoImageView.setImageURI(uri)
                    selectedUri = uri
                } else{
                    Toast.makeText(this, "사진을 가져오지 못했습니다.", Toast.LENGTH_SHORT).show()
                }
            }else -> {
                Toast.makeText(this, "사진을 가져오지 못했습니다.", Toast.LENGTH_SHORT).show()
            }
        }
    }
    private fun showPermissionContextPopup(){
        AlertDialog.Builder(this)
            .setTitle("권한이 필요합니다.")
            .setMessage("사진을 가져오기 위해 필요합니다.")
            .setPositiveButton("동의", { _,_ ->
                requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),1010)
            })
            .create()
            .show()
    }
}

이미지 등록 버튼 클릭시 권한 요청을 하는 코드와 게시물 등록 버튼 클릭시 Realtime DB에 게시물 정보를 저장하고, Storage에 image uri를 저장/불러오기를 하는 코드를 작성했다.

profile
매일 조금씩이라도 성장하자

0개의 댓글