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 @@ + + + + +