본문 바로가기

TIL

[TIL] Kotlin Firestore, Storage [ 4 ] 게시글 이미지 여러장, 글 수정하기

Topic =  게시글 이미지 여러장 수정하기

 

 

 


 

 

 

[TIL] Kotlin Firestore, Storage [ 3 ] 게시글 디테일 페이지로 이동하기, 수정하기 ( 좋아요 기능 )

Topic = 클릭 한 게시글 Item의 디테일 페이지로 이동하기, 수정하기 ( 좋아요 기능 ) 게시글 디테일 페이지 작업 전 사전 작업 지난 글에서 MyPostFeedFragment에서 CurrentUser의 Uid를 통해서 내가 쓴 글을

junes-daily.tistory.com

  • 위에 링크 게시물에서 RecyclerView의 특정 아이템 클릭 Listener를 통해 해당 아이템의 데이터를 ViewModel의 LiveData에 postvalue로 데이터를 넘기는 내용에 이어서 작성. 
  • 이번 게시물에서 정리할 내용 -
    • 수정 페이지인 PostEditFragment도 LiveData의 observe를 통해 데이터를 현재 게시글의  정보를 받아오기.
    • 수정 페이지가 빈 페이지로 보이면 안되니 기존 데이터를 editText, 선택된 카테고리 표시 (clicked chip) 및 이미지를 불러온다.
    • 완료 버튼 클릭 시 입력된 필드 값을 기존의 게시글 정보에 업데이트 해준다.

 

 

게시물 DB 수정하기 [ 1 ] - 현재 디테일 페이지에 해당하는 게시글 정보 수정페이지로 넘겨주기 

 

ViewModel.kt

class MyPostFeedViewModel : ViewModel() {

    ...
    
    // 수정 페이지 이동 시 기존에 작성 되어 있던 정보 불러오기
    private val _currentPostEditPage = MutableLiveData<PostRcv>()
    val currentPostToEditPage : MutableLiveData<PostRcv> get() = _currentPostEditPage
    
    ...
    
    
    // 게시물 수정 페이지 정보 이동하기
    fun setCurrentPostEdit(postRcv : PostRcv){
        _currentPostEditPage.value = postRcv
    }
}
  • ViewModel의 setCurrentPostEdit() 함수를 통해 LiveData으로 값을 넘겨줄 수 있도록 정의해준다.

 

