diff --git a/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt b/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt index 1de3f1b9..508f3b21 100644 --- a/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/home/HomeViewModel.kt @@ -7,6 +7,8 @@ import com.egobook.app.ui.home.repository.UserRepository import com.egobook.app.ui.home.user.Ink import com.egobook.app.ui.home.user.Level import com.egobook.app.ui.home.user.User +import com.egobook.app.ui.shop.CustomItem +import com.egobook.app.ui.shop.StoreRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -16,16 +18,27 @@ import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val storeRepository: StoreRepository ) : ViewModel() { private val _uiState = MutableStateFlow(User(Level(1), Ink(0))) val uiState: StateFlow = _uiState.asStateFlow() + private val _equippedItems = MutableStateFlow>(emptyList()) + val equippedItems: StateFlow> = _equippedItems + init { fetchUser() + fetchEquipItems() + } + + fun fetchEquipItems() { + viewModelScope.launch { + _equippedItems.value = storeRepository.loadEquippedItems() + } } - private fun fetchUser() { + fun fetchUser() { viewModelScope.launch { try { val user = userRepository.load() @@ -33,7 +46,6 @@ class HomeViewModel @Inject constructor( } catch(error: Exception) { Log.e("HomeViewModel", "Failed to fetch user", error) } - } } } diff --git a/app/src/main/java/com/egobook/app/ui/home/ui/HomeFragment.kt b/app/src/main/java/com/egobook/app/ui/home/ui/HomeFragment.kt index 94981733..990485bc 100644 --- a/app/src/main/java/com/egobook/app/ui/home/ui/HomeFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/home/ui/HomeFragment.kt @@ -10,6 +10,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import coil.load import com.egobook.app.BlurLevel import com.egobook.app.NotificationController import com.egobook.app.R @@ -20,6 +21,9 @@ import com.egobook.app.ui.home.repository.UserTendencyRepository import com.egobook.app.ui.home.user.LevelType import com.egobook.app.ui.home.ui.RadarDialog import com.egobook.app.ui.home.ui.StreakDialog +import com.egobook.app.ui.shop.CustomItem +import com.egobook.app.ui.shop.ItemImage +import com.egobook.app.ui.shop.ItemType import dagger.hilt.android.AndroidEntryPoint import jakarta.inject.Inject import kotlinx.coroutines.launch @@ -47,6 +51,15 @@ class HomeFragment(): Fragment() { } } } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.equippedItems.collect { equippedList -> + equippedList.forEach { equippedItem -> + updateEquipItemUi(equippedItem) + } + } + } + binding.ivStore.setOnClickListener { findNavController().navigate(R.id.action_homeFragment_to_storeFragment) } @@ -83,6 +96,44 @@ class HomeFragment(): Fragment() { } } + private fun updateEquipItemUi(item: CustomItem) { + when (item.type) { + ItemType.BACK -> { + if (item.outfitImage is ItemImage.Url) { + binding.ivHomeTurtleBack.load(item.outfitImage.path) + } + } + ItemType.SKIN -> { + if (item.outfitImage is ItemImage.Url) { + binding.ivHomeTurtleSkin.load(item.outfitImage.path) + } + } + ItemType.DECO_1 -> { + if (item.outfitImage is ItemImage.Url) { + if(item.outfitImage.path.contains("Default")) { + binding.ivHomeTurtleDeco1.load(null) + return + } + binding.ivHomeTurtleDeco1.load(item.outfitImage.path) + } + } + ItemType.DECO_2 -> { + if (item.outfitImage is ItemImage.Url) { + if(item.outfitImage.path.contains("Default")) { + binding.ivHomeTurtleDeco2.load(null) + return + } + binding.ivHomeTurtleDeco2.load(item.outfitImage.path) + } + } + ItemType.BACKGROUND -> { + if (item.outfitImage is ItemImage.Url) { + binding.ivHomeBackground.load(item.outfitImage.path) + } + } + } + } + private fun LevelType.getResId(): Int { return when (this) { LevelType.ONE -> R.drawable.level_type_1 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 bced4414..3b4fdf08 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 @@ -5,12 +5,13 @@ import kotlinx.coroutines.flow.flow import retrofit2.Retrofit import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Query import javax.inject.Inject import javax.inject.Singleton -private fun String.toItemType(): ItemType = when(this) { +private fun String.toItemType(): ItemType = when (this) { "BACK" -> ItemType.BACK "SKIN" -> ItemType.SKIN "DECOR_ONE" -> ItemType.DECO_1 @@ -23,6 +24,7 @@ interface StoreRepository { fun loadStoreItems(itemType: ItemType): Flow suspend fun purchaseItems(item: CustomItem): PurchaseState suspend fun loadEquippedItems(): List + suspend fun permanentEquipItem(item: CustomItem): EquipState } data class BaseResponse( @@ -40,6 +42,15 @@ data class PurchaseState( val isSuccess: Boolean ) +data class PermanentEquipRequest( + val itemId: Int, + val isEquipped: Boolean +) + +data class EquipState( + val isSuccess: Boolean +) + interface NetworkStoreItemService { @GET("/shop/items") suspend fun loadItemsResponse( @@ -59,6 +70,11 @@ interface NetworkEquippedItemService { suspend fun loadEquippedItemsResponse(): BaseResponse> } +interface NetworkPermanentEquipService { + @PATCH("/shop/equip") + suspend fun loadPermanentEquipResponse(@Body request: PermanentEquipRequest): BaseResponse +} + data class EquippedItemDto( val itemId: Int, val itemCategory: String, @@ -124,6 +140,11 @@ class NetworkStoreRepository @Inject constructor( retrofit.create(NetworkPurchaseItemService::class.java) } + private val permanentEquipService by lazy { + retrofit.create(NetworkPermanentEquipService::class.java) + } + + override fun loadStoreItems(itemType: ItemType): Flow { return flow { var currentPage = 1 @@ -166,9 +187,10 @@ class NetworkStoreRepository @Inject constructor( override suspend fun purchaseItems(item: CustomItem): PurchaseState { try { - val response = purchaseItemService.loadPurchaseItemsResponse(PurchaseRequest(item.id.toInt())) + val response = + purchaseItemService.loadPurchaseItemsResponse(PurchaseRequest(item.id.toInt())) return PurchaseState(isSuccess = true) - } catch(err: Exception) { + } catch (err: Exception) { return PurchaseState(isSuccess = false) } } @@ -178,5 +200,16 @@ class NetworkStoreRepository @Inject constructor( equippedItemService.loadEquippedItemsResponse() return equippedItemsResponse.data.map { it.toDomain() } } + + override suspend fun permanentEquipItem(item: CustomItem): EquipState { + try { + permanentEquipService.loadPermanentEquipResponse( + PermanentEquipRequest(itemId = item.id.toInt(), isEquipped = true) + ) + return EquipState(isSuccess = true) + } catch (err: Exception) { + return EquipState(isSuccess = false) + } + } } 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 316b1abe..c7e67fb9 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 @@ -92,6 +92,24 @@ class StoreViewModel @Inject constructor( _equippedItems.update { currentList -> currentList.filter { it.type != item.type } + item } + + if (item.itemStatus != ItemStatus.PURCHASABLE) { + viewModelScope.launch { + try { + val result = storeRepository.permanentEquipItem(item) + if (result.isSuccess) { + Log.d("StoreViewModel", "서버 착용 성공: ${item.id}") + loadEquippedItems() + } else { + _toastEvent.emit("아이템(${item.id}) 착용에 실패했습니다.") + } + } catch (e: Exception) { + Log.e("StoreViewModel", "서버 통신 에러", e) + } + } + } else { + Log.d("StoreViewModel", "미구매 아이템 - 프리뷰 모드") + } } fun loadPurchaseItem(): CustomItem? { @@ -114,6 +132,8 @@ class StoreViewModel @Inject constructor( if (purchaseState.isSuccess) { loadItems(item.type, true) loadInk() + val purchasedItem = item.copy(itemStatus = ItemStatus.PURCHASED) + equipItem(purchasedItem) _toastEvent.emit("구매가 완료되었어요") } else { _toastEvent.emit("잉크가 부족해요") @@ -123,6 +143,6 @@ class StoreViewModel @Inject constructor( fun resetEquipItems() { - _equippedItems.value = emptyList() + loadEquippedItems() } } diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 7e200145..29072679 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -6,6 +6,16 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:background="@drawable/default_background"> + + + + + + + + + +