Apple is Apple

일일 회고


오늘의 키워드

  • 리뷰 작성 flow

작성 flow

  1. 이미지를 가져오기 위해 갤러리 접근 권한을 얻는다.
  2. 이미지 및 리뷰 항목을 작성한다.
  3. 등록 버튼을 누른다.
    1. 이미지를 먼저 파이어 스토어에 저장한다.
    2. 저장을 성공하면 ReviewWritingModel의 imageUrl 값을 업데이트한다. (실제 접근할 imageUrl로 만듦)
    3. 업데이트 성공하면 firestore에 리뷰를 저장한다.(업데이트)
    4. 저장을 성공하면 CalendarUserModel의 isReviewed 값을 업데이트 한다 (리뷰가 작성된 상태로 만듦)

이미지를 가져오기 위해 갤러리 접근 권한을 얻는다.33 이상 버전은 READ_MEDIA_IMAGES, 이하 버전은 READ_EXTERNAL_STORAGE 권한을 통해 갤러리에 접근 할 수 있다.MVVM 아키텍쳐를 이용하여 권한은 얻는 로직은 MainActivty로 위임을 시켰다. (ActivityCompat 사용을 위해)

fun checkPermission(
    context: Context,
    permission: String,
    permissionLauncher: ActivityResultLauncher<String>,
    showPermissionContextPopUp: () -> Unit,
    runTaskAfterPermissionGranted: () -> Unit
) {
    when {
				// 권한이 설정 되어 있으면 다음 로직 실행
        ActivityCompat.checkSelfPermission(
            context as MainActivity,
            permission,
        ) == PackageManager.PERMISSION_GRANTED -> {
            runTaskAfterPermissionGranted()
        }
        // 권한 안내가 필요 하면
        ActivityCompat.shouldShowRequestPermissionRationale(
            context,
            permission
        ) -> {
            showPermissionContextPopUp()
        }
        // 그외이면 다시 권한 요
        else -> {
            permissionLauncher.launch(permission)
        }
    }
}

갤러리 접근 권한 이외에, 위치 접근 권한 코드도 있어 중복 코드를 방지하기 위해 checkPermission이라는 함수를 만들어 재 사용 할 수 있도록 하였다.사용자가 권한을 취소 하고, 다시 권한이 필요한 동작을 할 때, 이 메소드를 통해 안내 팝업을 보여 줄 수 있다.

private val getGalleryImageLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
            if (it.resultCode == Activity.RESULT_OK) {
                val entity = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    it.data?.getParcelableExtra(URI_LIST_KEY, GalleryPhotoEntity::class.java)
                } else {
                    it.data?.getParcelableExtra(URI_LIST_KEY)
                }
                if (entity != null) {
                    binding.reviewWritingAddImageView.isGone = true
                    imageUrlValue = entity.uri.toString()
                    binding.reviewWritingImageButton.load(entity.uri)
                } else {
                    requireActivity().toast(사진을 가져오지 못했습니다.)
                }
            } else {
                requireActivity().toast(getString(사진을 가져오지 못했습니다.)
                return@registerForActivityResult
            }
        }

모든 정보를 입력하고, 등록 버튼을 눌렀을 때, 상태를 살펴보자

  1. 먼저 이미지를 파이어베이스 스토리지에 저장한다.

먼저 이미지만 저장하는 이유는 위에서 이미지를 갤러리에서 가져왔을 떄, 안드로이드 기기의 저장소 uri에서 가져오는데, 추후 보여줄 때는, 스토리지에서 이미지를 불러와야 하기 때문이다.스토리지에 저장을 하는데, review/사용자아이디 형식으로 디렉토리를 설정하여 사용자 별로 이미지를 구분 할 수 있도록 하였다. 그리고 downloadUrl을 통해 업로드 된 Url을 가져 올 수 있도록 하였다.

  1. 저장을 성공하면 ReviewWritingViewModel의 imageUrl 값을 업데이트한다. (실제 접근할 imageUrl로 만듦)
val reviewedModel = reviewWritingViewModel.copy(
                    imageUrl = returnImageUrl
                )

받아온 imageUrl을 copy를 통해 업데이트 해준다.

  1. 업데이트 성공하면 firestore에 리뷰를 저장
fireStore.runTransaction { transaction ->
		     val reviewReference =
		            fireStore.collection("reviews").document(userInfo).collection("review")
		                .document(documentId)

						when (writingType) {
												// 새로운 글 작성
				                **WritingType.NEW -> {
				                    transaction.set(
				                        reviewReference,
				                        reviewedModel
				                    )
				                }**
												// 기존 글 업데이트
				                WritingType.MODIFY -> {
				                    transaction.update(
				                        reviewReference,
				                        reviewedModel.toMap(),
				                    )
				                }
				            }
}.await()

파이어베이스 스토어에 저장을 하는데 runTranscation을 이용한다.데이터 정합성을 위해 트랜잭션을 사용하여 저장을 진행하였다.

저장을 성공하면 해당 일정의 isReviewed 값을 업데이트 한다 (리뷰가 작성된 상태로 만듦)