DetailFragment.kt

    private val currentPostInfo = mutableListOf<PostRcv>()
    
    override fun onCreateView(...) {
    
   
        val imgs = mutableListOf<Uri>()
        myPostFeedViewModel.currentPost.observe(viewLifecycleOwner) { it ->
            binding.detailId.text = it.nickname
            ...
            binding.detailTvLikeCount.text = "${it.likeUsers.size}"
            
            // 수정할 페이지에 넘길 데이터
            currentPostInfo.add(it)
            
            
            // 수정 버튼 visibility
            if (it.uid == Constants.currentUserUid){
                binding.detailBtnEditPost.visibility = View.VISIBLE
            }
        }
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        ...
        
        // 게시물 수정 버튼 클릭 이벤트
        binding.detailBtnEditPost.setOnClickListener {
            // myPostFeedViewModel.currentPostToEditPage.postValue(currentPostInfo[0])
            // or
            myPostFeedViewModel.setCurrentPostEdit(currentPostInfo[0])
            requireActivity().supportFragmentManager.beginTransaction().replace(R.id.frag_edit,PostEditFragment()).addToBackStack(null).commit()

        }
        
        ...
  • 디테일 페이지의 게시글 정보를 받아온 뒤 해당 게시글 정보를 currentPostInfo에 담아 ViewModel의 currentPostEditPage LiveData에 데이터를 담아주고 수정 페이지로 Fragment를 전환해준다. 
  • 게시물 수정 버튼 클릭 이벤트 내에 LiveData에 데이터를 이동하는 2가지 방법이 있는데 차이점은 아래에서 간단하게 설명할 예정.

 

 

✅ Why - Fragment 간의 데이터 이동 방식의 차이

 

[ 1 ] - Fragment에서 직접적으로 LiveData에 값을 지정해주는 방식

 

[ 2 ] - Fragment에서 View모델의 함수를 통해 간접적으로 LiveData에 값을 지정해주는 방식

 

Fragment 에서의 작성 방법 차이

[ 1 ] - myPostFeedViewModel.currentPostToEditPage.postValue(currentPostInfo[0])

[ 2 ] - myPostFeedViewModel.setCurrentPostEdit(currentPostInfo[0])

 

 

ViewModel 에서의 차이점 - 

1. ViewModel에서 별도로 함수를 지정해주지 않아도 된다.

var currentPostToEditPage = MutableLiveData<PostRcv>()

 



2. ViewModel에서 별도로 함수를 지정해주어야 한다.

 

    // 수정 페이지 이동 시 기존에 작성 되어 있던 정보 불러오기
    private val _currentPostEditPage = MutableLiveData<PostRcv>()
    val currentPostToEditPage : MutableLiveData<PostRcv> get() = _currentPostEditPage
    
    // 게시물 수정 페이지 정보 이동하기
    fun setCurrentPostEdit(postRcv : PostRcv){
        _currentPostEditPage.value = postRcv
    }

 

이번 프로젝트 진행 초기에는 Fragment 에서 LiveData에 직접적으로 데이터를 넣는 방식으로 데이터를 이동하다가, ViewModel의 함수를 통해서 LiveData 값을 간접적으로 변경하는 방식으로 변경하였다.

왜 변경했을까?
- 직접적으로 LiveData에 데이터를 넣어 변경하는 방식은 간단하며 직관적인 방식으로 Data를 옮길 수 있도록 단순하게 작성되었지만 이러한 방식은 MVVM패턴의 View와 ViewModel의 관계를 무시하며 View의 데이터를 직접 조작하여 LiveData의 데이터를 추가하므로, View와 ViewModel 사이의 의존성을 만들게 되는 결과를 낳을 수 있어 유지보수성 측면에서도 좋지 못한 방법이다.

 

 

게시물 DB 수정하기 [ 2 ] - EditFragment에서 데이터 수정 및 DB에 수정된 데이터 업데이트하기

 

[ 2 - 1 ] Edit Fragment View에 기존 아이템 정보 가져오기

Fragment

    private var uris: MutableList<Uri> = mutableListOf()
    private var currentPost : PostRcv? = null
    
    // 이미지 선택 기능
    private val pickMultipleMedia =
        registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(10)) { uriList ->
            if (uriList.isNotEmpty()) {
                uriList.forEach { uri ->
                    Log.d("xxxx", "Selected URI: $uri")
                }
                uris.addAll(uriList)

                Log.d("xxxx", "Edit Frag Number of items selected : ${uris} ")
                writePostImgAdapter.submitList(uris)
                binding.imageCount.text = "${uris.size}/10"
                writePostImgAdapter.notifyDataSetChanged()
            } else {
                Log.d("xxxx", "Edit Frag No media selected: ")
            }
        }
        
    private fun setupRcv() {
        writePostImgAdapter = WritePostImageAdapter(object : ImgClick {
            override fun imgClick(uri: Uri) {
                Log.d("xxxx", "imgClicked: ${uri} , whole uris : $uris")
                // todo 아이템 클릭 시 다이얼로그 or 삭제 버튼
                Util.showDialog(requireContext(),"이미지 삭제","선택한 이미지를 삭제 하시겠습니까?"){
                    uris.remove(uri)
                    binding.imageCount.text = "${uris.size}/10"
                    writePostImgAdapter.notifyDataSetChanged()
                }

            }

        })
        binding.recyclerView.apply {
            setHasFixedSize(true)
            layoutManager =
                LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
            adapter = writePostImgAdapter
        }
    }
  • 기존 게시글의 이미지 리스트와 새로 추가된 이미지 리스트를 담기위해 mutableList<Uri> 형식으로 List를 선언해준다.
  • 모든 데이터를 변경하지 않고 기존 게시글의 데이터를 유지하는 Field 값도 있어 기존 게시글의 정보를 저장하는 currentPost 변수도 선언해둔다. 
  • 이미지는 Rcv의 아이템 클릭 시 Dialog를 통해 제거할 수 있도록 정의 해주었다.

 

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 선택된 이미지 리스트 확인 RecyclerView
        setupRcv()

        // 이미지 선택 ( 새로운 이미지 추가 )
        binding.editBtnSelectImg.setOnClickListener {
            pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
        }

        // 기존 포스트 정보 가져와서 수정 페이지에 띄우기
        var category : String
        myPostFeedViewModel.currentPostToEditPage.observe(viewLifecycleOwner){ post ->
            currentPost = post

            binding.imageCount.text = "${post.imgs.size}/10"
            
            // 기존 게시글 정보의 이미지를 uris에 담아준다.
            uris.addAll(post.imgs)

            // 게시글 불러오기와 동일하게 기존 데이터를 수정페이지에서 확인할 수 있도록 처리
            binding.editTvTitle.setText("${post.title}")
            ...


            // 기존의 이미지 리스트를 Rcv에 적용.
            writePostImgAdapter.submitList(uris)
            writePostImgAdapter.notifyDataSetChanged()
            
            ...
            
        }   
    }
    
    ...
  • 받아온 데이터를 토대로 각 View에 적용시켜준다.

 

 

