diff --git a/Week08/OneStone/.gitignore b/Week08/OneStone/.gitignore
new file mode 100644
index 0000000..54e91e0
--- /dev/null
+++ b/Week08/OneStone/.gitignore
@@ -0,0 +1,10 @@
+.idea/
+*.iml
+.gradle
+/local.properties
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
\ No newline at end of file
diff --git a/Week08/OneStone/app/.gitignore b/Week08/OneStone/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/Week08/OneStone/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/Week08/OneStone/app/build.gradle.kts b/Week08/OneStone/app/build.gradle.kts
new file mode 100644
index 0000000..8477ced
--- /dev/null
+++ b/Week08/OneStone/app/build.gradle.kts
@@ -0,0 +1,82 @@
+plugins {
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.hilt.android)
+ alias(libs.plugins.legacy.kapt)
+ alias(libs.plugins.kotlin.compose)
+}
+
+apply(plugin = "androidx.navigation.safeargs.kotlin")
+
+android {
+ namespace = "com.example.umc10th"
+ compileSdk {
+ version = release(36) {
+ minorApiLevel = 1
+ }
+ }
+
+ buildFeatures {
+ viewBinding = true
+ compose = true
+ }
+
+ defaultConfig {
+ applicationId = "com.example.umc10th"
+ minSdk = 33
+ 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
+ }
+}
+
+
+dependencies {
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.material)
+ implementation(libs.androidx.activity)
+ implementation(libs.androidx.constraintlayout)
+ implementation(libs.androidx.navigation.fragment.ktx)
+ implementation(libs.androidx.navigation.ui.ktx)
+ implementation(libs.androidx.navigation.compose)
+ implementation(libs.androidx.ui.tooling.preview)
+ testImplementation(libs.junit)
+ androidTestImplementation(libs.androidx.junit)
+ androidTestImplementation(libs.androidx.espresso.core)
+ implementation("com.google.code.gson:gson:2.10.1")
+ implementation("androidx.datastore:datastore-preferences:1.1.1")
+
+ implementation("com.squareup.retrofit2:retrofit:2.9.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.9.0")
+
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
+ implementation(libs.hilt.android)
+ debugImplementation(libs.androidx.ui.tooling)
+ kapt(libs.hilt.android.compiler)
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.compose.ui)
+ implementation(libs.androidx.compose.ui.tooling.preview)
+ implementation(libs.androidx.compose.foundation)
+ implementation(libs.androidx.compose.material3)
+ debugImplementation(libs.androidx.compose.ui.tooling)
+
+}
diff --git a/Week08/OneStone/app/proguard-rules.pro b/Week08/OneStone/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/Week08/OneStone/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
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/androidTest/java/com/example/umc10th/ExampleInstrumentedTest.kt b/Week08/OneStone/app/src/androidTest/java/com/example/umc10th/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..0f9f0f9
--- /dev/null
+++ b/Week08/OneStone/app/src/androidTest/java/com/example/umc10th/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.example.umc10th
+
+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.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.example.umc10th", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/AndroidManifest.xml b/Week08/OneStone/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..173c96d
--- /dev/null
+++ b/Week08/OneStone/app/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/MyApplication.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/MyApplication.kt
new file mode 100644
index 0000000..24f54ba
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/MyApplication.kt
@@ -0,0 +1,9 @@
+package com.example.umc10th
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class MyApplication : Application(){
+
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/local/DataStoreManager.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/local/DataStoreManager.kt
new file mode 100644
index 0000000..ecfb605
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/local/DataStoreManager.kt
@@ -0,0 +1,132 @@
+package com.example.umc10th.data.local
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import androidx.datastore.preferences.core.Preferences
+import com.example.umc10th.R
+import com.example.umc10th.data.model.Product
+import com.example.umc10th.data.model.PurchaseProduct
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import kotlinx.coroutines.flow.first
+import kotlin.collections.listOf
+
+
+val Context.dataStore: DataStore by preferencesDataStore(name = "ex_name")
+
+val PRODUCTS_KEY = stringPreferencesKey("products_key")
+
+val PURCHASE_PRODUCTS_KEY = stringPreferencesKey("purchase_products_key")
+private val gson = Gson()
+private val productImageResIds = setOf(R.drawable.shoes1, R.drawable.shoes2)
+private val purchaseProductImageResIds = setOf(
+ R.drawable.socks1,
+ R.drawable.socks2,
+ R.drawable.shoes1,
+ R.drawable.shoes2
+)
+
+suspend fun saveProducts(context: Context, Products: List) {
+
+ val jsonString = gson.toJson(Products)
+ context.dataStore.edit { settings ->
+ settings[PRODUCTS_KEY] = jsonString
+ }
+}
+
+fun getProducts(context: Context): Flow> {
+ return context.dataStore.data.map { preferences ->
+ val jsonString = preferences[PRODUCTS_KEY] ?: "[]"
+ val type = object : TypeToken>() {}.type
+ gson.fromJson(jsonString, type)
+ }
+}
+
+
+
+suspend fun savePurchaseProducts(context: Context, Products: List) {
+
+ val jsonString = gson.toJson(Products)
+ context.dataStore.edit { settings ->
+ settings[PURCHASE_PRODUCTS_KEY] = jsonString
+ }
+}
+
+fun getPurchaseProducts(context: Context): Flow> {
+ return context.dataStore.data.map { preferences ->
+ val jsonString = preferences[PURCHASE_PRODUCTS_KEY] ?: "[]"
+ val type = object : TypeToken>() {}.type
+ gson.fromJson(jsonString, type)
+ }
+}
+suspend fun initializeProducts(context: Context) {
+ val currentProducts = getProducts(context).first()
+
+ val defaultProducts = listOf(
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 1),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 2),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 3),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 4),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 5),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 6),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 7),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 8),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 9),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 10),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 11),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 12),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 13),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 14),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 15),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 16),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 17),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 18),
+ Product(R.drawable.shoes1, "Air Jordan XXXVI", "Men's Shoes", "US$185", id = 19),
+ Product(R.drawable.shoes2, "Nike Air Force 1 '07", "Women's Shoes", "US$115", id = 20)
+ )
+ val currentProductIds = currentProducts.map { it.id }
+ val hasValidProducts = currentProducts.isNotEmpty() &&
+ currentProducts.all { it.imageResId in productImageResIds && it.id > 0 } &&
+ currentProductIds.distinct().size == currentProductIds.size
+ if (hasValidProducts) return
+ saveProducts(context, defaultProducts)
+}
+
+
+
+suspend fun initializePurchaseProducts(context: Context) {
+ val currentProducts = getPurchaseProducts(context).first()
+
+ val defaultPurchaseProducts = listOf(
+ PurchaseProduct(1, R.drawable.socks1, false, "Nike Everyday Plus Cushioned", "Training Ankle Socks (6 Pairs)", "5 Colours", "US\$10"),
+ PurchaseProduct(2, R.drawable.socks2, false, "Nike Elite Crew", "Basketball Socks", "7 Colours","US\$16"),
+ PurchaseProduct(3, R.drawable.shoes1, true,"Nike Air Force 1 '07", "Women's Shoes", "5 Colours", "US\$115"),
+ PurchaseProduct(4, R.drawable.shoes2, true, "Jordan ENike Air Force 1 '07ssentials", "Men's Shoes", "2 Colours","US\$115"),
+ PurchaseProduct(5, R.drawable.socks1, false, "Nike Everyday Plus Cushioned", "Training Ankle Socks (6 Pairs)", "5 Colours", "US\$10"),
+ PurchaseProduct(6, R.drawable.socks2, false, "Nike Elite Crew", "Basketball Socks", "7 Colours","US\$16"),
+ PurchaseProduct(7, R.drawable.shoes1, true,"Nike Air Force 1 '07", "Women's Shoes", "5 Colours", "US\$115"),
+ PurchaseProduct(8, R.drawable.shoes2, true, "Jordan ENike Air Force 1 '07ssentials", "Men's Shoes", "2 Colours","US\$115"),
+ PurchaseProduct(9, R.drawable.socks1, false, "Nike Everyday Plus Cushioned", "Training Ankle Socks (6 Pairs)", "5 Colours", "US\$10"),
+ PurchaseProduct(10, R.drawable.socks2, false, "Nike Elite Crew", "Basketball Socks", "7 Colours","US\$16"),
+ PurchaseProduct(11, R.drawable.shoes1, true,"Nike Air Force 1 '07", "Women's Shoes", "5 Colours", "US\$115"),
+ PurchaseProduct(12, R.drawable.shoes2, true, "Jordan ENike Air Force 1 '07ssentials", "Men's Shoes", "2 Colours","US\$115"),
+ PurchaseProduct(13, R.drawable.socks1, false, "Nike Everyday Plus Cushioned", "Training Ankle Socks (6 Pairs)", "5 Colours", "US\$10"),
+ PurchaseProduct(14, R.drawable.shoes1, true,"Nike Air Force 1 '07", "Women's Shoes", "5 Colours", "US\$115"),
+ PurchaseProduct(15, R.drawable.shoes2, true, "Jordan ENike Air Force 1 '07ssentials", "Men's Shoes", "2 Colours","US\$115"),
+ PurchaseProduct(16, R.drawable.socks1, false, "Nike Everyday Plus Cushioned", "Training Ankle Socks (6 Pairs)", "5 Colours", "US\$10"),
+ PurchaseProduct(17, R.drawable.socks2, false, "Nike Elite Crew", "Basketball Socks", "7 Colours","US\$16"),
+ PurchaseProduct(18, R.drawable.shoes1, true,"Nike Air Force 1 '07", "Women's Shoes", "5 Colours", "US\$115"),
+ PurchaseProduct(19, R.drawable.shoes2, true, "Jordan ENike Air Force 1 '07ssentials", "Men's Shoes", "2 Colours","US\$115"),
+ PurchaseProduct(20, R.drawable.socks1, false, "Nike Everyday Plus Cushioned", "Training Ankle Socks (6 Pairs)", "5 Colours", "US\$10"),
+ PurchaseProduct(21, R.drawable.socks2, false, "Nike Elite Crew", "Basketball Socks", "7 Colours","US\$16"),
+ PurchaseProduct(22, R.drawable.shoes1, true,"Nike Air Force 1 '07", "Women's Shoes", "5 Colours", "US\$115"),
+ PurchaseProduct(23, R.drawable.shoes2, true, "Jordan ENike Air Force 1 '07ssentials", "Men's Shoes", "2 Colours","US\$115"),
+ )
+ if (currentProducts.isNotEmpty() && currentProducts.all { it.imageResId in purchaseProductImageResIds }) return
+ savePurchaseProducts(context, defaultPurchaseProducts)
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/FollowingProfile.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/FollowingProfile.kt
new file mode 100644
index 0000000..201aeb0
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/FollowingProfile.kt
@@ -0,0 +1,8 @@
+package com.example.umc10th.data.model
+
+import android.graphics.Bitmap
+
+data class FollowingProfile(
+ val id: Int,
+ val avatarBitmap: Bitmap
+)
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/Product.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/Product.kt
new file mode 100644
index 0000000..6480970
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/Product.kt
@@ -0,0 +1,11 @@
+package com.example.umc10th.data.model
+
+import androidx.annotation.DrawableRes
+
+data class Product(
+ val imageResId: Int,
+ val name: String,
+ val description: String,
+ val price: String,
+ val id: Int = 0
+)
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/PurchaseProduct.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/PurchaseProduct.kt
new file mode 100644
index 0000000..b9c3e3a
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/PurchaseProduct.kt
@@ -0,0 +1,14 @@
+package com.example.umc10th.data.model
+
+import androidx.annotation.DrawableRes
+
+data class PurchaseProduct(
+ val id: Int,
+ val imageResId: Int,
+ val isBest: Boolean,
+ val title: String,
+ val description: String,
+ val colornum: String,
+ val price: String,
+ var isWishlisted: Boolean = false
+)
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/ReqResModels.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/ReqResModels.kt
new file mode 100644
index 0000000..7b53ecc
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/model/ReqResModels.kt
@@ -0,0 +1,23 @@
+package com.example.umc10th.data.model
+
+import com.google.gson.annotations.SerializedName
+
+data class ReqResUsersResponse(
+ val page: Int,
+ @SerializedName("per_page")
+ val perPage: Int,
+ val total: Int,
+ @SerializedName("total_pages")
+ val totalPages: Int,
+ val data: List
+)
+
+data class ReqResUser(
+ val id: Int,
+ val email: String,
+ @SerializedName("first_name")
+ val firstName: String,
+ @SerializedName("last_name")
+ val lastName: String,
+ val avatar: String
+)
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResApiService.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResApiService.kt
new file mode 100644
index 0000000..f3ef292
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResApiService.kt
@@ -0,0 +1,12 @@
+package com.example.umc10th.data.remote
+
+import com.example.umc10th.data.model.ReqResUsersResponse
+import retrofit2.http.GET
+
+interface ReqResApiService {
+ @GET("api/users")
+ suspend fun getUsers(): ReqResUsersResponse
+
+ @GET("api/users/23")
+ suspend fun getMissingUser(): ReqResUsersResponse
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResDebugConfig.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResDebugConfig.kt
new file mode 100644
index 0000000..4529675
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResDebugConfig.kt
@@ -0,0 +1,5 @@
+package com.example.umc10th.data.remote
+
+object ReqResDebugConfig {
+ val forceHttpErrorCode: Int? = null // null : 정상작동, 400 : 내잘못 토스트메세지, 500 : 니잘못 토스트메세지
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResLoggingInterceptor.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResLoggingInterceptor.kt
new file mode 100644
index 0000000..cdf330f
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResLoggingInterceptor.kt
@@ -0,0 +1,46 @@
+package com.example.umc10th.data.remote
+
+import android.util.Log
+import okhttp3.Interceptor
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Protocol
+import okhttp3.Response
+import okhttp3.ResponseBody.Companion.toResponseBody
+
+class ReqResLoggingInterceptor : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request = chain.request()
+ val maskedApiKey = request.header("x-api-key")?.let { maskApiKey(it) }
+
+ Log.d(TAG, "Request ${request.method} ${request.url}")
+ Log.d(TAG, "Header x-api-key=$maskedApiKey")
+
+ ReqResDebugConfig.forceHttpErrorCode?.let { forcedCode ->
+ val response = Response.Builder()
+ .request(request)
+ .protocol(Protocol.HTTP_1_1)
+ .code(forcedCode)
+ .message("Forced debug HTTP error")
+ .body("""{"error":"forced debug error"}"""
+ .toResponseBody("application/json".toMediaType()))
+ .build()
+
+ Log.d(TAG, "Forced response code=${response.code} url=${response.request.url}")
+ return response
+ }
+
+ val response = chain.proceed(request)
+ Log.d(TAG, "Response code=${response.code} url=${response.request.url}")
+
+ return response
+ }
+
+ private fun maskApiKey(apiKey: String): String {
+ if (apiKey.length <= 8) return "*".repeat(apiKey.length)
+ return "${apiKey.take(8)}...${apiKey.takeLast(4)}"
+ }
+
+ companion object {
+ private const val TAG = "ReqResApi"
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResRetrofitClient.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResRetrofitClient.kt
new file mode 100644
index 0000000..285b314
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/remote/ReqResRetrofitClient.kt
@@ -0,0 +1,30 @@
+package com.example.umc10th.data.remote
+
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+object ReqResRetrofitClient {
+ private const val API_KEY = "reqres_3eb039d9ecd24873b53b35fb4d3ad155"
+
+ private val okHttpClient: OkHttpClient by lazy {
+ OkHttpClient.Builder()
+ .addInterceptor { chain ->
+ val request = chain.request().newBuilder()
+ .addHeader("x-api-key", API_KEY)
+ .build()
+ chain.proceed(request)
+ }
+ .addInterceptor(ReqResLoggingInterceptor())
+ .build()
+ }
+
+ val api: ReqResApiService by lazy {
+ Retrofit.Builder()
+ .baseUrl("https://reqres.in/")
+ .client(okHttpClient)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ .create(ReqResApiService::class.java)
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProductRepository.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProductRepository.kt
new file mode 100644
index 0000000..6f0607c
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProductRepository.kt
@@ -0,0 +1,19 @@
+package com.example.umc10th.data.repository
+
+import com.example.umc10th.data.model.Product
+import com.example.umc10th.data.model.PurchaseProduct
+import kotlinx.coroutines.flow.Flow
+
+interface ProductRepository {
+ suspend fun initializeProducts()
+
+ fun getProducts(): Flow>
+
+ suspend fun initializePurchaseProducts()
+
+ fun getPurchaseProducts(): Flow>
+
+ suspend fun savePurchaseProducts(products: List)
+
+ suspend fun toggleWishlist(productId: Int)
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProductRepositoryImpl.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProductRepositoryImpl.kt
new file mode 100644
index 0000000..67e918c
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProductRepositoryImpl.kt
@@ -0,0 +1,51 @@
+package com.example.umc10th.data.repository
+
+import android.content.Context
+import com.example.umc10th.data.local.getProducts
+import com.example.umc10th.data.local.getPurchaseProducts
+import com.example.umc10th.data.local.initializeProducts
+import com.example.umc10th.data.local.initializePurchaseProducts
+import com.example.umc10th.data.local.savePurchaseProducts
+import com.example.umc10th.data.model.Product
+import com.example.umc10th.data.model.PurchaseProduct
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ProductRepositoryImpl @Inject constructor(
+ @param:ApplicationContext private val context: Context
+) : ProductRepository {
+ override suspend fun initializeProducts() {
+ initializeProducts(context)
+ }
+
+ override fun getProducts(): Flow> {
+ return getProducts(context)
+ }
+
+ override suspend fun initializePurchaseProducts() {
+ initializePurchaseProducts(context)
+ }
+
+ override fun getPurchaseProducts(): Flow> {
+ return getPurchaseProducts(context)
+ }
+
+ override suspend fun savePurchaseProducts(products: List) {
+ savePurchaseProducts(context, products)
+ }
+
+ override suspend fun toggleWishlist(productId: Int) {
+ val updatedProducts = getPurchaseProducts(context).first().map { product ->
+ if (product.id == productId) {
+ product.copy(isWishlisted = !product.isWishlisted)
+ } else {
+ product
+ }
+ }
+ savePurchaseProducts(context, updatedProducts)
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProfileRepository.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProfileRepository.kt
new file mode 100644
index 0000000..6d64294
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProfileRepository.kt
@@ -0,0 +1,7 @@
+package com.example.umc10th.data.repository
+
+import com.example.umc10th.data.model.ReqResUser
+
+interface ProfileRepository {
+ suspend fun getUsers(): List
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProfileRepositoryImpl.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProfileRepositoryImpl.kt
new file mode 100644
index 0000000..d4f58a5
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/data/repository/ProfileRepositoryImpl.kt
@@ -0,0 +1,13 @@
+package com.example.umc10th.data.repository
+
+import com.example.umc10th.data.model.ReqResUser
+import com.example.umc10th.data.remote.ReqResRetrofitClient
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ProfileRepositoryImpl @Inject constructor() : ProfileRepository {
+ override suspend fun getUsers(): List {
+ return ReqResRetrofitClient.api.getUsers().data
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/di/RepositoryModule.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/di/RepositoryModule.kt
new file mode 100644
index 0000000..b65af3c
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/di/RepositoryModule.kt
@@ -0,0 +1,27 @@
+package com.example.umc10th.di
+
+import com.example.umc10th.data.repository.ProductRepository
+import com.example.umc10th.data.repository.ProductRepositoryImpl
+import com.example.umc10th.data.repository.ProfileRepository
+import com.example.umc10th.data.repository.ProfileRepositoryImpl
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class RepositoryModule {
+ @Binds
+ @Singleton
+ abstract fun bindProductRepository(
+ productRepositoryImpl: ProductRepositoryImpl
+ ): ProductRepository
+
+ @Binds
+ @Singleton
+ abstract fun bindProfileRepository(
+ profileRepositoryImpl: ProfileRepositoryImpl
+ ): ProfileRepository
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/base/BaseViewModel.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/base/BaseViewModel.kt
new file mode 100644
index 0000000..f08a8ff
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/base/BaseViewModel.kt
@@ -0,0 +1,25 @@
+package com.example.umc10th.ui.base
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+
+interface UiState
+
+abstract class BaseViewModel(
+ initialState: STATE
+) : ViewModel() {
+ private val _uiState = MutableStateFlow(initialState)
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ protected val currentState: STATE
+ get() = _uiState.value
+
+ protected fun setState(reducer: STATE.() -> STATE) {
+ _uiState.update { currentState ->
+ currentState.reducer()
+ }
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/cart/CartFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/cart/CartFragment.kt
new file mode 100644
index 0000000..c958bf5
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/cart/CartFragment.kt
@@ -0,0 +1,41 @@
+package com.example.umc10th.ui.cart
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.navOptions
+import com.example.umc10th.databinding.FragmentCartBinding
+
+class CartFragment : Fragment() {
+
+ private var _binding: FragmentCartBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentCartBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ binding.cartButton.setOnClickListener {
+ val navOptions = navOptions {
+ launchSingleTop = true
+ }
+ val action = CartFragmentDirections.actionCartToPurchase(fromCart = true)
+ findNavController().navigate(action, navOptions)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/cart/CartScreen.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/cart/CartScreen.kt
new file mode 100644
index 0000000..075abfb
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/cart/CartScreen.kt
@@ -0,0 +1,71 @@
+package com.example.umc10th.ui.cart
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.umc10th.R
+
+@Composable
+fun CartScreen(onOrderClick: () -> Unit) {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ Column(
+ modifier = Modifier.align(Alignment.Center),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_bag_circle),
+ contentDescription = null,
+ modifier = Modifier.size(88.dp)
+ )
+ Spacer(modifier = Modifier.height(27.dp))
+ Text(
+ text = "장바구니가 비어 있습니다.\n상품을 추가하면 여기에 표시됩니다.",
+ color = Color.Black,
+ fontSize = 16.sp,
+ textAlign = TextAlign.Center,
+ lineHeight = 23.sp
+ )
+ }
+
+ Button(
+ onClick = onOrderClick,
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .fillMaxWidth()
+ .padding(start = 40.dp, end = 40.dp, bottom = 16.dp)
+ .height(48.dp),
+ shape = RoundedCornerShape(4.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.Black,
+ contentColor = Color.White
+ )
+ ) {
+ Text(
+ text = "주문하기",
+ fontSize = 14.sp,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeFragment.kt
new file mode 100644
index 0000000..e85834c
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeFragment.kt
@@ -0,0 +1,132 @@
+package com.example.umc10th.ui.home
+
+import android.os.Bundle
+import android.os.SystemClock
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.navigation.navOptions
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.example.umc10th.databinding.FragmentHomeBinding
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class HomeFragment : Fragment() {
+
+ private var _binding: FragmentHomeBinding? = null
+ private val binding get() = _binding!!
+ private val args: HomeFragmentArgs by navArgs()
+ private val viewModel: HomeViewModel by viewModels()
+ private var lastBackPressedAt = 0L
+
+ private lateinit var productAdapter: ProductAdapter
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentHomeBinding.inflate(inflater, container, false)
+ val receivedTitle = args.homeTitle
+ Log.d("LIFE_QUIZ", "HomeFragment?먯꽌 ?뺤씤??homeTitle: $receivedTitle")
+
+ args.homeTitle?.let { binding.homeTitle.text = it }
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupProductRecyclerView()
+ observeProducts()
+ setupBackPressedCallback()
+ viewModel.loadProducts()
+ }
+
+ private fun setupBackPressedCallback() {
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ val now = SystemClock.elapsedRealtime()
+ if (now - lastBackPressedAt <= 2000L) {
+ requireActivity().finish()
+ } else {
+ lastBackPressedAt = now
+ Toast.makeText(
+ requireContext(),
+ "?쒕쾲 ???꾨Ⅴ硫??깆쓣 醫낅즺?⑸땲??",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ )
+ }
+
+ private fun observeProducts() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.uiState.collect { uiState ->
+ if (uiState.isLoading) {
+ showLoading()
+ } else {
+ Log.d(TAG, "collect products size=${uiState.products.size}")
+ productAdapter.submitList(uiState.products)
+ }
+ }
+ }
+ }
+ }
+
+ private fun showLoading() {
+ Log.d(TAG, "showLoading()")
+ productAdapter.showLoading()
+ }
+
+ private fun setupProductRecyclerView() {
+ productAdapter = ProductAdapter(emptyList()) { item ->
+ val navController = findNavController()
+ val navOptions = navOptions {
+ popUpTo(navController.graph.findStartDestination().id) {
+ saveState = false
+ }
+ }
+ val action = HomeFragmentDirections.actionMenuHouseSimpleToMenuListMagnifyingGlass(
+ fromCart = false,
+ title = item.name,
+ imageResId = item.imageResId,
+ description = item.description,
+ price = item.price
+ )
+ navController.navigate(action, navOptions)
+ }
+
+ binding.productRecyclerView.layoutManager = LinearLayoutManager(
+ requireContext(),
+ LinearLayoutManager.HORIZONTAL,
+ false
+ )
+ binding.productRecyclerView.adapter = productAdapter
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ companion object {
+ private const val TAG = "HomeFragment"
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeScreen.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeScreen.kt
new file mode 100644
index 0000000..f7f3c84
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeScreen.kt
@@ -0,0 +1,159 @@
+package com.example.umc10th.ui.home
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.umc10th.R
+import com.example.umc10th.data.model.Product
+
+@Composable
+fun HomeScreen(
+ title: String,
+ viewModel: HomeViewModel,
+ onProductClick: (Product) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsState()
+
+ LaunchedEffect(Unit) {
+ viewModel.loadProducts()
+ }
+
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ ) {
+ item {
+ Column(modifier = Modifier.padding(horizontal = 20.dp)) {
+ Spacer(modifier = Modifier.height(20.dp))
+ Text(
+ text = title,
+ color = Color.Black,
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = "9월 4일 목요일",
+ color = Color(0xFF767676),
+ fontSize = 14.sp
+ )
+ }
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(16.dp))
+ Image(
+ painter = painterResource(id = R.drawable.home_logo),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(360.dp),
+ contentScale = ContentScale.Fit
+ )
+ }
+
+ item {
+ Column(modifier = Modifier.padding(horizontal = 20.dp)) {
+ Spacer(modifier = Modifier.height(20.dp))
+ Text(
+ text = "What's new",
+ color = Color.Black,
+ fontSize = 16.sp
+ )
+ Spacer(modifier = Modifier.height(6.dp))
+ Text(
+ text = "나이키 최신 상품",
+ color = Color.Black,
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+
+ item {
+ Spacer(modifier = Modifier.height(16.dp))
+ LazyRow(
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = PaddingValues(horizontal = 20.dp),
+ horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(16.dp)
+ ) {
+ items(
+ items = uiState.products,
+ key = { product -> product.id }
+ ) { product ->
+ HomeProductItem(
+ product = product,
+ onClick = { onProductClick(product) }
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+ }
+}
+
+@Composable
+private fun HomeProductItem(
+ product: Product,
+ onClick: () -> Unit
+) {
+ val imageResId = product.imageResId.takeIf {
+ it == R.drawable.shoes1 || it == R.drawable.shoes2
+ } ?: R.drawable.shoes1
+
+ Column(
+ modifier = Modifier
+ .width(314.dp)
+ .clickable { onClick() }
+ ) {
+ Image(
+ painter = painterResource(id = imageResId),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(314.dp),
+ contentScale = ContentScale.Crop
+ )
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ text = product.name,
+ color = Color(0xFF111111),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.width(160.dp)
+ )
+ Spacer(modifier = Modifier.height(6.dp))
+ Text(
+ text = product.price,
+ color = Color(0xFF767676),
+ fontSize = 13.sp
+ )
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeViewModel.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeViewModel.kt
new file mode 100644
index 0000000..4a0ee23
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/HomeViewModel.kt
@@ -0,0 +1,42 @@
+package com.example.umc10th.ui.home
+
+import androidx.lifecycle.viewModelScope
+import com.example.umc10th.data.model.Product
+import com.example.umc10th.data.repository.ProductRepository
+import com.example.umc10th.ui.base.BaseViewModel
+import com.example.umc10th.ui.base.UiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class HomeViewModel @Inject constructor(
+ private val productRepository: ProductRepository
+) : BaseViewModel(HomeUiState()) {
+ private var hasStartedLoading = false
+
+ fun loadProducts() {
+ if (hasStartedLoading) return
+ hasStartedLoading = true
+
+ viewModelScope.launch {
+ setState { copy(isLoading = true) }
+ delay(1500)
+ productRepository.initializeProducts()
+ productRepository.getProducts().collect { products ->
+ setState {
+ copy(
+ isLoading = false,
+ products = products
+ )
+ }
+ }
+ }
+ }
+}
+
+data class HomeUiState(
+ val isLoading: Boolean = true,
+ val products: List = emptyList()
+) : UiState
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/ProductAdapter.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/ProductAdapter.kt
new file mode 100644
index 0000000..0c8f757
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/home/ProductAdapter.kt
@@ -0,0 +1,114 @@
+package com.example.umc10th.ui.home
+
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.example.umc10th.data.model.Product
+import com.example.umc10th.databinding.ItemLoadingProductBinding
+import com.example.umc10th.databinding.ItemProductBinding
+
+class ProductAdapter(
+ private var items: List,
+ private val onItemClick: (Product) -> Unit
+) : RecyclerView.Adapter() {
+
+ private var isLoading = false
+ private var loadingItemCount = DEFAULT_LOADING_ITEM_COUNT
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ Log.d(TAG, "onCreateViewHolder(viewType=$viewType, isLoading=$isLoading)")
+ return when (viewType) {
+ VIEW_TYPE_LOADING -> {
+ val binding = ItemLoadingProductBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ LoadingViewHolder(binding)
+ }
+ else -> {
+ val binding = ItemProductBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ ProductViewHolder(binding)
+ }
+ }
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ Log.d(
+ TAG,
+ "onBindViewHolder(position=$position, holder=${holder::class.java.simpleName}, isLoading=$isLoading)"
+ )
+ if (holder is ProductViewHolder) {
+ holder.bind(items[position])
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ val viewType = if (isLoading) VIEW_TYPE_LOADING else VIEW_TYPE_PRODUCT
+ Log.d(TAG, "getItemViewType(position=$position) -> $viewType")
+ return viewType
+ }
+
+ fun showLoading() {
+ isLoading = true
+ Log.d(TAG, "showLoading()")
+ notifyDataSetChanged()
+ }
+
+ fun submitList(newItems: List) {
+ isLoading = false
+ items = newItems
+ Log.d(TAG, "submitList(size=${newItems.size})")
+ notifyDataSetChanged()
+ }
+
+ override fun getItemCount(): Int {
+ val count = if (isLoading) loadingItemCount else items.size
+ Log.d(TAG, "getItemCount() -> $count (isLoading=$isLoading)")
+ return count
+ }
+
+ class ProductViewHolder(
+ private val binding: ItemProductBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(item: Product) {
+ binding.productImage.setImageResource(item.imageResId)
+ binding.productName.text = item.name
+ binding.productPrice.text = item.price
+ }
+ }
+
+ class LoadingViewHolder(
+ binding: ItemLoadingProductBinding
+ ) : RecyclerView.ViewHolder(binding.root)
+
+ override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
+ super.onViewAttachedToWindow(holder)
+ if (holder is ProductViewHolder) {
+ holder.itemView.setOnClickListener {
+ val position = holder.bindingAdapterPosition
+ if (position != RecyclerView.NO_POSITION && !isLoading) {
+ onItemClick(items[position])
+ }
+ }
+ }
+ }
+
+ override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
+ holder.itemView.setOnClickListener(null)
+ super.onViewDetachedFromWindow(holder)
+ }
+
+ companion object {
+ private const val TAG = "ProductAdapter"
+ private const val VIEW_TYPE_PRODUCT = 0
+ private const val VIEW_TYPE_LOADING = 1
+ private const val DEFAULT_LOADING_ITEM_COUNT = 3
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainActivity.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainActivity.kt
new file mode 100644
index 0000000..29ed577
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainActivity.kt
@@ -0,0 +1,84 @@
+package com.example.umc10th.ui.main
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import com.example.umc10th.ui.home.HomeViewModel
+import com.example.umc10th.ui.purchase.PurchaseViewModel
+import com.example.umc10th.ui.wishlist.WishlistViewModel
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class MainActivity : AppCompatActivity() {
+ private val homeViewModel: HomeViewModel by viewModels()
+ private val purchaseViewModel: PurchaseViewModel by viewModels()
+ private val wishlistViewModel: WishlistViewModel by viewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ val homeTitle = intent.getStringExtra(EXTRA_HOME_TITLE)
+
+ /*
+ 기존 XML + Fragment + BottomNavigationView 기반 구현입니다.
+ Compose 미션 진행을 위해 삭제하지 않고 주석 처리했습니다.
+
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.setPadding(systemBars.left, systemBars.top, systemBars.right, 0)
+ insets
+ }
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
+ val navController = navHostFragment.navController
+ if (savedInstanceState == null) {
+ val startArgs = Bundle().apply {
+ putString("homeTitle", homeTitle)
+ }
+ navController.setGraph(R.navigation.nav_graph, startArgs)
+ }
+
+ binding.bottomNav.setOnItemSelectedListener { item ->
+ if (navController.currentDestination?.id == item.itemId) {
+ return@setOnItemSelectedListener true
+ }
+
+ navigateToTopLevel(item.itemId, navController)
+ true
+ }
+
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ val matchingItem = binding.bottomNav.menu.findItem(destination.id)
+ if (matchingItem != null) {
+ matchingItem.isChecked = true
+ } else if (destination.id == R.id.product_detail_fragment) {
+ binding.bottomNav.menu.findItem(R.id.menu_list_magnifying_glass)?.isChecked = true
+ } else {
+ for (index in 0 until binding.bottomNav.menu.size()) {
+ binding.bottomNav.menu.getItem(index).isChecked = false
+ }
+ }
+ }
+ */
+
+ setContent {
+ MainComposeScreen(
+ homeTitle = homeTitle,
+ homeViewModel = homeViewModel,
+ purchaseViewModel = purchaseViewModel,
+ wishlistViewModel = wishlistViewModel
+ )
+ }
+ }
+
+ companion object {
+ const val EXTRA_HOME_TITLE = "extra_home_title"
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainBottomBar.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainBottomBar.kt
new file mode 100644
index 0000000..a936a92
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainBottomBar.kt
@@ -0,0 +1,80 @@
+package com.example.umc10th.ui.main
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Composable
+fun MainBottomBar(
+ selectedTab: MainTab,
+ onTabSelected: (MainTab) -> Unit
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(Color.White)
+ .navigationBarsPadding()
+ ) {
+ HorizontalDivider(color = Color(0xFFEAEAEA), thickness = 1.dp)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(64.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ MainTab.entries.forEach { tab ->
+ val selected = selectedTab == tab
+ Column(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight()
+ .clickable { onTabSelected(tab) }
+ .padding(top = 8.dp, bottom = 6.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ painter = painterResource(id = tab.iconResId),
+ contentDescription = tab.label,
+ tint = if (selected) Color.Black else Color(0xFF8A8A8A),
+ modifier = Modifier.size(24.dp)
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = tab.label,
+ color = if (selected) Color.Black else Color(0xFF8A8A8A),
+ fontSize = 11.sp,
+ fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
+ maxLines = 1
+ )
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+
+fun MainBottomBar2(){
+ MainBottomBar(selectedTab = MainTab.Home, onTabSelected = {})
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainComposeScreen.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainComposeScreen.kt
new file mode 100644
index 0000000..7329bae
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainComposeScreen.kt
@@ -0,0 +1,201 @@
+package com.example.umc10th.ui.main
+
+import android.app.Activity
+import android.os.SystemClock
+import android.widget.Toast
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableLongStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.navigation.NavGraph.Companion.findStartDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.currentBackStackEntryAsState
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.navOptions
+import com.example.umc10th.data.model.Product
+import com.example.umc10th.data.model.PurchaseProduct
+import com.example.umc10th.ui.cart.CartScreen
+import com.example.umc10th.ui.home.HomeScreen
+import com.example.umc10th.ui.home.HomeViewModel
+import com.example.umc10th.ui.productdetail.ProductDetailScreen
+import com.example.umc10th.ui.productdetail.ProductDetailUiState
+import com.example.umc10th.ui.profile.ProfileScreen
+import com.example.umc10th.ui.purchase.PurchaseScreen
+import com.example.umc10th.ui.purchase.PurchaseViewModel
+import com.example.umc10th.ui.wishlist.WishlistScreen
+import com.example.umc10th.ui.wishlist.WishlistViewModel
+
+@Composable
+fun MainComposeScreen(
+ homeTitle: String?,
+ homeViewModel: HomeViewModel,
+ purchaseViewModel: PurchaseViewModel,
+ wishlistViewModel: WishlistViewModel
+) {
+ val context = LocalContext.current
+ val activity = context as? Activity
+ val navController = rememberNavController()
+ val backStackEntry by navController.currentBackStackEntryAsState()
+ val currentRoute = backStackEntry?.destination?.route
+ val selectedTab = if (currentRoute == PRODUCT_DETAIL_ROUTE) {
+ MainTab.Purchase
+ } else {
+ MainTab.fromRoute(currentRoute)
+ }
+ var lastBackPressedAt by remember { mutableLongStateOf(0L) }
+ var selectedProduct by remember { mutableStateOf(null) }
+
+ fun openProductDetail(product: ProductDetailUiState) {
+ selectedProduct = product
+ if (navController.currentDestination?.route != MainTab.Purchase.route) {
+ navController.navigateToTopLevel(MainTab.Purchase)
+ }
+ navController.navigate(PRODUCT_DETAIL_ROUTE) {
+ launchSingleTop = true
+ }
+ }
+
+ BackHandler {
+ if (currentRoute == PRODUCT_DETAIL_ROUTE) {
+ navController.popBackStack()
+ return@BackHandler
+ }
+
+ if (selectedTab != MainTab.Home) {
+ navController.navigateToTopLevel(MainTab.Home)
+ return@BackHandler
+ }
+
+ val now = SystemClock.elapsedRealtime()
+ if (now - lastBackPressedAt <= 2000L) {
+ activity?.finish()
+ } else {
+ lastBackPressedAt = now
+ Toast.makeText(context, "뒤로가기를 한 번 더 누르면 종료됩니다.", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ Scaffold(
+ containerColor = Color.White,
+ contentWindowInsets = WindowInsets(0),
+ bottomBar = {
+ MainBottomBar(
+ selectedTab = selectedTab,
+ onTabSelected = { tab ->
+ navController.navigateToTopLevel(tab)
+ }
+ )
+ }
+ ) { innerPadding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .padding(innerPadding)
+ .statusBarsPadding()
+ ) {
+ NavHost(
+ navController = navController,
+ startDestination = MainTab.Home.route,
+ modifier = Modifier.fillMaxSize(),
+ enterTransition = { EnterTransition.None },
+ exitTransition = { ExitTransition.None },
+ popEnterTransition = { EnterTransition.None },
+ popExitTransition = { ExitTransition.None }
+ ) {
+ composable(MainTab.Home.route) {
+ HomeScreen(
+ title = homeTitle ?: "Discover",
+ viewModel = homeViewModel,
+ onProductClick = { product ->
+ openProductDetail(product.toDetailUiState())
+ }
+ )
+ }
+ composable(MainTab.Purchase.route) {
+ PurchaseScreen(
+ viewModel = purchaseViewModel,
+ onProductClick = { product ->
+ openProductDetail(product.toDetailUiState())
+ }
+ )
+ }
+ composable(MainTab.Wishlist.route) {
+ WishlistScreen(
+ viewModel = wishlistViewModel,
+ onProductClick = { product ->
+ openProductDetail(product.toDetailUiState())
+ }
+ )
+ }
+ composable(MainTab.Cart.route) {
+ CartScreen(
+ onOrderClick = {
+ navController.navigateToTopLevel(MainTab.Purchase)
+ }
+ )
+ }
+ composable(MainTab.Profile.route) {
+ ProfileScreen()
+ }
+ composable(PRODUCT_DETAIL_ROUTE) {
+ selectedProduct?.let { product ->
+ ProductDetailScreen(
+ product = product,
+ onBackClick = { navController.popBackStack() }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun NavHostController.navigateToTopLevel(tab: MainTab) {
+ navigate(
+ route = tab.route,
+ navOptions = navOptions {
+ launchSingleTop = true
+ popUpTo(graph.findStartDestination().id) {
+ saveState = false
+ }
+ }
+ )
+}
+
+private fun Product.toDetailUiState(): ProductDetailUiState {
+ return ProductDetailUiState(
+ title = name,
+ imageResId = imageResId,
+ description = description,
+ price = price
+ )
+}
+
+private fun PurchaseProduct.toDetailUiState(): ProductDetailUiState {
+ return ProductDetailUiState(
+ title = title,
+ imageResId = imageResId,
+ description = description,
+ price = price
+ )
+}
+
+private const val PRODUCT_DETAIL_ROUTE = "product_detail"
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainTab.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainTab.kt
new file mode 100644
index 0000000..f6c455b
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/main/MainTab.kt
@@ -0,0 +1,21 @@
+package com.example.umc10th.ui.main
+
+import com.example.umc10th.R
+
+enum class MainTab(
+ val route: String,
+ val label: String,
+ val iconResId: Int
+) {
+ Home("home", "홈", R.drawable.ic_house_simple),
+ Purchase("purchase", "구매하기", R.drawable.ic_list_magnifying_glass),
+ Wishlist("wishlist", "위시리스트", R.drawable.ic_heart_straight),
+ Cart("cart", "장바구니", R.drawable.ic_bag_simple),
+ Profile("profile", "프로필", R.drawable.ic_user);
+
+ companion object {
+ fun fromRoute(route: String?): MainTab {
+ return entries.firstOrNull { it.route == route } ?: Home
+ }
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/productdetail/ProductDetailFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/productdetail/ProductDetailFragment.kt
new file mode 100644
index 0000000..d0406dd
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/productdetail/ProductDetailFragment.kt
@@ -0,0 +1,47 @@
+package com.example.umc10th.ui.productdetail
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import com.example.umc10th.databinding.FragmentProductDetailBinding
+
+class ProductDetailFragment : Fragment() {
+
+ private var _binding: FragmentProductDetailBinding? = null
+ private val binding get() = _binding!!
+ private val args: ProductDetailFragmentArgs by navArgs()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentProductDetailBinding.inflate(inflater, container, false)
+ bindProductDetails()
+ setupHeader()
+ return binding.root
+ }
+
+ private fun bindProductDetails() {
+ binding.productDetailHeaderTitle.text = args.title
+ binding.productDetailImage.setImageResource(args.imageResId)
+ binding.productDetailTitle.text = args.title
+ binding.productDetailDescription.text = args.description
+ binding.productDetailPrice.text = args.price
+ }
+
+ private fun setupHeader() {
+ binding.productDetailBackButton.setOnClickListener {
+ findNavController().navigateUp()
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/productdetail/ProductDetailScreen.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/productdetail/ProductDetailScreen.kt
new file mode 100644
index 0000000..80117b2
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/productdetail/ProductDetailScreen.kt
@@ -0,0 +1,151 @@
+package com.example.umc10th.ui.productdetail
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.umc10th.R
+
+data class ProductDetailUiState(
+ val title: String,
+ val imageResId: Int,
+ val description: String,
+ val price: String
+)
+
+@Composable
+fun ProductDetailScreen(
+ product: ProductDetailUiState,
+ onBackClick: () -> Unit
+) {
+ val imageResId = product.imageResId.takeIf {
+ it == R.drawable.socks1 ||
+ it == R.drawable.socks2 ||
+ it == R.drawable.shoes1 ||
+ it == R.drawable.shoes2
+ } ?: R.drawable.shoes1
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .verticalScroll(rememberScrollState())
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(start = 16.dp, top = 16.dp, end = 16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.icon_back),
+ contentDescription = null,
+ modifier = Modifier
+ .size(40.dp)
+ .padding(8.dp)
+ .clickable { onBackClick() }
+ )
+ Text(
+ text = product.title,
+ color = Color(0xFF111111),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.weight(1f)
+ )
+ Image(
+ painter = painterResource(id = R.drawable.ic_list_magnifying_glass),
+ contentDescription = null,
+ modifier = Modifier
+ .size(40.dp)
+ .padding(8.dp)
+ )
+ }
+
+ Image(
+ painter = painterResource(id = imageResId),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(240.dp)
+ .padding(start = 24.dp, top = 24.dp, end = 24.dp),
+ contentScale = ContentScale.Crop
+ )
+
+ Text(
+ text = product.title,
+ color = Color(0xFF111111),
+ fontSize = 24.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp)
+ )
+ Text(
+ text = product.description,
+ color = Color(0xFF767676),
+ fontSize = 16.sp,
+ modifier = Modifier.padding(start = 24.dp, top = 12.dp, end = 24.dp)
+ )
+ Text(
+ text = product.price,
+ color = Color(0xFF111111),
+ fontSize = 18.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(start = 24.dp, top = 16.dp, end = 24.dp)
+ )
+ Text(
+ text = "The Nike Everyday Plus Cushioned Socks bring comfort to your workout with extra cushioning under the heel and forefoot and a snug, supportive arch band. Sweat-wicking power and breathability up top help keep your feet dry and cool to help push you through that extra set.\n\nShown: Multi-Color\nStyle: SX6897-965",
+ color = Color(0xFF444444),
+ fontSize = 15.sp,
+ lineHeight = 22.sp,
+ modifier = Modifier.padding(start = 24.dp, top = 24.dp, end = 24.dp)
+ )
+
+ ProductDetailButton(text = "Select Size")
+ ProductDetailButton(text = "Add to Cart", backgroundColor = Color(0xFF111111), textColor = Color.White)
+ ProductDetailButton(text = "Wishlist")
+ Spacer(modifier = Modifier.height(24.dp))
+ }
+}
+
+@Composable
+private fun ProductDetailButton(
+ text: String,
+ backgroundColor: Color = Color(0xFFE4E4E4),
+ textColor: Color = Color.Black
+) {
+ Text(
+ text = text,
+ color = textColor,
+ fontSize = 14.sp,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .padding(start = 24.dp, top = 12.dp, end = 24.dp)
+ .fillMaxWidth()
+ .height(56.dp)
+ .background(backgroundColor)
+ .padding(top = 18.dp)
+ )
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/FollowingProfileAdapter.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/FollowingProfileAdapter.kt
new file mode 100644
index 0000000..bd5aca5
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/FollowingProfileAdapter.kt
@@ -0,0 +1,41 @@
+package com.example.umc10th.ui.profile
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.example.umc10th.data.model.FollowingProfile
+import com.example.umc10th.databinding.ItemFollowingProfileBinding
+
+class FollowingProfileAdapter(
+ private var profiles: List = emptyList()
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowingProfileViewHolder {
+ val binding = ItemFollowingProfileBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return FollowingProfileViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: FollowingProfileViewHolder, position: Int) {
+ holder.bind(profiles[position])
+ }
+
+ override fun getItemCount(): Int = profiles.size
+
+ fun submitList(newProfiles: List) {
+ profiles = newProfiles
+ notifyDataSetChanged()
+ }
+
+ class FollowingProfileViewHolder(
+ private val binding: ItemFollowingProfileBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(profile: FollowingProfile) {
+ binding.ivFollowingProfile.setImageBitmap(profile.avatarBitmap)
+ }
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileFragment.kt
new file mode 100644
index 0000000..8484b10
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileFragment.kt
@@ -0,0 +1,119 @@
+package com.example.umc10th.ui.profile
+
+import android.os.Bundle
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.ProgressBar
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.example.umc10th.databinding.FragmentProfileBinding
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class ProfileFragment : Fragment() {
+
+ private var _binding: FragmentProfileBinding? = null
+ private val binding get() = _binding!!
+ private val viewModel: ProfileViewModel by viewModels()
+ private lateinit var profileImageView: ImageView
+ private lateinit var profileProgressBar: ProgressBar
+ private val followingProfileAdapter = FollowingProfileAdapter()
+ private var lastShownErrorMessage: String? = null
+
+ 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)
+ setupProfileImageView()
+ setupFollowingRecyclerView()
+ observeProfile()
+ viewModel.loadUserProfile()
+ }
+
+ private fun setupProfileImageView() {
+ profileImageView = ImageView(requireContext()).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+ scaleType = ImageView.ScaleType.CENTER_CROP
+ }
+ profileProgressBar = ProgressBar(requireContext()).apply {
+ layoutParams = FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER
+ )
+ }
+ binding.userProfile.addView(profileImageView)
+ binding.userProfile.addView(profileProgressBar)
+ }
+
+ private fun setupFollowingRecyclerView() {
+ binding.rvFollowing.layoutManager = LinearLayoutManager(
+ requireContext(),
+ LinearLayoutManager.HORIZONTAL,
+ false
+ )
+ binding.rvFollowing.adapter = followingProfileAdapter
+ }
+
+ private fun observeProfile() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.uiState.collect { uiState ->
+ profileProgressBar.visibility = if (uiState.isLoading) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ profileImageView.visibility = if (uiState.profileBitmap == null) {
+ View.INVISIBLE
+ } else {
+ View.VISIBLE
+ }
+ if (uiState.isLoading) {
+ binding.userName.text = ""
+ }
+ if (uiState.userName.isNotEmpty()) {
+ binding.userName.text = uiState.userName
+ }
+ uiState.profileBitmap?.let { profileImageView.setImageBitmap(it) }
+ followingProfileAdapter.submitList(uiState.followingProfiles)
+ showErrorToast(uiState.errorMessage)
+ }
+ }
+ }
+ }
+
+ private fun showErrorToast(errorMessage: String?) {
+ if (errorMessage.isNullOrBlank()) return
+ if (lastShownErrorMessage == errorMessage) return
+
+ lastShownErrorMessage = errorMessage
+ Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onDestroyView() {
+ binding.rvFollowing.adapter = null
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileScreen.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileScreen.kt
new file mode 100644
index 0000000..56b5ee3
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileScreen.kt
@@ -0,0 +1,13 @@
+package com.example.umc10th.ui.profile
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun ProfileScreen() {
+ Box(
+ modifier = Modifier.fillMaxSize()
+ )
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileViewModel.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileViewModel.kt
new file mode 100644
index 0000000..2a9d252
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/profile/ProfileViewModel.kt
@@ -0,0 +1,96 @@
+package com.example.umc10th.ui.profile
+
+import android.graphics.BitmapFactory
+import android.graphics.Bitmap
+import android.util.Log
+import androidx.lifecycle.viewModelScope
+import com.example.umc10th.data.model.FollowingProfile
+import com.example.umc10th.data.repository.ProfileRepository
+import com.example.umc10th.ui.base.BaseViewModel
+import com.example.umc10th.ui.base.UiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import retrofit2.HttpException
+import java.net.URL
+
+@HiltViewModel
+class ProfileViewModel @Inject constructor(
+ private val profileRepository: ProfileRepository
+) : BaseViewModel(ProfileUiState()) {
+ fun loadUserProfile() {
+ if (currentState.userName.isNotEmpty()) return
+
+ viewModelScope.launch {
+ setState { copy(isLoading = true, errorMessage = null) }
+
+ runCatching {
+ val users = profileRepository.getUsers()
+ val user = users.firstOrNull { it.id == 1 }
+ ?: error("User id 1 not found")
+
+ val profileBitmap = loadBitmap(user.avatar)
+ val followingProfiles = users
+ .filter { it.id != 1 }
+ .map { followingUser ->
+ async {
+ FollowingProfile(
+ id = followingUser.id,
+ avatarBitmap = loadBitmap(followingUser.avatar)
+ )
+ }
+ }
+ .awaitAll()
+
+ ProfileUiState(
+ isLoading = false,
+ userName = "${user.firstName} ${user.lastName}",
+ profileBitmap = profileBitmap,
+ followingProfiles = followingProfiles
+ )
+ }.onSuccess { loadedState ->
+ setState { loadedState }
+ }.onFailure { throwable ->
+ Log.e(TAG, "Failed to load user profile", throwable)
+ setState {
+ copy(
+ isLoading = false,
+ errorMessage = throwable.toProfileErrorMessage()
+ )
+ }
+ }
+ }
+ }
+
+ private fun Throwable.toProfileErrorMessage(): String? {
+ if (this !is HttpException) return message
+
+ return when (code()) {
+ in 400..499 -> "내잘못"
+ in 500..599 -> "니잘못"
+ else -> message
+ }
+ }
+
+ private suspend fun loadBitmap(imageUrl: String) = withContext(Dispatchers.IO) {
+ URL(imageUrl).openStream().use { inputStream ->
+ BitmapFactory.decodeStream(inputStream)
+ }
+ }
+
+ companion object {
+ private const val TAG = "ProfileViewModel"
+ }
+}
+
+data class ProfileUiState(
+ val isLoading: Boolean = true,
+ val userName: String = "",
+ val profileBitmap: Bitmap? = null,
+ val followingProfiles: List = emptyList(),
+ val errorMessage: String? = null
+) : UiState
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseAllFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseAllFragment.kt
new file mode 100644
index 0000000..40ae89e
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseAllFragment.kt
@@ -0,0 +1,64 @@
+package com.example.umc10th.ui.purchase
+
+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.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.GridLayoutManager
+import com.example.umc10th.databinding.FragmentPurchaseAllBinding
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class PurchaseAllFragment : Fragment() {
+
+ private var _binding: FragmentPurchaseAllBinding? = null
+ private val binding get() = _binding!!
+ private val viewModel: PurchaseViewModel by viewModels()
+
+
+ private lateinit var purchaseProductAdapter: PurchaseProductAdapter
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentPurchaseAllBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ setupRecyclerView()
+ observePurchaseProducts()
+
+ }
+
+ private fun observePurchaseProducts(){
+ viewLifecycleOwner.lifecycleScope.launch{
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.uiState.collect { uiState ->
+ purchaseProductAdapter.submitList(uiState.products)
+ }
+ }
+ }
+ viewModel.loadPurchaseProducts()
+ }
+ private fun setupRecyclerView() {
+ purchaseProductAdapter = PurchaseProductAdapter(emptyList()) { clickedItem ->
+ viewModel.toggleWishlist(clickedItem.id)
+ }
+ binding.purchaseAllRecyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
+ binding.purchaseAllRecyclerView.adapter = purchaseProductAdapter
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseFragment.kt
new file mode 100644
index 0000000..9a83259
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseFragment.kt
@@ -0,0 +1,82 @@
+package com.example.umc10th.ui.purchase
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.navigation.navOptions
+import com.example.umc10th.R
+import com.example.umc10th.databinding.FragmentPurchaseBinding
+import com.google.android.material.tabs.TabLayoutMediator
+
+class PurchaseFragment : Fragment() {
+
+ companion object {
+ private const val DETAIL_OPENED_KEY = "detail_opened"
+ }
+
+ private var _binding: FragmentPurchaseBinding? = null
+ private val binding get() = _binding!!
+ private val args: PurchaseFragmentArgs by navArgs()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentPurchaseBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val adapter = PurchasePagerAdapter(this)
+ binding.purchasePager.adapter = adapter
+
+ TabLayoutMediator(binding.purchaseTabs, binding.purchasePager) { tab, position ->
+ tab.text = when (position) {
+ 0 -> "전체"
+ 1 -> "Tops & T-Shirts"
+ else -> "Sales"
+ }
+ }.attach()
+
+ openProductDetailIfNeeded(savedInstanceState == null)
+ }
+
+ private fun openProductDetailIfNeeded(isFirstCreation: Boolean) {
+ val title = args.title
+ val description = args.description
+ val price = args.price
+ val navController = findNavController()
+ val backStackEntry = navController.currentBackStackEntry ?: return
+ val detailOpened = backStackEntry.savedStateHandle.get(DETAIL_OPENED_KEY) == true
+
+ if (!isFirstCreation || detailOpened || title == null || description == null || price == null) {
+ return
+ }
+
+ backStackEntry.savedStateHandle[DETAIL_OPENED_KEY] = true
+ val navOptions = navOptions {
+ popUpTo(R.id.menu_list_magnifying_glass) {
+ // 구매하기까지 지우고 상품 디테일로 이동
+ inclusive = true
+ }
+ }
+ val action = PurchaseFragmentDirections.actionMenuListMagnifyingGlassToProductDetailFragment(
+ title = title,
+ imageResId = args.imageResId,
+ description = description,
+ price = price
+ )
+ navController.navigate(action, navOptions)
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchasePagerAdapter.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchasePagerAdapter.kt
new file mode 100644
index 0000000..a403ec2
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchasePagerAdapter.kt
@@ -0,0 +1,17 @@
+package com.example.umc10th.ui.purchase
+
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.adapter.FragmentStateAdapter
+
+class PurchasePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
+
+ override fun getItemCount(): Int = 3
+
+ override fun createFragment(position: Int): Fragment {
+ return when (position) {
+ 0 -> PurchaseAllFragment()
+ 1 -> PurchaseTopsFragment()
+ else -> PurchaseSalesFragment()
+ }
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseProductAdapter.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseProductAdapter.kt
new file mode 100644
index 0000000..6510d68
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseProductAdapter.kt
@@ -0,0 +1,62 @@
+package com.example.umc10th.ui.purchase
+
+import android.view.View
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.example.umc10th.R
+import com.example.umc10th.data.model.PurchaseProduct
+import com.example.umc10th.databinding.ItemPurchaseProductBinding
+
+class PurchaseProductAdapter(
+ private var items: List,
+ private val onWishListClick: (PurchaseProduct) -> Unit
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PurchaseProductViewHolder {
+ val binding = ItemPurchaseProductBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return PurchaseProductViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: PurchaseProductViewHolder, position: Int) {
+ holder.bind(items[position])
+ }
+
+ override fun getItemCount(): Int = items.size
+
+ fun submitList(newItems: List){
+ items = newItems
+ notifyDataSetChanged()
+ }
+ inner class PurchaseProductViewHolder(
+ private val binding: ItemPurchaseProductBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(item: PurchaseProduct) {
+ binding.productImage.setImageResource(item.imageResId)
+ binding.productBestSeller.visibility = if (item.isBest) View.VISIBLE else View.GONE
+ binding.productTitle.text = item.title
+ binding.productDescription.text = item.description
+ binding.productPrice.text = item.price
+ updateWishlistIcon(item.isWishlisted)
+
+ binding.wishlistButton.setOnClickListener {
+ onWishListClick(item)
+
+ }
+ }
+
+ private fun updateWishlistIcon(isWishlisted: Boolean) {
+ val iconResId = if (isWishlisted) {
+ R.drawable.img_filled_heart
+ } else {
+ R.drawable.img_blank_heart
+ }
+ binding.wishlistButton.setImageResource(iconResId)
+ }
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseSalesFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseSalesFragment.kt
new file mode 100644
index 0000000..48e918a
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseSalesFragment.kt
@@ -0,0 +1,28 @@
+package com.example.umc10th.ui.purchase
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.example.umc10th.databinding.FragmentPurchaseSalesBinding
+
+class PurchaseSalesFragment : Fragment() {
+
+ private var _binding: FragmentPurchaseSalesBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentPurchaseSalesBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseScreen.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseScreen.kt
new file mode 100644
index 0000000..30218cd
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseScreen.kt
@@ -0,0 +1,250 @@
+package com.example.umc10th.ui.purchase
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRow
+import androidx.compose.material3.TabRowDefaults
+import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.umc10th.R
+import com.example.umc10th.data.model.PurchaseProduct
+import kotlinx.coroutines.launch
+
+@Composable
+fun PurchaseScreen(
+ viewModel: PurchaseViewModel,
+ onProductClick: (PurchaseProduct) -> Unit
+) {
+ var selectedTabIndex by remember { mutableIntStateOf(0) }
+ val uiState by viewModel.uiState.collectAsState()
+ val gridState = rememberLazyGridState()
+ val coroutineScope = rememberCoroutineScope()
+ val showScrollToTop by remember {
+ derivedStateOf {
+ gridState.firstVisibleItemIndex > 3 ||
+ gridState.firstVisibleItemScrollOffset > 600
+ }
+ }
+ val tabs = listOf("전체", "Tops & T-Shirts", "sale")
+
+ LaunchedEffect(Unit) {
+ viewModel.loadPurchaseProducts()
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ ) {
+ TabRow(
+ selectedTabIndex = selectedTabIndex,
+ containerColor = Color.White,
+ contentColor = Color.Black,
+ indicator = { tabPositions ->
+ TabRowDefaults.SecondaryIndicator(
+ modifier = Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]),
+ color = Color.Black
+ )
+ },
+ divider = {
+ HorizontalDivider(color = Color(0xFFEAEAEA), thickness = 1.dp)
+ }
+ ) {
+ tabs.forEachIndexed { index, title ->
+ Tab(
+ selected = selectedTabIndex == index,
+ onClick = { selectedTabIndex = index },
+ text = {
+ Text(
+ text = title,
+ color = if (selectedTabIndex == index) Color.Black else Color(0xFF8A8A8A),
+ fontSize = 14.sp,
+ fontWeight = if (selectedTabIndex == index) {
+ FontWeight.SemiBold
+ } else {
+ FontWeight.Normal
+ }
+ )
+ }
+ )
+ }
+ }
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f)
+ ) {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ state = gridState,
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(
+ start = 16.dp,
+ top = 20.dp,
+ end = 16.dp,
+ bottom = 84.dp
+ ),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ items(
+ items = uiState.products,
+ key = { product -> product.id }
+ ) { product ->
+ PurchaseProductItem(
+ product = product,
+ onClick = { onProductClick(product) },
+ onWishlistClick = { viewModel.toggleWishlist(product.id) }
+ )
+ }
+ }
+
+ if (showScrollToTop) {
+ Button(
+ onClick = {
+ coroutineScope.launch {
+ gridState.animateScrollToItem(0)
+ }
+ },
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .offset(y = (-20).dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.Black,
+ contentColor = Color.White
+ )
+ ) {
+ Text(text = "맨 위로", fontSize = 14.sp)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun PurchaseProductItem(
+ product: PurchaseProduct,
+ onClick: () -> Unit,
+ onWishlistClick: () -> Unit
+) {
+ val imageResId = product.imageResId.takeIf {
+ it == R.drawable.socks1 ||
+ it == R.drawable.socks2 ||
+ it == R.drawable.shoes1 ||
+ it == R.drawable.shoes2
+ } ?: R.drawable.shoes1
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onClick() }
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ ) {
+ Image(
+ painter = painterResource(id = imageResId),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ Image(
+ painter = painterResource(
+ id = if (product.isWishlisted) {
+ R.drawable.img_filled_heart
+ } else {
+ R.drawable.img_blank_heart
+ }
+ ),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 6.dp, end = 6.dp)
+ .size(34.dp)
+ .align(androidx.compose.ui.Alignment.TopEnd)
+ .clickable { onWishlistClick() }
+ )
+ }
+
+ if (product.isBest) {
+ Text(
+ text = "BestSeller",
+ color = Color(0xFFFC5100),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top = 12.dp)
+ )
+ }
+
+ Text(
+ text = product.title,
+ color = Color(0xFF111111),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(top = if (product.isBest) 4.dp else 12.dp)
+ )
+ Text(
+ text = product.description,
+ color = Color(0xFF767676),
+ fontSize = 13.sp,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(top = 6.dp)
+ )
+ Text(
+ text = product.colornum,
+ color = Color(0xFF767676),
+ fontSize = 13.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = product.price,
+ color = Color(0xFF111111),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseTopsFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseTopsFragment.kt
new file mode 100644
index 0000000..7cd58e9
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseTopsFragment.kt
@@ -0,0 +1,28 @@
+package com.example.umc10th.ui.purchase
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.example.umc10th.databinding.FragmentPurchaseTopsBinding
+
+class PurchaseTopsFragment : Fragment() {
+
+ private var _binding: FragmentPurchaseTopsBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentPurchaseTopsBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseViewModel.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseViewModel.kt
new file mode 100644
index 0000000..86d8a80
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/purchase/PurchaseViewModel.kt
@@ -0,0 +1,39 @@
+package com.example.umc10th.ui.purchase
+
+import androidx.lifecycle.viewModelScope
+import com.example.umc10th.data.model.PurchaseProduct
+import com.example.umc10th.data.repository.ProductRepository
+import com.example.umc10th.ui.base.BaseViewModel
+import com.example.umc10th.ui.base.UiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class PurchaseViewModel @Inject constructor(
+ private val productRepository: ProductRepository
+) : BaseViewModel(PurchaseUiState()) {
+ private var hasStartedLoading = false
+
+ fun loadPurchaseProducts() {
+ if (hasStartedLoading) return
+ hasStartedLoading = true
+
+ viewModelScope.launch {
+ productRepository.initializePurchaseProducts()
+ productRepository.getPurchaseProducts().collect { products ->
+ setState { copy(products = products) }
+ }
+ }
+ }
+
+ fun toggleWishlist(productId: Int) {
+ viewModelScope.launch {
+ productRepository.toggleWishlist(productId)
+ }
+ }
+}
+
+data class PurchaseUiState(
+ val products: List = emptyList()
+) : UiState
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/splash/SplashActivity.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/splash/SplashActivity.kt
new file mode 100644
index 0000000..d3f8a91
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/splash/SplashActivity.kt
@@ -0,0 +1,32 @@
+package com.example.umc10th.ui.splash
+
+import android.content.Intent
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import com.example.umc10th.databinding.ActivitySplashBinding
+import com.example.umc10th.ui.main.MainActivity
+
+class SplashActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivitySplashBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ binding = ActivitySplashBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ val message = binding.splashText.text.toString()
+ Handler(Looper.getMainLooper()).postDelayed({
+ val intent = Intent(this, MainActivity::class.java).apply {
+ putExtra(MainActivity.EXTRA_HOME_TITLE, message)
+ }
+ startActivity(intent)
+ finish()
+ }, 3000L)
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistFragment.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistFragment.kt
new file mode 100644
index 0000000..9491e5b
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistFragment.kt
@@ -0,0 +1,65 @@
+package com.example.umc10th.ui.wishlist
+
+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.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.GridLayoutManager
+import com.example.umc10th.databinding.FragmentWishlistBinding
+import com.example.umc10th.ui.purchase.PurchaseProductAdapter
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class WishlistFragment : Fragment() {
+
+ private var _binding: FragmentWishlistBinding? = null
+ private val binding get() = _binding!!
+ private val viewModel: WishlistViewModel by viewModels()
+
+ private lateinit var wishListAdapter: PurchaseProductAdapter
+
+ 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)
+ setupRecyclerView()
+ observeWishListProducts()
+ }
+
+ private fun observeWishListProducts(){
+ viewLifecycleOwner.lifecycleScope.launch{
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.uiState.collect { uiState ->
+ wishListAdapter.submitList(uiState.products)
+ }
+ }
+ }
+ viewModel.loadWishlistProducts()
+ }
+ private fun setupRecyclerView() {
+ wishListAdapter = PurchaseProductAdapter(emptyList()) { clickedItem ->
+ viewModel.toggleWishlist(clickedItem.id)
+ }
+
+ binding.wishlistRecyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
+ binding.wishlistRecyclerView.adapter = wishListAdapter
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistProductAdapter.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistProductAdapter.kt
new file mode 100644
index 0000000..5d2868c
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistProductAdapter.kt
@@ -0,0 +1,60 @@
+package com.example.umc10th.ui.wishlist
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.example.umc10th.data.model.PurchaseProduct
+import com.example.umc10th.databinding.ItemWishlistProductBinding
+
+class WishlistProductAdapter(
+ private var items: List,
+ private val onItemClick: (PurchaseProduct) -> Unit
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WishlistProductViewHolder {
+ val binding = ItemWishlistProductBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ return WishlistProductViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: WishlistProductViewHolder, position: Int) {
+ holder.bind(items[position])
+ }
+
+ override fun getItemCount(): Int = items.size
+
+ fun submitList(newItems: List){
+ items = newItems
+ notifyDataSetChanged()
+ }
+
+ class WishlistProductViewHolder(
+ private val binding: ItemWishlistProductBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(item: PurchaseProduct) {
+ binding.productImage.setImageResource(item.imageResId)
+ binding.productTitle.text = item.title
+ binding.productDescription.text = item.description
+ binding.productPrice.text = item.price
+ }
+ }
+
+ override fun onViewAttachedToWindow(holder: WishlistProductViewHolder) {
+ super.onViewAttachedToWindow(holder)
+ holder.itemView.setOnClickListener {
+ val position = holder.bindingAdapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ onItemClick(items[position])
+ }
+ }
+ }
+
+ override fun onViewDetachedFromWindow(holder: WishlistProductViewHolder) {
+ holder.itemView.setOnClickListener(null)
+ super.onViewDetachedFromWindow(holder)
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistScreen.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistScreen.kt
new file mode 100644
index 0000000..6e526db
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistScreen.kt
@@ -0,0 +1,171 @@
+package com.example.umc10th.ui.wishlist
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.example.umc10th.R
+import com.example.umc10th.data.model.PurchaseProduct
+
+@Composable
+fun WishlistScreen(
+ viewModel: WishlistViewModel,
+ onProductClick: (PurchaseProduct) -> Unit
+) {
+ val uiState by viewModel.uiState.collectAsState()
+
+ LaunchedEffect(Unit) {
+ viewModel.loadWishlistProducts()
+ }
+
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ ) {
+ Text(
+ text = "위시리스트",
+ color = Color(0xFF111111),
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(start = 24.dp, top = 44.dp)
+ )
+
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(2),
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ contentPadding = PaddingValues(
+ start = 12.dp,
+ top = 24.dp,
+ end = 12.dp,
+ bottom = 24.dp
+ ),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp)
+ ) {
+ items(
+ items = uiState.products,
+ key = { product -> product.id }
+ ) { product ->
+ WishlistProductItem(
+ product = product,
+ onClick = { onProductClick(product) },
+ onWishlistClick = { viewModel.toggleWishlist(product.id) }
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun WishlistProductItem(
+ product: PurchaseProduct,
+ onClick: () -> Unit,
+ onWishlistClick: () -> Unit
+) {
+ val imageResId = product.imageResId.takeIf {
+ it == R.drawable.socks1 ||
+ it == R.drawable.socks2 ||
+ it == R.drawable.shoes1 ||
+ it == R.drawable.shoes2
+ } ?: R.drawable.shoes1
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable { onClick() }
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(1f)
+ ) {
+ Image(
+ painter = painterResource(id = imageResId),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize(),
+ contentScale = ContentScale.Crop
+ )
+ Image(
+ painter = painterResource(id = R.drawable.img_filled_heart),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = 6.dp, end = 6.dp)
+ .size(34.dp)
+ .align(Alignment.TopEnd)
+ .clickable {
+ onWishlistClick()
+ }
+ )
+ }
+
+ if (product.isBest) {
+ Text(
+ text = "BestSeller",
+ color = Color(0xFFFC5100),
+ fontSize = 12.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top = 12.dp)
+ )
+ }
+
+ Text(
+ text = product.title,
+ color = Color(0xFF111111),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(top = if (product.isBest) 4.dp else 12.dp)
+ )
+ Text(
+ text = product.description,
+ color = Color(0xFF767676),
+ fontSize = 13.sp,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(top = 6.dp)
+ )
+ Text(
+ text = product.colornum,
+ color = Color(0xFF767676),
+ fontSize = 13.sp,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = product.price,
+ color = Color(0xFF111111),
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ }
+}
diff --git a/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistViewModel.kt b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistViewModel.kt
new file mode 100644
index 0000000..a180481
--- /dev/null
+++ b/Week08/OneStone/app/src/main/java/com/example/umc10th/ui/wishlist/WishlistViewModel.kt
@@ -0,0 +1,39 @@
+package com.example.umc10th.ui.wishlist
+
+import androidx.lifecycle.viewModelScope
+import com.example.umc10th.data.model.PurchaseProduct
+import com.example.umc10th.data.repository.ProductRepository
+import com.example.umc10th.ui.base.BaseViewModel
+import com.example.umc10th.ui.base.UiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class WishlistViewModel @Inject constructor(
+ private val productRepository: ProductRepository
+) : BaseViewModel(WishlistUiState()) {
+ private var hasStartedLoading = false
+
+ fun loadWishlistProducts() {
+ if (hasStartedLoading) return
+ hasStartedLoading = true
+
+ viewModelScope.launch {
+ productRepository.initializePurchaseProducts()
+ productRepository.getPurchaseProducts().collect { products ->
+ setState { copy(products = products.filter { it.isWishlisted }) }
+ }
+ }
+ }
+
+ fun toggleWishlist(productId: Int) {
+ viewModelScope.launch {
+ productRepository.toggleWishlist(productId)
+ }
+ }
+}
+
+data class WishlistUiState(
+ val products: List = emptyList()
+) : UiState
diff --git a/Week08/OneStone/app/src/main/res/color/bottom_nav_item.xml b/Week08/OneStone/app/src/main/res/color/bottom_nav_item.xml
new file mode 100644
index 0000000..887754f
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/color/bottom_nav_item.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/caretdown.xml b/Week08/OneStone/app/src/main/res/drawable/caretdown.xml
new file mode 100644
index 0000000..2df0c32
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/caretdown.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/heartstraight.xml b/Week08/OneStone/app/src/main/res/drawable/heartstraight.xml
new file mode 100644
index 0000000..c67340f
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/heartstraight.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/home_logo.png b/Week08/OneStone/app/src/main/res/drawable/home_logo.png
new file mode 100644
index 0000000..f42aa9d
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/drawable/home_logo.png differ
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_arrow_closed.xml b/Week08/OneStone/app/src/main/res/drawable/ic_arrow_closed.xml
new file mode 100644
index 0000000..34f9759
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_arrow_closed.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_bag_circle.xml b/Week08/OneStone/app/src/main/res/drawable/ic_bag_circle.xml
new file mode 100644
index 0000000..8a14160
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_bag_circle.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_bag_simple.xml b/Week08/OneStone/app/src/main/res/drawable/ic_bag_simple.xml
new file mode 100644
index 0000000..cbe132a
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_bag_simple.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_calendar.xml b/Week08/OneStone/app/src/main/res/drawable/ic_calendar.xml
new file mode 100644
index 0000000..233ed65
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_calendar.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_heart_straight.xml b/Week08/OneStone/app/src/main/res/drawable/ic_heart_straight.xml
new file mode 100644
index 0000000..4fabb40
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_heart_straight.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_house_simple.xml b/Week08/OneStone/app/src/main/res/drawable/ic_house_simple.xml
new file mode 100644
index 0000000..64c8240
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_house_simple.xml
@@ -0,0 +1,14 @@
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_launcher_background.xml b/Week08/OneStone/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_launcher_foreground.xml b/Week08/OneStone/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_list_magnifying_glass.xml b/Week08/OneStone/app/src/main/res/drawable/ic_list_magnifying_glass.xml
new file mode 100644
index 0000000..38de4b7
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_list_magnifying_glass.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_order.xml b/Week08/OneStone/app/src/main/res/drawable/ic_order.xml
new file mode 100644
index 0000000..d1e92fd
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_order.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_pass.xml b/Week08/OneStone/app/src/main/res/drawable/ic_pass.xml
new file mode 100644
index 0000000..6fe2cba
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_pass.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_setting.xml b/Week08/OneStone/app/src/main/res/drawable/ic_setting.xml
new file mode 100644
index 0000000..47531ec
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_setting.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/ic_user.xml b/Week08/OneStone/app/src/main/res/drawable/ic_user.xml
new file mode 100644
index 0000000..c8eccd7
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/ic_user.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/icon_back.xml b/Week08/OneStone/app/src/main/res/drawable/icon_back.xml
new file mode 100644
index 0000000..af62026
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/icon_back.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/img_blank_heart.png b/Week08/OneStone/app/src/main/res/drawable/img_blank_heart.png
new file mode 100644
index 0000000..ca7108f
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/drawable/img_blank_heart.png differ
diff --git a/Week08/OneStone/app/src/main/res/drawable/img_filled_heart.png b/Week08/OneStone/app/src/main/res/drawable/img_filled_heart.png
new file mode 100644
index 0000000..49cb87c
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/drawable/img_filled_heart.png differ
diff --git a/Week08/OneStone/app/src/main/res/drawable/magnifyingglass.xml b/Week08/OneStone/app/src/main/res/drawable/magnifyingglass.xml
new file mode 100644
index 0000000..9077a42
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/drawable/magnifyingglass.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/drawable/shoes1.png b/Week08/OneStone/app/src/main/res/drawable/shoes1.png
new file mode 100644
index 0000000..286ad63
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/drawable/shoes1.png differ
diff --git a/Week08/OneStone/app/src/main/res/drawable/shoes2.png b/Week08/OneStone/app/src/main/res/drawable/shoes2.png
new file mode 100644
index 0000000..3bf50b1
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/drawable/shoes2.png differ
diff --git a/Week08/OneStone/app/src/main/res/drawable/socks1.png b/Week08/OneStone/app/src/main/res/drawable/socks1.png
new file mode 100644
index 0000000..18a863f
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/drawable/socks1.png differ
diff --git a/Week08/OneStone/app/src/main/res/drawable/socks2.png b/Week08/OneStone/app/src/main/res/drawable/socks2.png
new file mode 100644
index 0000000..22db145
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/drawable/socks2.png differ
diff --git a/Week08/OneStone/app/src/main/res/layout/activity_main.xml b/Week08/OneStone/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..4c9b653
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/activity_splash.xml b/Week08/OneStone/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..b5b4ec9
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_cart.xml b/Week08/OneStone/app/src/main/res/layout/fragment_cart.xml
new file mode 100644
index 0000000..77c2e60
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_cart.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_home.xml b/Week08/OneStone/app/src/main/res/layout/fragment_home.xml
new file mode 100644
index 0000000..a51beb7
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_home.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_product_detail.xml b/Week08/OneStone/app/src/main/res/layout/fragment_product_detail.xml
new file mode 100644
index 0000000..6648a42
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_product_detail.xml
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_profile.xml b/Week08/OneStone/app/src/main/res/layout/fragment_profile.xml
new file mode 100644
index 0000000..63bed9e
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_profile.xml
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_purchase.xml b/Week08/OneStone/app/src/main/res/layout/fragment_purchase.xml
new file mode 100644
index 0000000..4ac452a
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_purchase.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_purchase_all.xml b/Week08/OneStone/app/src/main/res/layout/fragment_purchase_all.xml
new file mode 100644
index 0000000..b5c5134
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_purchase_all.xml
@@ -0,0 +1,11 @@
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_purchase_sales.xml b/Week08/OneStone/app/src/main/res/layout/fragment_purchase_sales.xml
new file mode 100644
index 0000000..caa57b3
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_purchase_sales.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_purchase_tops.xml b/Week08/OneStone/app/src/main/res/layout/fragment_purchase_tops.xml
new file mode 100644
index 0000000..c7e2c7d
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_purchase_tops.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/fragment_wishlist.xml b/Week08/OneStone/app/src/main/res/layout/fragment_wishlist.xml
new file mode 100644
index 0000000..07c6890
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/fragment_wishlist.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/item_following_profile.xml b/Week08/OneStone/app/src/main/res/layout/item_following_profile.xml
new file mode 100644
index 0000000..a91378a
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/item_following_profile.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/item_loading_product.xml b/Week08/OneStone/app/src/main/res/layout/item_loading_product.xml
new file mode 100644
index 0000000..ab4c3ea
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/item_loading_product.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/item_product.xml b/Week08/OneStone/app/src/main/res/layout/item_product.xml
new file mode 100644
index 0000000..73b3316
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/item_product.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/item_purchase_product.xml b/Week08/OneStone/app/src/main/res/layout/item_purchase_product.xml
new file mode 100644
index 0000000..42fa075
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/item_purchase_product.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/layout/item_wishlist_product.xml b/Week08/OneStone/app/src/main/res/layout/item_wishlist_product.xml
new file mode 100644
index 0000000..e8243e3
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/layout/item_wishlist_product.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/menu/menu_bottom_nav.xml b/Week08/OneStone/app/src/main/res/menu/menu_bottom_nav.xml
new file mode 100644
index 0000000..4522a63
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/menu/menu_bottom_nav.xml
@@ -0,0 +1,23 @@
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/Week08/OneStone/app/src/main/res/mipmap-anydpi/ic_launcher.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/mipmap-anydpi/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/Week08/OneStone/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
new file mode 100644
index 0000000..6f3b755
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Week08/OneStone/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Week08/OneStone/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Week08/OneStone/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Week08/OneStone/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Week08/OneStone/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Week08/OneStone/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Week08/OneStone/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Week08/OneStone/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Week08/OneStone/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/Week08/OneStone/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Week08/OneStone/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/Week08/OneStone/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/Week08/OneStone/app/src/main/res/navigation/nav_graph.xml b/Week08/OneStone/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..aee43d2
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Week08/OneStone/app/src/main/res/values-night/themes.xml b/Week08/OneStone/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..aeaa294
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/values/colors.xml b/Week08/OneStone/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..82ba1de
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FF000000
+ #FFFFFFFF
+ #FFEFB6
+ #CEE7F5
+ #BEC3ED
+ #B1D3B9
+ #EB8B8B
+
diff --git a/Week08/OneStone/app/src/main/res/values/strings.xml b/Week08/OneStone/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6674a85
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ UMC10th
+ BestSeller
+
diff --git a/Week08/OneStone/app/src/main/res/values/themes.xml b/Week08/OneStone/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..730be2c
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/xml/backup_rules.xml b/Week08/OneStone/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..4df9255
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/main/res/xml/data_extraction_rules.xml b/Week08/OneStone/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..9ee9997
--- /dev/null
+++ b/Week08/OneStone/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Week08/OneStone/app/src/test/java/com/example/umc10th/ExampleUnitTest.kt b/Week08/OneStone/app/src/test/java/com/example/umc10th/ExampleUnitTest.kt
new file mode 100644
index 0000000..f11f122
--- /dev/null
+++ b/Week08/OneStone/app/src/test/java/com/example/umc10th/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.example.umc10th
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/Week08/OneStone/build.gradle.kts b/Week08/OneStone/build.gradle.kts
new file mode 100644
index 0000000..de94608
--- /dev/null
+++ b/Week08/OneStone/build.gradle.kts
@@ -0,0 +1,9 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.androidx.navigation.safeargs) apply false
+ alias(libs.plugins.legacy.kapt) apply false
+ alias(libs.plugins.hilt.android) apply false
+ alias(libs.plugins.kotlin.compose) apply false
+
+}
diff --git a/Week08/OneStone/gradle.properties b/Week08/OneStone/gradle.properties
new file mode 100644
index 0000000..34c5e9e
--- /dev/null
+++ b/Week08/OneStone/gradle.properties
@@ -0,0 +1,15 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/Week08/OneStone/gradle/gradle-daemon-jvm.properties b/Week08/OneStone/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 0000000..6c1139e
--- /dev/null
+++ b/Week08/OneStone/gradle/gradle-daemon-jvm.properties
@@ -0,0 +1,12 @@
+#This file is generated by updateDaemonJvm
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect
+toolchainVersion=21
diff --git a/Week08/OneStone/gradle/libs.versions.toml b/Week08/OneStone/gradle/libs.versions.toml
new file mode 100644
index 0000000..e11ff00
--- /dev/null
+++ b/Week08/OneStone/gradle/libs.versions.toml
@@ -0,0 +1,47 @@
+[versions]
+agp = "9.1.0"
+coreKtx = "1.18.0"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+appcompat = "1.7.1"
+material = "1.13.0"
+activity = "1.13.0"
+constraintlayout = "2.2.1"
+navigation = "2.9.7"
+kotlin = "2.0.0"
+hilt = "2.59.2"
+composeBom = "2024.12.01"
+uiToolingPreview = "1.11.1"
+uiTooling = "1.11.1"
+
+[libraries]
+androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
+androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
+hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
+hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
+androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
+androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity" }
+androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
+androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
+androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
+androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
+androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
+androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "uiToolingPreview" }
+androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "uiTooling" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+androidx-navigation-safeargs = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" }
+legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" }
+hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
diff --git a/Week08/OneStone/gradle/wrapper/gradle-wrapper.jar b/Week08/OneStone/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/Week08/OneStone/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Week08/OneStone/gradle/wrapper/gradle-wrapper.properties b/Week08/OneStone/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..f5ed0b2
--- /dev/null
+++ b/Week08/OneStone/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,9 @@
+#Thu Mar 12 16:12:26 KST 2026
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/Week08/OneStone/gradlew b/Week08/OneStone/gradlew
new file mode 100644
index 0000000..ef07e01
--- /dev/null
+++ b/Week08/OneStone/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/Week08/OneStone/gradlew.bat b/Week08/OneStone/gradlew.bat
new file mode 100644
index 0000000..db3a6ac
--- /dev/null
+++ b/Week08/OneStone/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Week08/OneStone/settings.gradle.kts b/Week08/OneStone/settings.gradle.kts
new file mode 100644
index 0000000..89c9fba
--- /dev/null
+++ b/Week08/OneStone/settings.gradle.kts
@@ -0,0 +1,27 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "UMC10th"
+include(":app")
+
\ No newline at end of file