From 1e8230d8a7998e05b37e9eb4f10bc08cb9184b74 Mon Sep 17 00:00:00 2001 From: aksworns22 Date: Tue, 10 Feb 2026 20:36:24 +0900 Subject: [PATCH 1/5] =?UTF-8?q?chore:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 3 ++- gradle/libs.versions.toml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) 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/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" } From 3c6d6cfa53ca7c1498635fbf06e5918ebf3e51ea Mon Sep 17 00:00:00 2001 From: aksworns22 Date: Tue, 10 Feb 2026 20:36:37 +0900 Subject: [PATCH 2/5] =?UTF-8?q?design:=20=EA=B5=AC=EB=A7=A4=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EB=A1=9C=EA=B7=B8=20ui=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../res/layout/dialog_purchasing_item.xml | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 app/src/main/res/layout/dialog_purchasing_item.xml 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 99f0b2d8b826306a77b233a910b76ba2c15b1e61 Mon Sep 17 00:00:00 2001 From: aksworns22 Date: Tue, 10 Feb 2026 20:37:00 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EA=B5=AC=EB=A7=A4=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20=EC=84=9C=EB=B2=84=EC=99=80=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../egobook/app/ui/shop/StoreRepository.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) 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() From 4b206bb27872167c7ea8fc3ea3be280c06d4e279 Mon Sep 17 00:00:00 2001 From: aksworns22 Date: Tue, 10 Feb 2026 20:37:08 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EA=B5=AC=EB=A7=A4=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20ui=EB=A5=BC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/egobook/app/ui/shop/StoreFragment.kt | 24 ++++++++ .../app/ui/shop/StorePurchasingItemDialog.kt | 60 +++++++++++++++++++ .../com/egobook/app/ui/shop/StoreViewModel.kt | 60 ++++++++++++++----- 3 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/egobook/app/ui/shop/StorePurchasingItemDialog.kt 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..4d036d1c 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,15 @@ 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") + } + } + viewLifecycleOwner.lifecycleScope.launch { @@ -81,6 +94,8 @@ class StoreFragment: Fragment() { } } + observeViewModel() + } override fun onDestroyView() { @@ -127,6 +142,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/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() } From cb50b64589829c6fc2fa66c3e0b50328fb1062d4 Mon Sep 17 00:00:00 2001 From: aksworns22 Date: Tue, 10 Feb 2026 20:38:28 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EB=A6=AC=EC=85=8B=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/egobook/app/ui/shop/StoreFragment.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 4d036d1c..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 @@ -84,6 +84,10 @@ class StoreFragment: Fragment() { } } + binding.ivReset.setOnClickListener { + viewModel.loadEquippedItems() + } + viewLifecycleOwner.lifecycleScope.launch {