diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 40866d57..4e76128b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -88,6 +88,7 @@ dependencies { implementation("androidx.hilt:hilt-navigation-fragment:1.2.0") implementation(libs.androidx.annotation) implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) ksp("com.google.dagger:hilt-compiler:2.54") implementation("androidx.hilt:hilt-navigation-compose:1.2.0") implementation("androidx.navigation:navigation-fragment-ktx:2.8.5") @@ -165,4 +166,4 @@ dependencies { ksp { arg("dagger.fastInit", "enabled") arg("dagger.hilt.android.internal.projectType", "APP") -} \ No newline at end of file +} diff --git a/app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt b/app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt index 8f2a6ee4..ef47b353 100644 --- a/app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt @@ -5,11 +5,14 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.viewpager2.widget.ViewPager2 import coil.load @@ -27,6 +30,7 @@ class StoreFragment: Fragment() { private var _binding: FragmentStoreBinding? = null private val binding get() = checkNotNull(_binding) { "Fragment가 제거되었습니다." } private lateinit var viewPager: ViewPager2 + private val viewModel: StoreViewModel by activityViewModels() private var lastSelected = -1 @@ -71,6 +75,19 @@ class StoreFragment: Fragment() { dialog.show(parentFragmentManager, "StoreLeavingDialog") } + binding.tvPurchase.setOnClickListener { + if(viewModel.loadPurchaseItem() != null) { + applyScreenBlur(BlurLevel.BASE) + val dialog = StorePurchasingItemDialog() + dialog.isCancelable = false + dialog.show(parentFragmentManager, "StorePurchasingItemDialog") + } + } + + binding.ivReset.setOnClickListener { + viewModel.loadEquippedItems() + } + viewLifecycleOwner.lifecycleScope.launch { @@ -81,6 +98,8 @@ class StoreFragment: Fragment() { } } + observeViewModel() + } override fun onDestroyView() { @@ -127,6 +146,15 @@ class StoreFragment: Fragment() { } } + private fun observeViewModel() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.toastEvent.collect { message -> + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } + } + } + } } diff --git a/app/src/main/java/com/egobook/app/ui/shop/StorePurchasingItemDialog.kt b/app/src/main/java/com/egobook/app/ui/shop/StorePurchasingItemDialog.kt new file mode 100644 index 00000000..935a6df7 --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/shop/StorePurchasingItemDialog.kt @@ -0,0 +1,60 @@ +package com.egobook.app.ui.shop + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import com.egobook.app.R +import com.egobook.app.databinding.DialogLeavingStoreBinding +import com.egobook.app.databinding.DialogPurchasingItemBinding +import com.egobook.app.removeScreenBlur + +class StorePurchasingItemDialog: DialogFragment() { + private var _binding: DialogPurchasingItemBinding? = null + private val binding get() = checkNotNull(_binding) { "Fragment가 제거되었습니다." } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + _binding = DialogPurchasingItemBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val viewModel: StoreViewModel by activityViewModels() + + val item = viewModel.loadPurchaseItem() + + binding.tvPrice.text = checkNotNull(item).price.value.toString() + + binding.btnBack.setOnClickListener { + removeScreenBlur() + dismiss() + } + + + binding.btnBuy.setOnClickListener { + if (item != null) { + viewModel.purchaseItem(item) + } + removeScreenBlur() + dismiss() + } + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/egobook/app/ui/shop/StoreRepository.kt b/app/src/main/java/com/egobook/app/ui/shop/StoreRepository.kt index 094fcd7a..bced4414 100644 --- a/app/src/main/java/com/egobook/app/ui/shop/StoreRepository.kt +++ b/app/src/main/java/com/egobook/app/ui/shop/StoreRepository.kt @@ -3,7 +3,9 @@ package com.egobook.app.ui.shop import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import retrofit2.Retrofit +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Query import javax.inject.Inject import javax.inject.Singleton @@ -19,6 +21,7 @@ private fun String.toItemType(): ItemType = when(this) { interface StoreRepository { fun loadStoreItems(itemType: ItemType): Flow + suspend fun purchaseItems(item: CustomItem): PurchaseState suspend fun loadEquippedItems(): List } @@ -29,6 +32,14 @@ data class BaseResponse( val data: T ) +data class PurchaseRequest( + val itemId: Int +) + +data class PurchaseState( + val isSuccess: Boolean +) + interface NetworkStoreItemService { @GET("/shop/items") suspend fun loadItemsResponse( @@ -38,6 +49,11 @@ interface NetworkStoreItemService { ): BaseResponse } +interface NetworkPurchaseItemService { + @POST("/shop/purchase") + suspend fun loadPurchaseItemsResponse(@Body request: PurchaseRequest): BaseResponse +} + interface NetworkEquippedItemService { @GET("/shop/items/equipped") suspend fun loadEquippedItemsResponse(): BaseResponse> @@ -104,6 +120,10 @@ class NetworkStoreRepository @Inject constructor( retrofit.create(NetworkEquippedItemService::class.java) } + private val purchaseItemService by lazy { + retrofit.create(NetworkPurchaseItemService::class.java) + } + override fun loadStoreItems(itemType: ItemType): Flow { return flow { var currentPage = 1 @@ -144,6 +164,15 @@ class NetworkStoreRepository @Inject constructor( } } + override suspend fun purchaseItems(item: CustomItem): PurchaseState { + try { + val response = purchaseItemService.loadPurchaseItemsResponse(PurchaseRequest(item.id.toInt())) + return PurchaseState(isSuccess = true) + } catch(err: Exception) { + return PurchaseState(isSuccess = false) + } + } + override suspend fun loadEquippedItems(): List { val equippedItemsResponse: BaseResponse> = equippedItemService.loadEquippedItemsResponse() diff --git a/app/src/main/java/com/egobook/app/ui/shop/StoreViewModel.kt b/app/src/main/java/com/egobook/app/ui/shop/StoreViewModel.kt index 56c7d000..feedbbdf 100644 --- a/app/src/main/java/com/egobook/app/ui/shop/StoreViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/shop/StoreViewModel.kt @@ -4,9 +4,11 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn @@ -22,11 +24,12 @@ data class CustomItemState( class StoreViewModel @Inject constructor( private val storeRepository: StoreRepository ) : ViewModel() { + + private val _toastEvent = MutableSharedFlow() + val toastEvent = _toastEvent.asSharedFlow() private val _equippedItems = MutableStateFlow>(emptyList()) val equippedItems: StateFlow> = _equippedItems private val _items = MutableStateFlow>>(mutableMapOf()) - val items: StateFlow>> = _items.asStateFlow() - val itemStates: StateFlow>> = combine(_items, _equippedItems) { itemsMap, equippedList -> @@ -45,20 +48,21 @@ class StoreViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5000), initialValue = emptyMap() ) - fun loadItems(type: ItemType) { - if (_items.value.containsKey(type)) return - viewModelScope.launch { - storeRepository.loadStoreItems(type) - .collect { newItem -> - val loadedList = mutableListOf() - storeRepository.loadStoreItems(type).collect { newItem -> - loadedList.add(newItem) - } + fun loadItems(type: ItemType, forceRefresh: Boolean = false) { + if (!forceRefresh && _items.value.containsKey(type)) return - _items.update { currentMap -> - currentMap + (type to loadedList) - } + viewModelScope.launch { + try { + val loadedList = mutableListOf() + storeRepository.loadStoreItems(type).collect { newItem -> + loadedList.add(newItem) + } + _items.update { currentMap -> + currentMap + (type to loadedList) } + } catch (err: Exception) { + Log.e("jang", "$err") + } } } @@ -67,7 +71,6 @@ class StoreViewModel @Inject constructor( _equippedItems.value = storeRepository.loadEquippedItems() Log.d("jang", "load: ${_equippedItems.value}") } - } fun equipItem(item: CustomItem) { @@ -76,6 +79,33 @@ class StoreViewModel @Inject constructor( } } + fun loadPurchaseItem(): CustomItem? { + val purchasableItems = _equippedItems.value.filter { it.itemStatus == ItemStatus.PURCHASABLE } + if (purchasableItems.isEmpty()){ + viewModelScope.launch { + _toastEvent.emit("구매할 수 없는 상품입니다") + } + return null + } + return purchasableItems.first() + } + + fun purchaseItem(item: CustomItem) { + val purchasableItems = _equippedItems.value.filter { it.itemStatus == ItemStatus.PURCHASABLE } + if (purchasableItems.isEmpty()) { return } + val item = purchasableItems.first() + viewModelScope.launch { + val purchaseState = storeRepository.purchaseItems(item) + if (purchaseState.isSuccess) { + loadItems(item.type, true) + _toastEvent.emit("구매가 완료되었어요") + } else { + _toastEvent.emit("잉크가 부족해요") + } + } + } + + fun resetEquipItems() { _equippedItems.value = emptyList() } diff --git a/app/src/main/res/layout/dialog_purchasing_item.xml b/app/src/main/res/layout/dialog_purchasing_item.xml new file mode 100644 index 00000000..82adfdf6 --- /dev/null +++ b/app/src/main/res/layout/dialog_purchasing_item.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f03cb41e..06467dfe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ constraintlayout = "2.2.1" annotation = "1.6.0" lifecycleLivedataKtx = "2.10.0" lifecycleViewmodelKtx = "2.10.0" +lifecycleRuntimeKtx = "2.10.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -25,6 +26,7 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycleLivedataKtx" } androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } +androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }