From 91649423957771710f849548661bb6f42d69412a Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 22 Aug 2025 22:12:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(gallery):=20=EB=A1=9C=EB=94=A9=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=BA=90=EC=8B=B1=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fragment에서 isLoading에 따라 Loading Dialog를 표시/해제. Glide에 디스크 캐싱 적용으로 스크롤 성능 개선. --- .../detail/gallery/CharacterGalleryAdapter.kt | 2 + .../gallery/CharacterGalleryFragment.kt | 22 +++++ .../gallery/CharacterGalleryViewModel.kt | 88 +++++++++++-------- 3 files changed, 73 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryAdapter.kt index cc240ebc..349321fe 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryAdapter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryAdapter.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy import kr.co.vividnext.sodalive.databinding.ItemCharacterGalleryBinding class CharacterGalleryAdapter( @@ -19,6 +20,7 @@ class CharacterGalleryAdapter( fun bind(item: CharacterImageListItemResponse) { Glide.with(binding.ivImage) .load(item.imageUrl) + .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .into(binding.ivImage) if (item.isOwned) { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryFragment.kt index 1c3542be..1339adcc 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryFragment.kt @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.chat.character.detail.CharacterDetailActivity.Companion.EXTRA_CHARACTER_ID import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration +import kr.co.vividnext.sodalive.common.LoadingDialog import kr.co.vividnext.sodalive.databinding.FragmentCharacterGalleryBinding import kr.co.vividnext.sodalive.extensions.dpToPx import org.koin.androidx.viewmodel.ext.android.viewModel @@ -21,7 +22,9 @@ class CharacterGalleryFragment : BaseFragment( FragmentCharacterGalleryBinding::inflate ) { private val viewModel: CharacterGalleryViewModel by viewModel() + private lateinit var adapter: CharacterGalleryAdapter + private lateinit var loadingDialog: LoadingDialog private val characterId: Long by lazy { arguments?.getLong("arg_character_id") @@ -30,8 +33,11 @@ class CharacterGalleryFragment : BaseFragment( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + loadingDialog = LoadingDialog(requireActivity(), layoutInflater) setupRecyclerView() observeState() + viewModel.loadInitial(characterId) } @@ -70,6 +76,13 @@ class CharacterGalleryFragment : BaseFragment( View.GONE } + // 로딩 다이얼로그 표시/해제 + if (state.isLoading) { + loadingDialog.show(screenWidth) + } else { + hideLoadingDialog() + } + if (state.items.isNotEmpty() && !state.isLoading) { binding.rvGallery.visibility = View.VISIBLE binding.clRatio.visibility = View.VISIBLE @@ -117,4 +130,13 @@ class CharacterGalleryFragment : BaseFragment( viewModel.purchaseImage(item.id, position) }.show() } + + private fun hideLoadingDialog() { + loadingDialog.dismiss() + } + + override fun onDestroyView() { + hideLoadingDialog() + super.onDestroyView() + } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryViewModel.kt index 41fec3f1..168b0b23 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/character/detail/gallery/CharacterGalleryViewModel.kt @@ -39,7 +39,6 @@ class CharacterGalleryViewModel( isLastPage = false isRequesting = false accumulatedItems.clear() - _uiState.value = UiState(isLoading = true) request(page = currentPage) } @@ -51,6 +50,7 @@ class CharacterGalleryViewModel( private fun request(page: Int) { val token = "Bearer ${SharedPreferenceManager.token}" isRequesting = true + _uiState.value = _uiState.value?.copy(isLoading = isRequesting || isPurchasing) compositeDisposable.add( repository.getCharacterImageList( token = token, @@ -63,6 +63,9 @@ class CharacterGalleryViewModel( .subscribe({ response -> val success = response.success val data = response.data + + isRequesting = false + if (success && data != null) { // 누적 처리 val newItems = data.items @@ -88,18 +91,17 @@ class CharacterGalleryViewModel( ) } else { _uiState.value = _uiState.value?.copy( - isLoading = false, + isLoading = isRequesting || isPurchasing, error = response.message ?: "갤러리 정보를 불러오지 못했습니다." ) } - isRequesting = false }, { throwable -> + isRequesting = false Logger.e(throwable, throwable.message ?: "") _uiState.value = _uiState.value?.copy( - isLoading = false, + isLoading = isRequesting || isPurchasing, error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." ) - isRequesting = false }) ) } @@ -112,50 +114,58 @@ class CharacterGalleryViewModel( val token = "Bearer ${SharedPreferenceManager.token}" isPurchasing = true + _uiState.value = _uiState.value?.copy( + isLoading = isRequesting || isPurchasing, + ) compositeDisposable.add( repository.purchaseCharacterImage(token = token, imageId = imageId) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ response -> - val success = response.success - val data = response.data - if (success && data != null) { - // 응답 imageUrl로 교체, 소유 상태 true로 변경 - val updated = target.copy( - imageUrl = data.imageUrl, - isOwned = true - ) - accumulatedItems[position] = updated + .subscribe( + { response -> + val success = response.success + val data = response.data + isPurchasing = false + if (success && data != null) { + // 응답 imageUrl로 교체, 소유 상태 true로 변경 + val updated = target.copy( + imageUrl = data.imageUrl, + isOwned = true + ) + accumulatedItems[position] = updated - val total = _uiState.value?.totalCount ?: accumulatedItems.size.toLong() - val ownedBefore = _uiState.value?.ownedCount - ?: accumulatedItems.count { it.isOwned }.toLong() - val ownedAfter = ownedBefore + 1 - val ratio = if (total > 0) { - ownedAfter.toFloat() / total.toFloat() + val total = _uiState.value?.totalCount ?: accumulatedItems.size.toLong() + val ownedBefore = _uiState.value?.ownedCount + ?: accumulatedItems.count { it.isOwned }.toLong() + val ownedAfter = ownedBefore + 1 + val ratio = if (total > 0) { + ownedAfter.toFloat() / total.toFloat() + } else { + 0f + } + + _uiState.value = _uiState.value?.copy( + ownedCount = ownedAfter, + ratio = ratio, + items = accumulatedItems.toList(), + isLoading = isRequesting || isPurchasing, + error = null + ) } else { - 0f + _uiState.value = _uiState.value?.copy( + error = response.message ?: "구매에 실패했습니다." + ) } - + }, + { throwable -> + isPurchasing = false + Logger.e(throwable, throwable.message ?: "") _uiState.value = _uiState.value?.copy( - ownedCount = ownedAfter, - ratio = ratio, - items = accumulatedItems.toList(), - error = null - ) - } else { - _uiState.value = _uiState.value?.copy( - error = response.message ?: "구매에 실패했습니다." + isLoading = isRequesting || isPurchasing, + error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." ) } - isPurchasing = false - }, { throwable -> - Logger.e(throwable, throwable.message ?: "") - _uiState.value = _uiState.value?.copy( - error = "네트워크 오류가 발생했습니다. 잠시 후 다시 시도해 주세요." - ) - isPurchasing = false - }) + ) ) } }