diff --git a/app/src/main/java/com/egobook/app/ui/shop/CustomItem.kt b/app/src/main/java/com/egobook/app/ui/shop/CustomItem.kt index d8c3ad28..be3ae80a 100644 --- a/app/src/main/java/com/egobook/app/ui/shop/CustomItem.kt +++ b/app/src/main/java/com/egobook/app/ui/shop/CustomItem.kt @@ -5,9 +5,10 @@ data class CustomItem( val type: ItemType, val price: Price, val itemStatus: ItemStatus, - val image: ItemImage? = null + val image: ItemImage? = null, + val outfitImage: ItemImage? = null ) sealed class ItemImage { data class Url(val path: String): ItemImage() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/egobook/app/ui/shop/ItemAdapter.kt b/app/src/main/java/com/egobook/app/ui/shop/ItemAdapter.kt index f2be3f34..77f3235b 100644 --- a/app/src/main/java/com/egobook/app/ui/shop/ItemAdapter.kt +++ b/app/src/main/java/com/egobook/app/ui/shop/ItemAdapter.kt @@ -15,20 +15,23 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import com.egobook.app.R -class ItemAdapter : - ListAdapter(ItemDiffCallback) { +class ItemAdapter( + private val onItemClick: (CustomItem) -> Unit +) : ListAdapter(ItemDiffCallback) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): ItemViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.view_holder_store_item, parent, false) - return ItemViewHolder(view) } override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { holder.binding(getItem(position)) + holder.root.setOnClickListener { + onItemClick(getItem(position).item) + } } class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { @@ -38,8 +41,8 @@ class ItemAdapter : val itemImage: ImageView = view.findViewById(R.id.iv_item) val root: View = view.rootView - fun binding(item: CustomItem) { - + fun binding(itemState: CustomItemState) { + val item = itemState.item item.image?.let { image -> when(image) { is ItemImage.Url -> itemImage.load(image.path) @@ -56,25 +59,29 @@ class ItemAdapter : itemStatus.text = "구독전용" itemInfoLayout.setPadding(24,6,24,6) itemPriceIcon.visibility = GONE - root.background = null } ItemStatus.PURCHASABLE -> { itemStatus.text = item.price.toString() itemInfoLayout.updatePadding(left=12, right=16) itemPriceIcon.visibility = VISIBLE - root.background = null } } + + if (itemState.isSelected) { + root.setBackgroundResource(R.drawable.store_item_selection_border) + } else { + root.background = null + } } } companion object { - private val ItemDiffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: CustomItem, newItem: CustomItem): Boolean { - return oldItem.id == newItem.id // 고유 ID 비교 + private val ItemDiffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CustomItemState, newItem: CustomItemState): Boolean { + return oldItem.item.id == newItem.item.id // 고유 ID 비교 } - override fun areContentsTheSame(oldItem: CustomItem, newItem: CustomItem): Boolean { + override fun areContentsTheSame(oldItem: CustomItemState, newItem: CustomItemState): Boolean { return oldItem == newItem // 전체 객체 내용 비교 } } diff --git a/app/src/main/java/com/egobook/app/ui/shop/StoreCollectionFragment.kt b/app/src/main/java/com/egobook/app/ui/shop/StoreCollectionFragment.kt index c3bb22e4..25894f46 100644 --- a/app/src/main/java/com/egobook/app/ui/shop/StoreCollectionFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/shop/StoreCollectionFragment.kt @@ -1,12 +1,13 @@ package com.egobook.app.ui.shop import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.os.BundleCompat import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -31,13 +32,15 @@ class StoreCollectionFragment(): Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val viewModel: StoreViewModel by viewModels() + val viewModel: StoreViewModel by activityViewModels() val targetBundle = checkNotNull(arguments) { "구현 오류: 올바른 탭을 표시하기 위해 번들은 필수입니다"} val tabItem = checkNotNull(BundleCompat.getParcelable(targetBundle, ItemTab.BUNDLE_KEY, ItemTab::class.java)) { "구현 오류: 올바른 탭을 표시하기 위해 tabItem을 번들을 통해 넘겨야 합니다." } - val itemAdapter = ItemAdapter() + val itemAdapter = ItemAdapter { customItem -> + viewModel.equipItem(customItem) + } binding.rvItems.apply { adapter = itemAdapter @@ -46,8 +49,8 @@ class StoreCollectionFragment(): Fragment() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.items.collect { newItems -> - itemAdapter.submitList(newItems) + viewModel.itemStates.collect { newItems -> + itemAdapter.submitList(newItems[tabItem.type]) } } } 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 46b0325a..8f2a6ee4 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 @@ -1,18 +1,26 @@ package com.egobook.app.ui.shop import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.viewpager2.widget.ViewPager2 +import coil.load +import com.egobook.app.BlurLevel import com.egobook.app.R +import com.egobook.app.applyScreenBlur import com.egobook.app.databinding.FragmentStoreBinding +import com.egobook.app.ui.home.ui.AdDialog import com.google.android.material.tabs.TabLayoutMediator import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch @AndroidEntryPoint class StoreFragment: Fragment() { @@ -20,6 +28,8 @@ class StoreFragment: Fragment() { private val binding get() = checkNotNull(_binding) { "Fragment가 제거되었습니다." } private lateinit var viewPager: ViewPager2 + private var lastSelected = -1 + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -36,7 +46,7 @@ class StoreFragment: Fragment() { v.setPadding(0, 0, 0, systemBars.bottom) insets } - + val viewModel: StoreViewModel by activityViewModels() viewPager = binding.vp2StoreCollectionContainer viewPager.adapter = StoreCollectionAdapter(this) val tabLayout = binding.tlTabs @@ -44,8 +54,31 @@ class StoreFragment: Fragment() { tab.text = ItemTab.of(position).text }.attach() + viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + if (position == lastSelected) return + lastSelected = position + viewModel.loadEquippedItems() + Log.d("jang", "페이지 변경 감지됨: $position") + } + }) + binding.ivBack.setOnClickListener { - findNavController().navigate(R.id.action_storeFragment_to_homeFragment) + applyScreenBlur(BlurLevel.BASE) + val dialog = StoreLeavingDialog() + dialog.isCancelable = false + dialog.show(parentFragmentManager, "StoreLeavingDialog") + } + + + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.equippedItems.collect { equippedList -> + equippedList.forEach { equippedItem -> + updateEquipItemUi(equippedItem) + } + } } } @@ -54,6 +87,46 @@ class StoreFragment: Fragment() { super.onDestroyView() _binding = null } + + private fun updateEquipItemUi(item: CustomItem) { + binding.ivStoreTurtle.visibility = View.INVISIBLE + when (item.type) { + ItemType.BACK -> { + if (item.outfitImage is ItemImage.Url) { + binding.ivStoreTurtleBack.load(item.outfitImage.path) + } + } + ItemType.SKIN -> { + if (item.outfitImage is ItemImage.Url) { + binding.ivStoreTurtleSkin.load(item.outfitImage.path) + } + } + ItemType.DECO_1 -> { + if (item.outfitImage is ItemImage.Url) { + if(item.outfitImage.path.contains("Default")) { + binding.ivStoreTurtleDeco1.load(null) + return + } + binding.ivStoreTurtleDeco1.load(item.outfitImage.path) + } + } + ItemType.DECO_2 -> { + if (item.outfitImage is ItemImage.Url) { + if(item.outfitImage.path.contains("Default")) { + binding.ivStoreTurtleDeco2.load(null) + return + } + binding.ivStoreTurtleDeco2.load(item.outfitImage.path) + } + } + ItemType.BACKGROUND -> { + if (item.outfitImage is ItemImage.Url) { + binding.ivStoreBackground.load(item.outfitImage.path) + } + } + } + } + } diff --git a/app/src/main/java/com/egobook/app/ui/shop/StoreLeavingDialog.kt b/app/src/main/java/com/egobook/app/ui/shop/StoreLeavingDialog.kt new file mode 100644 index 00000000..fe3f492e --- /dev/null +++ b/app/src/main/java/com/egobook/app/ui/shop/StoreLeavingDialog.kt @@ -0,0 +1,53 @@ +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.removeScreenBlur + +class StoreLeavingDialog: DialogFragment() { + private var _binding: DialogLeavingStoreBinding? = 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 = DialogLeavingStoreBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val viewModel: StoreViewModel by activityViewModels() + binding.btnRemain.setOnClickListener { + removeScreenBlur() + dismiss() + } + + + binding.btnLeave.setOnClickListener { + removeScreenBlur() + dismiss() + viewModel.resetEquipItems() + findNavController().navigate(R.id.action_storeFragment_to_homeFragment) + } + } + + + 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 fbfc1b75..094fcd7a 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 @@ -8,8 +8,18 @@ import retrofit2.http.Query import javax.inject.Inject import javax.inject.Singleton +private fun String.toItemType(): ItemType = when(this) { + "BACK" -> ItemType.BACK + "SKIN" -> ItemType.SKIN + "DECOR_ONE" -> ItemType.DECO_1 + "DECOR_TWO" -> ItemType.DECO_2 + "BACKGROUND" -> ItemType.BACKGROUND + else -> throw IllegalArgumentException("${this}은 알 수 없는 아이템 타입 이름입니다") +} + interface StoreRepository { fun loadStoreItems(itemType: ItemType): Flow + suspend fun loadEquippedItems(): List } data class BaseResponse( @@ -19,21 +29,49 @@ data class BaseResponse( val data: T ) -interface NetworkStoreService { +interface NetworkStoreItemService { @GET("/shop/items") suspend fun loadItemsResponse( @Query("category") category: String, - @Query("slice") slice: Int, + @Query("page") slice: Int, @Query("size") size: Int ): BaseResponse } -data class CustomItemDto( +interface NetworkEquippedItemService { + @GET("/shop/items/equipped") + suspend fun loadEquippedItemsResponse(): BaseResponse> +} + +data class EquippedItemDto( val itemId: Int, + val itemCategory: String, val imageUrl: String, val price: Int, val isPurchased: Boolean, val isEquipped: Boolean +) { + fun toDomain(): CustomItem { + val itemType = itemCategory.toItemType() + return CustomItem( + id = itemId.toString(), + type = itemType, + price = Price(price), + itemStatus = if (isPurchased) ItemStatus.PURCHASED else ItemStatus.PURCHASABLE, + image = null, + outfitImage = ItemImage.Url(imageUrl) + ) + } +} + +data class CustomItemDto( + val itemId: Int, + val itemCategory: String?, + val shopImageUrl: String, + val myImageUrl: String, + val price: Int, + val isPurchased: Boolean, + val isEquipped: Boolean ) private fun ItemType.toDto(): String = @@ -57,11 +95,15 @@ data class CustomItemGroupDto( class NetworkStoreRepository @Inject constructor( private val retrofit: Retrofit ) : StoreRepository { - + private val storeService by lazy { - retrofit.create(NetworkStoreService::class.java) + retrofit.create(NetworkStoreItemService::class.java) } - + + private val equippedItemService by lazy { + retrofit.create(NetworkEquippedItemService::class.java) + } + override fun loadStoreItems(itemType: ItemType): Flow { return flow { var currentPage = 1 @@ -80,7 +122,8 @@ class NetworkStoreRepository @Inject constructor( itemType, Price(dto.price), if (dto.isPurchased) ItemStatus.PURCHASED else ItemStatus.PURCHASABLE, - ItemImage.Url(dto.imageUrl) + ItemImage.Url(dto.shopImageUrl), + ItemImage.Url(dto.myImageUrl) ) ) } @@ -100,5 +143,11 @@ class NetworkStoreRepository @Inject constructor( } } } + + override suspend fun loadEquippedItems(): List { + val equippedItemsResponse: BaseResponse> = + equippedItemService.loadEquippedItemsResponse() + return equippedItemsResponse.data.map { it.toDomain() } + } } 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 33195448..56c7d000 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 @@ -1,27 +1,82 @@ package com.egobook.app.ui.shop +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +data class CustomItemState( + val isSelected: Boolean, + val item: CustomItem +) @HiltViewModel class StoreViewModel @Inject constructor( private val storeRepository: StoreRepository ) : ViewModel() { - private val _items = MutableStateFlow>(emptyList()) - val items: StateFlow> = _items.asStateFlow() + 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 -> + + val equippedIds = equippedList.map { it.id }.toSet() + + itemsMap.mapValues { (_, items) -> + items.map { item -> + CustomItemState( + item = item, + isSelected = equippedIds.contains(item.id) + ) + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyMap() + ) fun loadItems(type: ItemType) { + if (_items.value.containsKey(type)) return viewModelScope.launch { - _items.value = emptyList() storeRepository.loadStoreItems(type) .collect { newItem -> - _items.value += newItem + val loadedList = mutableListOf() + storeRepository.loadStoreItems(type).collect { newItem -> + loadedList.add(newItem) + } + + _items.update { currentMap -> + currentMap + (type to loadedList) + } } } } + + fun loadEquippedItems() { + viewModelScope.launch { + _equippedItems.value = storeRepository.loadEquippedItems() + Log.d("jang", "load: ${_equippedItems.value}") + } + + } + + fun equipItem(item: CustomItem) { + _equippedItems.update { currentList -> + currentList.filter { it.type != item.type } + item + } + } + + fun resetEquipItems() { + _equippedItems.value = emptyList() + } } diff --git a/app/src/main/res/drawable/neutral_radius_border_dialog_button.xml b/app/src/main/res/drawable/neutral_radius_border_dialog_button.xml new file mode 100644 index 00000000..08e676da --- /dev/null +++ b/app/src/main/res/drawable/neutral_radius_border_dialog_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_leaving_store.xml b/app/src/main/res/layout/dialog_leaving_store.xml new file mode 100644 index 00000000..ce6eb355 --- /dev/null +++ b/app/src/main/res/layout/dialog_leaving_store.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_store.xml b/app/src/main/res/layout/fragment_store.xml index 4f453329..9138f3c8 100644 --- a/app/src/main/res/layout/fragment_store.xml +++ b/app/src/main/res/layout/fragment_store.xml @@ -6,6 +6,7 @@ android:background="@color/layer_white"> - + + + + - + + app:layout_constraintTop_toBottomOf="@id/ink_layout" /> + + + + + + + + + #191818 #E8EED9 #F4F7EE + #E14444 + #E69090