Topic = 게시글 이미지 여러장 수정하기
- 위에 링크 게시물에서 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]
'TIL' 카테고리의 다른 글
[TIL] Kotlin Firestore, Storage [ 5 ] 게시글 수정한 내용 적용해주기, 단일 LiveData (3) | 2023.10.31 |
---|---|
[Sub TIL] Kotlin - 정적 타입, 동적 타입 + Kotlin 변수 타입 특징 (1) | 2023.10.31 |
[TIL] Kotlin Firestore, Storage [ 3 ] 게시글 디테일 페이지로 이동하기, 수정하기 ( 좋아요 기능 ) (2) | 2023.10.29 |
[TIL] Kotlin Storage를 Adapter의 내부에서 호출 시 문제 (1) | 2023.10.28 |
[TIL] Kotlin Firestore, Storage [ 2 ] 이미지와 함께 게시글 데이터 가져오기 (2) | 2023.10.22 |