[ 2 - 2 ] EditFragment - 게시글 수정 로직 작성하기

 

게시글 수정 후 업로드를 하려고 기존 포스트의 Storage에서부터 가져온 이미지 2개, 새로 추가된 이미지 1개로 3개의 이미지를 업로드 하려는데 아래와 같은 오류가 발생했다.

 

imageUpload Failure : com.google.firebase.storage.StorageException: An unknown error occurred, please check the HTTP result code and inner exception for server response.
  • 처음에는 기존과 동일한 파일을 path를 변경해서 다시 업로드 해서 문제가 생기는 줄 알고 Storage에 기존 데이터를 삭제한 후 업로드를 시도해보았는데, 동일한 오류가 발생했다.
  • 해당 오류가 발생한 원인은 Storage에서 downloadUrl을 통해서 가져온 URL 을 List<Uri> 에 넣어서 사용해서 Uri 인 줄 알았으나 근본적으로 URL파일인 것이다. Storage에는 읽기 및 다운로드 용도인 URL 형식의 파일은 업로드할 수 없다.
  • 때문에 EditFragment의 List<Uri>의 형태의 uris 에는 기존 포스트에 저장되어 있던 URL 2개, 새롭게 추가된 URI 1개로 이루어져 있는 상황인 것이다.
  • URL을 URI 로 변환하는 것이 불가능하기 때문에 수정 전 Post의 이미지 데이터는 Stream의 형태로 변환하여 InputStream 으로 Storage에 추가해주고, 새로 추가된 이미지는 처음 Post에 추가하던 것과 같이 InputFile을 통해 저장해준다.

 

 

ViewModel.kt

    // 포스트 수정된 DB 업로드
    fun uploadEditPost(uris : MutableList<Any>, post : Post) {
        viewModelScope.launch(Dispatchers.IO) {
        val imgs : MutableList<String> = mutableListOf()
        fun getTime(): String {
            val currentDateTime = Calendar.getInstance().time
            val dateFormat =
                SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.KOREA).format(currentDateTime)

            return dateFormat
        }
        val time = getTime()
        uris?.let { uriAndUrl ->
            Log.d("xxxx", "imageUpload 1. uris : $uris")
                try {
                    var count : Int = 0
                    for (item in uriAndUrl){ // index in uriAndUrl로 하면 작동이 안됨.
                        count++
                        val fileName = "${time}_$count"
                        imgs.add(fileName)
                        when (item) {
                            is Uri -> {
                                storage.reference.child("post").child("${time}_$count").putFile(item)
                                    .addOnSuccessListener {
                                        Log.d("xxxx", " Uri Succesful : $it")
                                    }
                                    .addOnFailureListener {
                                        Log.d("xxxx", " Uri Failure : $it")
                                    }
                            }
                            is URL -> {
                                val connection = item.openConnection() as HttpURLConnection
                                connection.connect()
                                val inputStream = connection.inputStream
                                storage.reference.child("post").child("${time}_$count").putStream(inputStream)
                                    .addOnSuccessListener{
                                        Log.d("xxxx", " URL Successful : $it ")
                                    }
                                    .addOnFailureListener{
                                        Log.d("xxxx", " URL Failure : $it ")
                                    }
                            }
                            else -> {}
                        }
                    }
                }catch (exception : StorageException){
                    val errorCode = exception.errorCode
                    Log.d("xxxx", " 파일 업로드 실패 errorCode: ${errorCode}")
                }

            // timestamp 기준으로 수정하는 게시글 data를 가져와 변경된 게시글로 수정
            post.imgs = imgs
            db.collection("Posts").whereEqualTo("timestamp",post.timestamp).get()
                .addOnSuccessListener {querySnapshot ->
                    Log.d("xxxx", "uploadEditPost: 해당 게시글 $querySnapshot")
                    if (!querySnapshot.isEmpty){
                        querySnapshot.documents[0].reference.set(post)
                            .addOnSuccessListener {
                                Log.d("xxxx", "imageUpload: 게시글 수정 완료 ")
                            }
                            .addOnFailureListener {
                                Log.d("xxxx", "uploadEditPost: 게시글 수정 실패 $it")
                            }
                    }
                }
                .addOnFailureListener {
                    Log.d("xxxx", "uploadEditPost: 초장부터 실패 $it")
                }
        }
        }
    }

 

 