val userScheduleReference =
            fireStore.collection("calendar").document(userInfo).collection("plans")
                .document(documentId)

        // 데이터 무결성을 위해 트랜잭션 사용
        fireStore.runTransaction { transaction ->
						// 먼저 일정 객체를 가져옴
            val schedule = transaction.get(userScheduleReference).toObject<CalendarEntity>()
            // 일정 객체가 존재한다면
						if (schedule != null) {
                transaction.set(
                    userScheduleReference,
                    schedule.copy(
                        isReviewed = true
                    ),
                    // 바뀐 부분을 merge 한다. (기본 옵션은 덮어 쓰기)
                    SetOptions.merge()
                )
            }
        }.await()

특정 일정에 대한 리뷰를 작성했으면, 했다는 것을 알릴 필요성이 있어 진행을 하였다.SetOption.merge()를 토해 바뀐 부분만 업데이트를 하도록 하였다.

 

이 역시 데이터 정합성을 위해 트랜잭션 내에서 실행을 하였다.

 

파이어스토어에 저장되어있는 일정을 가져와서 isReviewed값을 true로 바꿔주는 작업을 진행하였다.

 

(트랜잭션이란 쪼갤 수 없는 업무 처리의 최소 단위라고 하며 트랜잭션 진행 중 문제가 발생하면, 어떠한 데이터도 입력되지 않는다 → 일부만 수행되는 것이 아니라 트랜잭션 내의 모든 것이 수행 되야하므로 데이터 정합성 확보)

 

등록을 하는 행위는 새로운 데이터를 집어 넣는 행위이다. 그렇기 때문에 모든 데이터가 잘 들어갈 필요성이 있다(데이터 정합성)

 

그 후 , 업데이트 성공하면 firestore에 리뷰를 저장하고, 저장을 성공하면 CalendarUserModel의 isReviewed 값을 업데이트 한다 (리뷰가 작성된 상태로 만듦)

 

private suspend fun saveNewImage(reviewWritingModel: ReviewWritingModel): String { 
    return storage.reference.child("reviews/${userInfo}").child(fileName) 
                .putFile(reviewWritingModel.imageUrl.toUri()) 
                .await() 
                .storage 
                .downloadUrl 
                .await() 
                .toString()
    }

 

권한을 얻은 후, gallerylauncher를 통해 갤러리에서 이미지를 하나 가져온다.

사용자가 처음에는 권한을 설정 하지 않을 수 있다. 이럴 때는 앱에서 왜 권한이 필요한지 안내를 해줄 필요성이 있다. 이때 사용 할 수 있는 것이 shouldShowRequestPermissionRationale 메소드이다.

 

private fun checkGalleryPermissions(permission: String) {
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { 
        checkPermission( 
        context = this, 
        permission = permission, 
        permissionLauncher = permissionGalleryLauncher, 
        showPermissionContextPopUp = { 
        	showGalleryPermissionPopUp() 
        }, 
    	runTaskAfterPermissionGranted = { 
    		sharedViewModel.runGalleryEvent() 
    	}
    ) 
    return 
  } 
    checkPermission( 
    	context = this,
        permission = permission, 
        permissionLauncher = permissionGalleryLauncher, 
        showPermissionContextPopUp = { 
            showGalleryPermissionPopUp()
        }, 
        runTaskAfterPermissionGranted = { 
            sharedViewModel.runGalleryEvent() 
        }
    ) 
} 
reviewWritingImageButton.setOnClickListener { 
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 
    	sharedViewModel.getGalleryPermissionEvent(Manifest.permission.READ_MEDIA_IMAGES)
    } else { 
    	sharedViewModel.getGalleryPermissionEvent(Manifest.permission.READ_EXTERNAL_STORAGE) 
    }
}

 

안드로이드 API 버전 33 이상부터 갤러리 접근 권한이 바뀌어 분기 처리를 해주어야 했다.

수정 flow

수정은 작성과 거의 비슷하다.

다른 점은 수정할 때는 기존 리뷰의 이미지를 지우고 다시 새로운 이미지를 넣는다는 점이다. (삭제하지 않으면 이미지가 계속 스토리지 상에 남아있는다.)

private suspend fun removePastImageAndSaveNewImage(reviewWritingModel: ReviewWritingModel): String {
        // 삭제 하고 다시 집어넣기
        storage.reference.child("reviews/${userInfo}").child(fileName)
            .delete()
            .await()
        return saveNewImage(reviewWritingModel)
    }

delete 메소드를 통해 기존의 이미지를 지우고 새로 이미지를 저장한다.

fireStore.runTransaction { transaction ->
	val reviewReference =
	        fireStore.collection("reviews").document(userInfo).collection("review")
	
	transaction.update(
	      reviewReference,
	      reviewedModel.toMap(),
	)
)

수정은 다양한 속성을 수정 할 수 있으므로, 트랜잭션의 update 메소드를 통해 한 번에 수정하는 작업을 진행한다.

이 역시 데이터 정합성을 위해 트랜잭션 내에서 실행을 하였다.

profile

Apple is Apple

@mjjjjjj