diff --git a/jongyee/Week5/.claude/settings.local.json b/jongyee/Week5/.claude/settings.local.json new file mode 100644 index 0000000..428eb42 --- /dev/null +++ b/jongyee/Week5/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(grep *)" + ] + } +} diff --git a/jongyee/Week5/.gitignore b/jongyee/Week5/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/jongyee/Week5/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/jongyee/Week5/app/.gitignore b/jongyee/Week5/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/jongyee/Week5/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/jongyee/Week5/app/build.gradle.kts b/jongyee/Week5/app/build.gradle.kts new file mode 100644 index 0000000..847fab7 --- /dev/null +++ b/jongyee/Week5/app/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.example.week5" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.week5" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.activity) + implementation(libs.androidx.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + implementation(libs.androidx.datastore.preferences) + implementation(libs.gson) + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.livedata.ktx) + + // Retrofit + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) + implementation(libs.okhttp.logging.interceptor) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + // Glide + implementation(libs.glide) +} diff --git a/jongyee/Week5/app/proguard-rules.pro b/jongyee/Week5/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/jongyee/Week5/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/jongyee/Week5/app/src/androidTest/java/com/example/week5/ExampleInstrumentedTest.kt b/jongyee/Week5/app/src/androidTest/java/com/example/week5/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..9873c9f --- /dev/null +++ b/jongyee/Week5/app/src/androidTest/java/com/example/week5/ExampleInstrumentedTest.kt @@ -0,0 +1,16 @@ +package com.example.week5 + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.Assert.* + +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.week5", appContext.packageName) + } +} diff --git a/jongyee/Week5/app/src/main/AndroidManifest.xml b/jongyee/Week5/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2aa574c --- /dev/null +++ b/jongyee/Week5/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/ApiClient.kt b/jongyee/Week5/app/src/main/java/com/example/week5/ApiClient.kt new file mode 100644 index 0000000..16d56c2 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/ApiClient.kt @@ -0,0 +1,41 @@ +package com.example.week5 + +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + +object ApiClient { + + private const val BASE_URL = "https://reqres.in/api/" + private const val API_KEY = "reqres_701d6373134941c7ba1e30b84be78104" + + private val logging = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + private val authInterceptor = Interceptor { chain -> + val request = chain.request().newBuilder() + .addHeader("x-api-key", API_KEY) + .build() + chain.proceed(request) + } + + private val client = OkHttpClient.Builder() + .addInterceptor(authInterceptor) + .addInterceptor(logging) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + + private val retrofit: Retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val reqResService: ReqResService = retrofit.create(ReqResService::class.java) +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/BagFragment.kt b/jongyee/Week5/app/src/main/java/com/example/week5/BagFragment.kt new file mode 100644 index 0000000..4a7210c --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/BagFragment.kt @@ -0,0 +1,36 @@ +package com.example.week5 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import com.example.week5.databinding.FragmentBagBinding + +class BagFragment : Fragment() { + private var _binding: FragmentBagBinding? = null + private val binding get() = _binding!! + + private val sharedViewModel: SharedViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentBagBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.bagBtnOrder.setOnClickListener { + sharedViewModel.requestNavigateToBuyTab() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/BuyFragment.kt b/jongyee/Week5/app/src/main/java/com/example/week5/BuyFragment.kt new file mode 100644 index 0000000..d51e806 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/BuyFragment.kt @@ -0,0 +1,81 @@ +package com.example.week5 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import com.example.week5.databinding.FragmentBuyBinding +import com.google.android.material.tabs.TabLayout +import kotlinx.coroutines.launch + +class TopsFragment : Fragment() +class SaleFragment : Fragment() + +class BuyFragment : Fragment() { + private var _binding: FragmentBuyBinding? = null + private val binding get() = _binding!! + + private val sharedViewModel: SharedViewModel by activityViewModels() + + private val productAdapter by lazy { + ProductAdapter( + productList = emptyList(), + isGrid = true, + onHeartClick = { product -> sharedViewModel.toggleFavorite(product) }, + onItemClick = { product -> sharedViewModel.requestNavigateToDetail(product) } + ) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentBuyBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.buyRvProducts.adapter = productAdapter + binding.buyRvProducts.layoutManager = GridLayoutManager(context, 2) + + viewLifecycleOwner.lifecycleScope.launch { + sharedViewModel.buyProducts.collect { products -> + productAdapter.updateData(products) + } + } + + binding.buyTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab?) { + when (tab?.position) { + 0 -> showRecyclerView() + 1 -> showChildFragment(TopsFragment()) + 2 -> showChildFragment(SaleFragment()) + } + } + override fun onTabUnselected(tab: TabLayout.Tab?) {} + override fun onTabReselected(tab: TabLayout.Tab?) {} + }) + } + + private fun showRecyclerView() { + binding.buyRvProducts.visibility = View.VISIBLE + binding.buyChildContainer.visibility = View.GONE + } + + private fun showChildFragment(fragment: Fragment) { + binding.buyRvProducts.visibility = View.GONE + binding.buyChildContainer.visibility = View.VISIBLE + childFragmentManager.beginTransaction().replace(R.id.buy_child_container, fragment).commit() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/DetailFragment.kt b/jongyee/Week5/app/src/main/java/com/example/week5/DetailFragment.kt new file mode 100644 index 0000000..8300bb4 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/DetailFragment.kt @@ -0,0 +1,60 @@ +package com.example.week5 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.example.week5.databinding.FragmentDetailBinding +import kotlinx.coroutines.launch + +class DetailFragment : Fragment() { + private var _binding: FragmentDetailBinding? = null + private val binding get() = _binding!! + + private val sharedViewModel: SharedViewModel by activityViewModels() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentDetailBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewLifecycleOwner.lifecycleScope.launch { + sharedViewModel.selectedProduct.collect { product -> + product?.let { + binding.tvDetailCategory.text = it.category + binding.tvDetailName.text = it.name + binding.tvDetailPrice.text = it.price + binding.tvDetailDescription.text = it.description + binding.imgDetailProduct.setImageResource(it.imageRes) + + val heartIcon = if (it.isFavorite) R.drawable.ic_heart_filled else R.drawable.ic_heart_empty + binding.btnWishlist.setIconResource(heartIcon) + } + } + } + + binding.btnWishlist.setOnClickListener { + sharedViewModel.selectedProduct.value?.let { currentProduct -> + sharedViewModel.toggleFavorite(currentProduct) + } + } + + binding.icBack.setOnClickListener { + parentFragmentManager.popBackStack() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/FollowingAdapter.kt b/jongyee/Week5/app/src/main/java/com/example/week5/FollowingAdapter.kt new file mode 100644 index 0000000..ae1c462 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/FollowingAdapter.kt @@ -0,0 +1,34 @@ +package com.example.week5 + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.example.week5.databinding.ItemFollowingBinding + +class FollowingAdapter : ListAdapter(DiffCallback) { + + inner class ViewHolder(val binding: ItemFollowingBinding) : RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemFollowingBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val user = getItem(position) + holder.binding.tvFollowingName.text = user.firstName + Glide.with(holder.itemView.context) + .load(user.avatar) + .circleCrop() + .placeholder(R.drawable.ic_launcher_background) + .into(holder.binding.ivFollowingAvatar) + } + + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: UserDto, newItem: UserDto) = oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: UserDto, newItem: UserDto) = oldItem == newItem + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/HomeFragment.kt b/jongyee/Week5/app/src/main/java/com/example/week5/HomeFragment.kt new file mode 100644 index 0000000..5b0b715 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/HomeFragment.kt @@ -0,0 +1,52 @@ +package com.example.week5 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.week5.databinding.FragmentHomeBinding +import kotlinx.coroutines.launch + +class HomeFragment : Fragment() { + private var _binding: FragmentHomeBinding? = null + private val binding get() = _binding!! + + private val sharedViewModel: SharedViewModel by activityViewModels() + + private val productAdapter by lazy { + ProductAdapter( + productList = emptyList(), + isGrid = false, + onItemClick = { product -> sharedViewModel.requestNavigateToDetail(product) } + ) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentHomeBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.homeRvProducts.apply { + adapter = productAdapter + layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + } + + viewLifecycleOwner.lifecycleScope.launch { + sharedViewModel.homeProducts.collect { products -> + productAdapter.updateData(products) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/MainActivity.kt b/jongyee/Week5/app/src/main/java/com/example/week5/MainActivity.kt new file mode 100644 index 0000000..a1d4e7e --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/MainActivity.kt @@ -0,0 +1,52 @@ +package com.example.week5 + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import com.example.week5.databinding.ActivityMainBinding +import kotlinx.coroutines.launch + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + private val sharedViewModel: SharedViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + if (savedInstanceState == null) changeFragment(HomeFragment()) + + binding.mainBottomNav.setOnItemSelectedListener { item -> + when (item.itemId) { + R.id.nav_home -> changeFragment(HomeFragment()) + R.id.nav_buy -> changeFragment(BuyFragment()) + R.id.nav_wishlist -> changeFragment(WishlistFragment()) + R.id.nav_bag -> changeFragment(BagFragment()) + R.id.nav_profile -> changeFragment(ProfileFragment()) + } + true + } + + lifecycleScope.launch { + sharedViewModel.navigateToDetail.collect { + supportFragmentManager.beginTransaction() + .replace(R.id.main_container, DetailFragment()) + .addToBackStack(null) + .commit() + } + } + + lifecycleScope.launch { + sharedViewModel.navigateToBuyTab.collect { + binding.mainBottomNav.selectedItemId = R.id.nav_buy + } + } + } + + private fun changeFragment(fragment: Fragment) { + supportFragmentManager.beginTransaction().replace(R.id.main_container, fragment).commit() + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/Product.kt b/jongyee/Week5/app/src/main/java/com/example/week5/Product.kt new file mode 100644 index 0000000..72b6af3 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/Product.kt @@ -0,0 +1,13 @@ +package com.example.week5 + +data class Product( + val id: Int, + val name: String, + val price: String, + val description: String, + val imageRes: Int, + val subTitle: String = "", + val category: String = "", + val colorsCount: String = "", + var isFavorite: Boolean = false +) diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/ProductAdapter.kt b/jongyee/Week5/app/src/main/java/com/example/week5/ProductAdapter.kt new file mode 100644 index 0000000..491bda5 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/ProductAdapter.kt @@ -0,0 +1,78 @@ +package com.example.week5 + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.example.week5.databinding.ItemGridProductBinding +import com.example.week5.databinding.ItemHomeProductBinding + +class ProductAdapter( + private var productList: List = emptyList(), + private val isGrid: Boolean = false, + private val onHeartClick: ((Product) -> Unit)? = null, + private val onItemClick: (Product) -> Unit +) : RecyclerView.Adapter() { + + fun updateData(newList: List) { + productList = newList + notifyDataSetChanged() + } + + inner class HomeViewHolder(private val binding: ItemHomeProductBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(product: Product) { + binding.ivProduct.setImageResource(product.imageRes) + binding.tvName.text = product.name + binding.tvPrice.text = product.price + binding.root.setOnClickListener { onItemClick(product) } + } + } + + inner class GridViewHolder(private val binding: ItemGridProductBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(product: Product) { + binding.ivProductGrid.setImageResource(product.imageRes) + binding.tvNameGrid.text = product.name + binding.tvPriceGrid.text = product.price + binding.tvCategoryGrid.text = product.category + binding.tvColorsGrid.text = product.colorsCount + + if (product.subTitle.isEmpty()) { + binding.tvSubtitleGrid.visibility = View.GONE + } else { + binding.tvSubtitleGrid.visibility = View.VISIBLE + binding.tvSubtitleGrid.text = product.subTitle + } + + updateHeartIcon(product.isFavorite) + binding.ivHeartGrid.setOnClickListener { + onHeartClick?.invoke(product) + } + + binding.root.setOnClickListener { onItemClick(product) } + } + + private fun updateHeartIcon(isFavorite: Boolean) { + val iconRes = if (isFavorite) R.drawable.ic_heart_filled else R.drawable.ic_heart_empty + binding.ivHeartGrid.setImageResource(iconRes) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val layoutInflater = LayoutInflater.from(parent.context) + return if (isGrid) { + GridViewHolder(ItemGridProductBinding.inflate(layoutInflater, parent, false)) + } else { + HomeViewHolder(ItemHomeProductBinding.inflate(layoutInflater, parent, false)) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val product = productList[position] + if (holder is HomeViewHolder) holder.bind(product) + else if (holder is GridViewHolder) holder.bind(product) + } + + override fun getItemCount(): Int = productList.size +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/ProfileEditActivity.kt b/jongyee/Week5/app/src/main/java/com/example/week5/ProfileEditActivity.kt new file mode 100644 index 0000000..38e4420 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/ProfileEditActivity.kt @@ -0,0 +1,15 @@ +package com.example.week5 + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.example.week5.databinding.ActivityProfileEditBinding + +class ProfileEditActivity : AppCompatActivity() { + private lateinit var binding: ActivityProfileEditBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityProfileEditBinding.inflate(layoutInflater) + setContentView(binding.root) + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/ProfileFragment.kt b/jongyee/Week5/app/src/main/java/com/example/week5/ProfileFragment.kt new file mode 100644 index 0000000..e5ab63b --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/ProfileFragment.kt @@ -0,0 +1,73 @@ +package com.example.week5 + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +import com.example.week5.databinding.FragmentProfileBinding + +class ProfileFragment : Fragment() { + private var _binding: FragmentProfileBinding? = null + private val binding get() = _binding!! + + private val profileViewModel: ProfileViewModel by viewModels() + private lateinit var followingAdapter: FollowingAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentProfileBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + followingAdapter = FollowingAdapter() + binding.rvFollowing.apply { + adapter = followingAdapter + layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + } + + profileViewModel.userProfile.observe(viewLifecycleOwner) { result -> + result.fold( + onSuccess = { user -> + binding.tvProfileName.text = "${user.firstName} ${user.lastName}" + Glide.with(this) + .load(user.avatar) + .circleCrop() + .placeholder(R.drawable.ic_launcher_background) + .into(binding.ivProfileAvatar) + }, + onFailure = { e -> Log.e("ProfileFragment", "유저 로드 실패", e) } + ) + } + + profileViewModel.followingList.observe(viewLifecycleOwner) { result -> + result.fold( + onSuccess = { users -> + followingAdapter.submitList(users) + binding.tvFollowingTitle.text = "팔로잉 (${users.size})" + }, + onFailure = { e -> Log.e("ProfileFragment", "팔로잉 로드 실패", e) } + ) + } + + binding.btnEditProfile.setOnClickListener { + val intent = Intent(requireActivity(), ProfileEditActivity::class.java) + startActivity(intent) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/ProfileRepository.kt b/jongyee/Week5/app/src/main/java/com/example/week5/ProfileRepository.kt new file mode 100644 index 0000000..960a7b2 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/ProfileRepository.kt @@ -0,0 +1,18 @@ +package com.example.week5 + +class ProfileRepository(private val service: ReqResService) { + + suspend fun getUser(id: Int): Result = try { + val response = service.getUser(id) + Result.success(response.data) + } catch (e: Exception) { + Result.failure(e) + } + + suspend fun getUsers(page: Int): Result> = try { + val response = service.getUsers(page) + Result.success(response.data) + } catch (e: Exception) { + Result.failure(e) + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/ProfileViewModel.kt b/jongyee/Week5/app/src/main/java/com/example/week5/ProfileViewModel.kt new file mode 100644 index 0000000..019ceb5 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/ProfileViewModel.kt @@ -0,0 +1,37 @@ +package com.example.week5 + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch + +class ProfileViewModel : ViewModel() { + + private val repository = ProfileRepository(ApiClient.reqResService) + + private val _userProfile = MutableLiveData>() + val userProfile: LiveData> = _userProfile + + private val _followingList = MutableLiveData>>() + val followingList: LiveData>> = _followingList + + init { + loadUserProfile() + loadFollowingList() + } + + private fun loadUserProfile() { + viewModelScope.launch { + val result = repository.getUser(1) + _userProfile.postValue(result) + } + } + + private fun loadFollowingList() { + viewModelScope.launch { + val result = repository.getUsers(1) + _followingList.postValue(result) + } + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/ReqResService.kt b/jongyee/Week5/app/src/main/java/com/example/week5/ReqResService.kt new file mode 100644 index 0000000..ab3a494 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/ReqResService.kt @@ -0,0 +1,14 @@ +package com.example.week5 + +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface ReqResService { + + @GET("users/{id}") + suspend fun getUser(@Path("id") id: Int): SingleUserResponse + + @GET("users") + suspend fun getUsers(@Query("page") page: Int): UserListResponse +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/SharedViewModel.kt b/jongyee/Week5/app/src/main/java/com/example/week5/SharedViewModel.kt new file mode 100644 index 0000000..3ae54ff --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/SharedViewModel.kt @@ -0,0 +1,119 @@ +package com.example.week5 + +import android.app.Application +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +private val Application.dataStore by preferencesDataStore(name = "nike_store") + +class SharedViewModel(application: Application) : AndroidViewModel(application) { + private val dataStore = application.dataStore + private val gson = Gson() + + private val HOME_KEY = stringPreferencesKey("home_products") + private val BUY_KEY = stringPreferencesKey("buy_products") + + private val _homeProducts = MutableStateFlow>(emptyList()) + val homeProducts: StateFlow> get() = _homeProducts + + private val _buyProducts = MutableStateFlow>(emptyList()) + val buyProducts: StateFlow> get() = _buyProducts + + private val _selectedProduct = MutableStateFlow(null) + val selectedProduct: StateFlow get() = _selectedProduct + + private val _navigateToDetail = MutableSharedFlow() + val navigateToDetail = _navigateToDetail.asSharedFlow() + + private val _navigateToBuyTab = MutableSharedFlow() + val navigateToBuyTab = _navigateToBuyTab.asSharedFlow() + + init { + loadProducts() + } + + private fun loadProducts() { + viewModelScope.launch { + val prefs = dataStore.data.first() + val type = object : TypeToken>() {}.type + + val homeJson = prefs[HOME_KEY] + if (homeJson != null) { + _homeProducts.value = gson.fromJson(homeJson, type) + } else { + val dummyHome = listOf( + Product(1, "Air Jordan XXXVI", "US$185", "Latest performance basketball shoe.", R.drawable.jordan), + Product(2, "Nike Dunk Low", "US$110", "A hoop icon returns with classic colors.", R.drawable.airforce) + ) + saveToDataStore(HOME_KEY, dummyHome) + _homeProducts.value = dummyHome + } + + val buyJson = prefs[BUY_KEY] + if (buyJson != null) { + _buyProducts.value = gson.fromJson(buyJson, type) + } else { + val dummyBuy = listOf( + Product(3, "Nike Everyday Plus Cushioned", "US$10", "Training Ankle Socks", R.drawable.trainingsocks, category = "Training Ankle Socks (6 Pairs)", colorsCount = "5 Colours"), + Product(4, "Nike Elite Crew", "US$16", "Performance Socks", R.drawable.jordan, category = "Basketball Socks", colorsCount = "7 Colours"), + Product(5, "Nike Air Force 1 '07", "US$115", "The legend lives on", R.drawable.airforce, subTitle = "BestSeller", category = "Women's Shoes", colorsCount = "5 Colours"), + Product(6, "Jordan Series .05", "US$115", "Men's Shoes", R.drawable.crewsocks, subTitle = "BestSeller", category = "Men's Shoes", colorsCount = "2 Colours") + ) + saveToDataStore(BUY_KEY, dummyBuy) + _buyProducts.value = dummyBuy + } + } + } + + fun toggleFavorite(product: Product) { + viewModelScope.launch { + val newFavStatus = !product.isFavorite + + val updatedBuyList = _buyProducts.value.map { + if (it.id == product.id) it.copy(isFavorite = newFavStatus) else it + } + _buyProducts.value = updatedBuyList + saveToDataStore(BUY_KEY, updatedBuyList) + + val updatedHomeList = _homeProducts.value.map { + if (it.id == product.id) it.copy(isFavorite = newFavStatus) else it + } + _homeProducts.value = updatedHomeList + saveToDataStore(HOME_KEY, updatedHomeList) + + if (_selectedProduct.value?.id == product.id) { + _selectedProduct.value = _selectedProduct.value?.copy(isFavorite = newFavStatus) + } + } + } + + private suspend fun saveToDataStore(key: androidx.datastore.preferences.core.Preferences.Key, data: List) { + dataStore.edit { prefs -> + prefs[key] = gson.toJson(data) + } + } + + fun requestNavigateToDetail(product: Product) { + _selectedProduct.value = product + viewModelScope.launch { + _navigateToDetail.emit(Unit) + } + } + + fun requestNavigateToBuyTab() { + viewModelScope.launch { + _navigateToBuyTab.emit(Unit) + } + } +} diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/UserDto.kt b/jongyee/Week5/app/src/main/java/com/example/week5/UserDto.kt new file mode 100644 index 0000000..598329d --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/UserDto.kt @@ -0,0 +1,23 @@ +package com.example.week5 + +import com.google.gson.annotations.SerializedName + +data class UserDto( + val id: Int, + val email: String, + @SerializedName("first_name") val firstName: String, + @SerializedName("last_name") val lastName: String, + val avatar: String +) + +data class SingleUserResponse( + val data: UserDto +) + +data class UserListResponse( + val page: Int, + @SerializedName("per_page") val perPage: Int, + val total: Int, + @SerializedName("total_pages") val totalPages: Int, + val data: List +) diff --git a/jongyee/Week5/app/src/main/java/com/example/week5/WishlistFragment.kt b/jongyee/Week5/app/src/main/java/com/example/week5/WishlistFragment.kt new file mode 100644 index 0000000..51ef090 --- /dev/null +++ b/jongyee/Week5/app/src/main/java/com/example/week5/WishlistFragment.kt @@ -0,0 +1,54 @@ +package com.example.week5 + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.GridLayoutManager +import com.example.week5.databinding.FragmentWishlistBinding +import kotlinx.coroutines.launch + +class WishlistFragment : Fragment() { + private var _binding: FragmentWishlistBinding? = null + private val binding get() = _binding!! + + private val sharedViewModel: SharedViewModel by activityViewModels() + + private val productAdapter by lazy { + ProductAdapter(emptyList(), isGrid = true, + onItemClick = { product -> sharedViewModel.requestNavigateToDetail(product) }, + onHeartClick = { product -> sharedViewModel.toggleFavorite(product) } + ) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentWishlistBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.wishlistRvProducts.adapter = productAdapter + binding.wishlistRvProducts.layoutManager = GridLayoutManager(context, 2) + + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + sharedViewModel.buyProducts.collect { products -> + val wishItems = products.filter { it.isFavorite } + productAdapter.updateData(wishItems) + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/jongyee/Week5/app/src/main/res/drawable/airforce.png b/jongyee/Week5/app/src/main/res/drawable/airforce.png new file mode 100644 index 0000000..99fcf6c Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/airforce.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/bag.xml b/jongyee/Week5/app/src/main/res/drawable/bag.xml new file mode 100644 index 0000000..9e97beb --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/bag.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/jongyee/Week5/app/src/main/res/drawable/bagcircle.xml b/jongyee/Week5/app/src/main/res/drawable/bagcircle.xml new file mode 100644 index 0000000..1af2ce6 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/bagcircle.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/jongyee/Week5/app/src/main/res/drawable/buy.xml b/jongyee/Week5/app/src/main/res/drawable/buy.xml new file mode 100644 index 0000000..483d117 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/buy.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/jongyee/Week5/app/src/main/res/drawable/crewsocks.png b/jongyee/Week5/app/src/main/res/drawable/crewsocks.png new file mode 100644 index 0000000..d7c6ef1 Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/crewsocks.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/event.png b/jongyee/Week5/app/src/main/res/drawable/event.png new file mode 100644 index 0000000..704512e Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/event.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/home.xml b/jongyee/Week5/app/src/main/res/drawable/home.xml new file mode 100644 index 0000000..a0d86b9 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/home.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/jongyee/Week5/app/src/main/res/drawable/ic_arrow_right.xml b/jongyee/Week5/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..19201cb --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/jongyee/Week5/app/src/main/res/drawable/ic_heart_empty.xml b/jongyee/Week5/app/src/main/res/drawable/ic_heart_empty.xml new file mode 100644 index 0000000..59b3907 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/ic_heart_empty.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/jongyee/Week5/app/src/main/res/drawable/ic_heart_filled.xml b/jongyee/Week5/app/src/main/res/drawable/ic_heart_filled.xml new file mode 100644 index 0000000..85961df --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/ic_heart_filled.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/jongyee/Week5/app/src/main/res/drawable/ic_launcher_background.xml b/jongyee/Week5/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jongyee/Week5/app/src/main/res/drawable/ic_launcher_foreground.xml b/jongyee/Week5/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/jongyee/Week5/app/src/main/res/drawable/image.png b/jongyee/Week5/app/src/main/res/drawable/image.png new file mode 100644 index 0000000..f42aa9d Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/image.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/jordan.png b/jongyee/Week5/app/src/main/res/drawable/jordan.png new file mode 100644 index 0000000..1bfc3f2 Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/jordan.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/order.png b/jongyee/Week5/app/src/main/res/drawable/order.png new file mode 100644 index 0000000..61200c1 Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/order.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/pass.png b/jongyee/Week5/app/src/main/res/drawable/pass.png new file mode 100644 index 0000000..af82b3b Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/pass.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/profile.xml b/jongyee/Week5/app/src/main/res/drawable/profile.xml new file mode 100644 index 0000000..1fc97b8 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/profile.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/jongyee/Week5/app/src/main/res/drawable/setting.png b/jongyee/Week5/app/src/main/res/drawable/setting.png new file mode 100644 index 0000000..ed55245 Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/setting.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/shape_circle_gray.xml b/jongyee/Week5/app/src/main/res/drawable/shape_circle_gray.xml new file mode 100644 index 0000000..1f18f34 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/shape_circle_gray.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/jongyee/Week5/app/src/main/res/drawable/trainingsocks.png b/jongyee/Week5/app/src/main/res/drawable/trainingsocks.png new file mode 100644 index 0000000..fc41659 Binary files /dev/null and b/jongyee/Week5/app/src/main/res/drawable/trainingsocks.png differ diff --git a/jongyee/Week5/app/src/main/res/drawable/wishlist.xml b/jongyee/Week5/app/src/main/res/drawable/wishlist.xml new file mode 100644 index 0000000..6597a4a --- /dev/null +++ b/jongyee/Week5/app/src/main/res/drawable/wishlist.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/jongyee/Week5/app/src/main/res/layout/activity_main.xml b/jongyee/Week5/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..51565b5 --- /dev/null +++ b/jongyee/Week5/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/jongyee/Week5/app/src/main/res/layout/activity_profile_edit.xml b/jongyee/Week5/app/src/main/res/layout/activity_profile_edit.xml new file mode 100644 index 0000000..77f947b --- /dev/null +++ b/jongyee/Week5/app/src/main/res/layout/activity_profile_edit.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/jongyee/Week5/app/src/main/res/layout/fragment_bag.xml b/jongyee/Week5/app/src/main/res/layout/fragment_bag.xml new file mode 100644 index 0000000..1a5ff2c --- /dev/null +++ b/jongyee/Week5/app/src/main/res/layout/fragment_bag.xml @@ -0,0 +1,59 @@ + + + + + + + + + + +