Fragment.kt

        // 업로드 하기
        binding.btnComplete.setOnClickListener {
            Log.d("xxxx", " postEditFrag 완료 버튼 클릭")
            val post = Post(
                Constants.currentUserUid!!,
                binding.editTvTitle.text.toString(),
                binding.editEtvPrice.text.toString(),
                editCategory,
                binding.editEtvAddress.text.toString(),
                //todo ↓ deadline 추가 - 임시로 city 값 넣어둠
                binding.editEtvAddress.text.toString(),
                binding.editEtvDesc.text.toString(),
                listOf(),
                Constants.currentUserInfo!!.nickname,
                currentPost!!.likeUsers,
                currentPost!!.token,
                currentPost!!.timestamp,
                currentPost!!.state,
                currentPost!!.documentId,
            )
            Log.d("xxxx", "onViewCreated: ${uris}")

            val uriAndUrlList = mutableListOf<Any>()
            uriAndUrlList.addAll(uris)
            for ( index in uriAndUrlList.indices){
                if (currentPost!!.imgs.contains(uriAndUrlList[index])){
                    uriAndUrlList[index] = (URI(uriAndUrlList[index].toString()).toURL())
                }
                else {
                }
            }

            myPostFeedViewModel.uploadEditPost(uriAndUrlList,post)
            parentFragmentManager.popBackStack()
        }
  • 변경된 데이터를 업데이트를 수행하는 코드를 지정해줌으로 완료버튼 클릭 시 데이터가 수정될 수 있도록 한다.
  • 디테일 페이지에 존재하는 기존 PostRcv 데이터의 imgs는 Firestore의 downloadUrl 을 통해서 가져온 이미지기 때문에 currentPost에서 가지고 있는 imgs 와 수정 완료 시점의 이미지 리스트인 uris 를 비교해서 기존의 imgs는 URL형식으로 변환해준다 - 원래 URL을 Uri로 사용할 수 있도록 리스트에 넣어둔 것이기 때문에 업로드 시에는 원래 형태인 URL로 변환 후 Stream으로 변환할 수 있도록 해주어야 한다.

 

 

 

 

 

 

 

 


 

[ A. 오늘 복습한 내용 / B. 다음에 학습할 내용 ]

A. ...DB 구조 공사

 

B. DB 구조 공사..

 


 

[오류,에러 등등]

1.

 


 

[느낀 점]

1.  게시글 Firestore DB 구조를 변경 해야겠다. 기존에 게시글 필드인 imgs : List<String>에 Storage에 저장된 이미지의 경로를 불러오는 방법으로 작업해서 어찌저찌 잘 돌아가게끔 지저분하게 코드가 작성되었는데 초기에 게시글 DB에 저장할 때 imgs : List<String> 에 Storage 각 이미지에 해당하는 URL을 바로 저장해줄 수 있는 것을 조금 늦게 알았다. 해당 방식으로 하는 것이 기존 이미지 수정에도 용이할 것이고 코드가 더 깨끗해 질 것 같다.

 

2. URL, URI를 통해 이루어지는 작업을 하면서 URI, URL에 대한 이해가 적어서 위와 같은 문제가 발생한 것 같다. 기능 구현에 초점을 맞추기 보다 작업하면서도 어떻게, 무엇을 통해 왜 작업되는지 생각해보는 것이 좋을 것 같다.

 

3. 바꾸긴 해야하는데 언제 다 바꿀지

 

 


[Reference]