feat(gallery): 로딩 다이얼로그 표시 및 이미지 캐싱 적용

Fragment에서 isLoading에 따라 Loading Dialog를 표시/해제.
Glide에 디스크 캐싱 적용으로 스크롤 성능 개선.
This commit is contained in:
2025-08-22 22:12:36 +09:00
parent e3ed816fb3
commit 9164942395
3 changed files with 73 additions and 39 deletions

View File

@@ -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) {

View File

@@ -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>(
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<FragmentCharacterGalleryBinding>(
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<FragmentCharacterGalleryBinding>(
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<FragmentCharacterGalleryBinding>(
viewModel.purchaseImage(item.id, position)
}.show()
}
private fun hideLoadingDialog() {
loadingDialog.dismiss()
}
override fun onDestroyView() {
hideLoadingDialog()
super.onDestroyView()
}
}

View File

@@ -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
})
)
)
}
}