diff --git a/Week08/Deku/.gitignore b/Week08/Deku/.gitignore new file mode 100644 index 0000000..841dd0e --- /dev/null +++ b/Week08/Deku/.gitignore @@ -0,0 +1,22 @@ +*.iml +.gradle +.kotlin/ +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +*.apk +*.ap_ +*.aab +*.jks +*.keystore +*.hprof diff --git a/Week08/Deku/.idea/.gitignore b/Week08/Deku/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/Week08/Deku/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/Week08/Deku/.idea/AndroidProjectSystem.xml b/Week08/Deku/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/Week08/Deku/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/Week08/Deku/.idea/compiler.xml b/Week08/Deku/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/Week08/Deku/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Week08/Deku/.idea/deploymentTargetSelector.xml b/Week08/Deku/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..ca16a99 --- /dev/null +++ b/Week08/Deku/.idea/deploymentTargetSelector.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/Week08/Deku/.idea/gradle.xml b/Week08/Deku/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/Week08/Deku/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/Week08/Deku/.idea/inspectionProfiles/Project_Default.xml b/Week08/Deku/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..7061a0d --- /dev/null +++ b/Week08/Deku/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,61 @@ + + + + \ No newline at end of file diff --git a/Week08/Deku/.idea/migrations.xml b/Week08/Deku/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/Week08/Deku/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/Week08/Deku/.idea/misc.xml b/Week08/Deku/.idea/misc.xml new file mode 100644 index 0000000..1a1bf72 --- /dev/null +++ b/Week08/Deku/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/Week08/Deku/.idea/runConfigurations.xml b/Week08/Deku/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/Week08/Deku/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/Week08/Deku/.idea/vcs.xml b/Week08/Deku/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/Week08/Deku/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Week08/Deku/app/.gitignore b/Week08/Deku/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/Week08/Deku/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Week08/Deku/app/build.gradle.kts b/Week08/Deku/app/build.gradle.kts new file mode 100644 index 0000000..84404e5 --- /dev/null +++ b/Week08/Deku/app/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) +} + +android { + namespace = "com.example.deku" + compileSdk { + version = release(36) + } + + defaultConfig { + applicationId = "com.example.deku" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } +} + +dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.navigation.compose) + implementation(libs.kotlinx.serialization.json) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) +} diff --git a/Week08/Deku/app/proguard-rules.pro b/Week08/Deku/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/Week08/Deku/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/Deku/app/src/androidTest/java/com/example/deku/ExampleInstrumentedTest.kt b/Week08/Deku/app/src/androidTest/java/com/example/deku/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..05aaa19 --- /dev/null +++ b/Week08/Deku/app/src/androidTest/java/com/example/deku/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.deku + +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.deku", appContext.packageName) + } +} \ No newline at end of file diff --git a/Week08/Deku/app/src/main/AndroidManifest.xml b/Week08/Deku/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..340e1cf --- /dev/null +++ b/Week08/Deku/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/Week08/Deku/app/src/main/java/com/example/deku/MainActivity.kt b/Week08/Deku/app/src/main/java/com/example/deku/MainActivity.kt new file mode 100644 index 0000000..8d92d93 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/MainActivity.kt @@ -0,0 +1,26 @@ +package com.example.deku + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.example.deku.core.common.DEFAULT_HOME_TITLE +import com.example.deku.core.common.EXTRA_HOME_TITLE +import com.example.deku.core.designsystem.theme.DekuTheme +import com.example.deku.feature.main.MainScreen + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + // Splash에서 전달한 title을 HomeScreen까지 내려보내는 7주차 시니어 미션 흐름입니다. + val homeTitle = intent.getStringExtra(EXTRA_HOME_TITLE) ?: DEFAULT_HOME_TITLE + + setContent { + DekuTheme { + MainScreen(homeTitle = homeTitle) + } + } + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/SplashActivity.kt b/Week08/Deku/app/src/main/java/com/example/deku/SplashActivity.kt new file mode 100644 index 0000000..b3b32ef --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/SplashActivity.kt @@ -0,0 +1,33 @@ +package com.example.deku + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import com.example.deku.core.common.EXTRA_HOME_TITLE +import com.example.deku.core.common.SPLASH_HOME_TITLE +import com.example.deku.core.designsystem.theme.DekuTheme +import com.example.deku.feature.splash.SplashScreen + +class SplashActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + + setContent { + DekuTheme { + SplashScreen( + onTimeout = { + val intent = Intent(this, MainActivity::class.java).apply { + // Activity 간 데이터 전달: MainActivity가 이 값을 읽어 Home route의 인자로 사용합니다. + putExtra(EXTRA_HOME_TITLE, SPLASH_HOME_TITLE) + } + startActivity(intent) + finish() + } + ) + } + } + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/core/common/AppConstants.kt b/Week08/Deku/app/src/main/java/com/example/deku/core/common/AppConstants.kt new file mode 100644 index 0000000..411a738 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/core/common/AppConstants.kt @@ -0,0 +1,6 @@ +package com.example.deku.core.common + +const val EXTRA_HOME_TITLE = "com.example.deku.extra.HOME_TITLE" +const val SPLASH_HOME_TITLE = "NIKE" + +internal const val DEFAULT_HOME_TITLE = "Discover" diff --git a/Week08/Deku/app/src/main/java/com/example/deku/core/component/MainBottomBar.kt b/Week08/Deku/app/src/main/java/com/example/deku/core/component/MainBottomBar.kt new file mode 100644 index 0000000..cd1c862 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/core/component/MainBottomBar.kt @@ -0,0 +1,109 @@ +package com.example.deku.core.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.deku.R +import com.example.deku.core.designsystem.ColorDivider +import com.example.deku.core.designsystem.ColorNavUnselected +import com.example.deku.core.designsystem.ColorTextPrimary +import com.example.deku.navigation.MainRouteName + +private data class BottomTabItem( + val label: String, + @param:DrawableRes val iconRes: Int, + val routeName: String +) + +private val bottomTabs = listOf( + BottomTabItem("홈", R.drawable.home, MainRouteName.HOME), + BottomTabItem("구매하기", R.drawable.shop, MainRouteName.SHOP), + BottomTabItem("위시리스트", R.drawable.heart, MainRouteName.WISH_LIST), + BottomTabItem("장바구니", R.drawable.cart, MainRouteName.CART), + BottomTabItem("프로필", R.drawable.profile, MainRouteName.PROFILE) +) + +@Composable +fun MainBottomBar( + currentRoute: String?, + onTabSelected: (String) -> Unit +) { + Surface( + color = Color.White, + shadowElevation = 0.dp + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(Color.White) + .navigationBarsPadding() + ) { + HorizontalDivider(color = ColorDivider, thickness = 1.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + .padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + bottomTabs.forEach { item -> + val selected = currentRoute == item.routeName + val color = if (selected) ColorTextPrimary else ColorNavUnselected + + Column( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(RoundedCornerShape(8.dp)) + .clickable { onTabSelected(item.routeName) } + .padding(vertical = 6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = item.iconRes), + contentDescription = item.label, + tint = color, + modifier = Modifier.size(29.dp) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = item.label, + color = color, + fontSize = 10.sp, + lineHeight = 12.sp, + fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal, + maxLines = 1, + textAlign = TextAlign.Center + ) + } + } + } + } + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/AppColors.kt b/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/AppColors.kt new file mode 100644 index 0000000..0be9a1b --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/AppColors.kt @@ -0,0 +1,9 @@ +package com.example.deku.core.designsystem + +import androidx.compose.ui.graphics.Color + +val ColorTextPrimary = Color(0xFF111111) +val ColorTextSecondary = Color(0xFF5F5F5F) +val ColorNavUnselected = Color(0xFF8A8A8A) +val ColorFrameBackground = Color(0xFFF7F7F7) +val ColorDivider = Color(0x1F203126) diff --git a/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Color.kt b/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Color.kt new file mode 100644 index 0000000..95167c1 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Color.kt @@ -0,0 +1,9 @@ +package com.example.deku.core.designsystem.theme + +import androidx.compose.ui.graphics.Color + +val NikeBlack = Color(0xFF111111) +val NikeWhite = Color(0xFFFFFFFF) +val NikeGray = Color(0xFF8A8A8A) +val NikeDarkGray = Color(0xFF2C2C2C) +val NikeFrameBackground = Color(0xFFF7F7F7) diff --git a/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Theme.kt b/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Theme.kt new file mode 100644 index 0000000..b5e8e25 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Theme.kt @@ -0,0 +1,61 @@ +package com.example.deku.core.designsystem.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = NikeWhite, + secondary = NikeGray, + tertiary = NikeFrameBackground, + background = NikeBlack, + surface = NikeBlack, + onPrimary = NikeBlack, + onSecondary = NikeWhite, + onTertiary = NikeBlack, + onBackground = NikeWhite, + onSurface = NikeWhite +) + +private val LightColorScheme = lightColorScheme( + primary = NikeBlack, + secondary = NikeDarkGray, + tertiary = NikeFrameBackground, + background = NikeWhite, + surface = NikeWhite, + onPrimary = NikeWhite, + onSecondary = NikeWhite, + onTertiary = NikeBlack, + onBackground = NikeBlack, + onSurface = NikeBlack +) + +@Composable +fun DekuTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = false, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Type.kt b/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Type.kt new file mode 100644 index 0000000..4e31103 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/core/designsystem/theme/Type.kt @@ -0,0 +1,19 @@ +package com.example.deku.core.designsystem.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + +) diff --git a/Week08/Deku/app/src/main/java/com/example/deku/data/ProductCatalog.kt b/Week08/Deku/app/src/main/java/com/example/deku/data/ProductCatalog.kt new file mode 100644 index 0000000..b70d298 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/data/ProductCatalog.kt @@ -0,0 +1,102 @@ +package com.example.deku.data + +import com.example.deku.R + +object ProductCatalog { + private val baseProducts = listOf( + ProductItem( + id = 1, + name = "Air Jordan XXXVI", + price = "US185", + imageResId = R.drawable.nike_item1, + category = CATEGORY_SHOES, + colorCount = 3, + description = PRODUCT_DESCRIPTION + ), + ProductItem( + id = 2, + name = "Nike Air Force 1 '07", + price = "US185", + imageResId = R.drawable.nike_item2, + category = CATEGORY_SHOES, + colorCount = 5, + description = PRODUCT_DESCRIPTION + ), + ProductItem( + id = 3, + name = "Nike Everyday Plus Cushioned", + price = "US185", + imageResId = R.drawable.nike_item3, + category = CATEGORY_SHOES, + colorCount = 2, + description = PRODUCT_DESCRIPTION + ), + ProductItem( + id = 4, + name = "Nike Dri-FIT Primary Top", + price = "US185", + imageResId = R.drawable.nike_item4, + category = CATEGORY_TOPS, + colorCount = 4, + description = PRODUCT_DESCRIPTION + ), + ProductItem( + id = 5, + name = "Nike Everyday Plus Cushioned", + price = "US185", + imageResId = R.drawable.nike_item5, + category = CATEGORY_SHOES, + colorCount = 4, + description = PRODUCT_DESCRIPTION + ), + ProductItem( + id = 6, + name = "Nike Everyday Plus Cushioned", + price = "US185", + imageResId = R.drawable.nike_item6, + category = CATEGORY_SHOES, + colorCount = 4, + description = PRODUCT_DESCRIPTION + ) + ) + + fun initialProducts(): List = buildList { + repeat(DUMMY_PRODUCT_GROUP_COUNT) { groupIndex -> + baseProducts.forEach { product -> + val nextId = groupIndex * baseProducts.size + product.id + val displayName = if (groupIndex == 0) { + product.name + } else { + "${product.name} ${groupIndex + 1}" + } + + add( + product.copy( + id = nextId, + name = displayName + ) + ) + } + } + } + + fun latestProducts(products: List): List = products.take(5) + + fun productsByCategory( + products: List, + category: String? + ): List { + return if (category == null) { + products + } else { + products.filter { it.category == category } + } + } + + const val CATEGORY_TOPS = "Tops & T-Shirts" + const val CATEGORY_SHOES = "Shoes" + + private const val DUMMY_PRODUCT_GROUP_COUNT = 6 + private const val PRODUCT_DESCRIPTION = + "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." +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/data/ProductItem.kt b/Week08/Deku/app/src/main/java/com/example/deku/data/ProductItem.kt new file mode 100644 index 0000000..16b61c3 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/data/ProductItem.kt @@ -0,0 +1,14 @@ +package com.example.deku.data + +import androidx.annotation.DrawableRes + +data class ProductItem( + val id: Int, + val name: String, + val price: String, + @param:DrawableRes val imageResId: Int, + val category: String, + val colorCount: Int, + val description: String, + val isWish: Boolean = false +) diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/cart/CartScreen.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/cart/CartScreen.kt new file mode 100644 index 0000000..51b7e0f --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/cart/CartScreen.kt @@ -0,0 +1,101 @@ +package com.example.deku.feature.cart + +import androidx.compose.foundation.background +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.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.deku.R +import com.example.deku.core.designsystem.ColorFrameBackground +import com.example.deku.core.designsystem.ColorNavUnselected +import com.example.deku.core.designsystem.ColorTextPrimary +import com.example.deku.core.designsystem.theme.DekuTheme + +@Composable +fun CartScreen( + onOrderClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 24.dp) + .padding(top = 40.dp) + ) { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Surface( + modifier = Modifier.size(70.dp), + shape = CircleShape, + color = ColorFrameBackground + ) { + Icon( + painter = painterResource(id = R.drawable.cart), + contentDescription = null, + tint = ColorNavUnselected, + modifier = Modifier.padding(17.dp) + ) + } + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = "장바구니가 비어있습니다\n제품을 추가하면 여기에 표시됩니다.", + color = ColorTextPrimary, + textAlign = TextAlign.Center, + fontSize = 16.sp, + lineHeight = 24.sp + ) + } + + Button( + onClick = onOrderClick, + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, bottom = 8.dp) + .height(52.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ), + shape = RoundedCornerShape(26.dp) + ) { + Text( + text = "주문하기", + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CartScreenPreview() { + DekuTheme { + CartScreen(onOrderClick = {}) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/home/HomeScreen.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/home/HomeScreen.kt new file mode 100644 index 0000000..d761a42 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/home/HomeScreen.kt @@ -0,0 +1,210 @@ +package com.example.deku.feature.home + +import android.app.Activity +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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 +import com.example.deku.R +import com.example.deku.core.common.SPLASH_HOME_TITLE +import com.example.deku.core.designsystem.ColorTextPrimary +import com.example.deku.core.designsystem.ColorTextSecondary +import com.example.deku.core.designsystem.theme.DekuTheme +import com.example.deku.data.ProductCatalog +import com.example.deku.data.ProductItem +import com.example.deku.feature.product.HomeProductCard +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlinx.coroutines.launch + +private const val BACK_PRESS_INTERVAL_MILLIS = 2_000L + +@Composable +fun HomeScreen( + title: String, + products: List, + onProductClick: (ProductItem) -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val activity = context as? Activity + var lastBackPressedAt by remember { mutableStateOf(0L) } + val today = remember { currentKoreanDate() } + val listState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() + // 스크롤 값은 자주 바뀌므로 derivedStateOf로 버튼 노출 조건이 변할 때만 재구성합니다. + val showScrollToTop by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 2 + } + } + + // Compose에서는 BackHandler로 시스템 뒤로가기를 가로채 2초 안에 두 번 누르면 Activity를 종료합니다. + BackHandler { + val now = System.currentTimeMillis() + if (now - lastBackPressedAt <= BACK_PRESS_INTERVAL_MILLIS) { + activity?.finish() + } else { + lastBackPressedAt = now + Toast.makeText(context, "한 번 더 누르면 종료됩니다.", Toast.LENGTH_SHORT).show() + } + } + + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + ) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = 24.dp, + top = 50.dp, + end = 24.dp, + bottom = 32.dp + ) + ) { + item(contentType = "header") { + Text( + text = title, + color = ColorTextPrimary, + fontSize = 30.sp, + lineHeight = 36.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = today, + color = ColorTextSecondary, + fontSize = 20.sp, + lineHeight = 26.sp + ) + } + + item(contentType = "logo") { + Spacer(modifier = Modifier.height(48.dp)) + Image( + painter = painterResource(id = R.drawable.nike_logo), + contentDescription = stringResource(id = R.string.home_brand_logo), + modifier = Modifier + .fillParentMaxWidth() + .height(220.dp) + .padding(horizontal = 36.dp) + ) + } + + item(contentType = "latestTitle") { + Spacer(modifier = Modifier.height(28.dp)) + Text( + text = "What's New", + color = ColorTextPrimary, + fontSize = 22.sp, + lineHeight = 28.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = "나이키 최신 상품", + color = ColorTextSecondary, + fontSize = 15.sp, + lineHeight = 20.sp + ) + Spacer(modifier = Modifier.height(18.dp)) + } + + item(contentType = "latestProducts") { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(end = 8.dp) + ) { + items( + items = ProductCatalog.latestProducts(products), + // Lazy item key는 RecyclerView의 stable id처럼 아이템 상태를 안정적으로 묶어줍니다. + key = { product -> product.id }, + contentType = { "latestProduct" } + ) { product -> + HomeProductCard( + product = product, + onClick = onProductClick + ) + } + } + Spacer(modifier = Modifier.height(18.dp)) + } + } + + if (showScrollToTop) { + Button( + onClick = { + coroutineScope.launch { + listState.animateScrollToItem(index = 0) + } + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 20.dp, bottom = 20.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ) + ) { + Text( + text = "맨 위로", + fontSize = 14.sp, + fontWeight = FontWeight.Bold + ) + } + } + } +} + +private fun currentKoreanDate(): String { + val formatter = SimpleDateFormat("M월 d일 EEEE", Locale.KOREAN) + return formatter.format(Date()) +} + +@Preview(showBackground = true) +@Composable +private fun HomeScreenPreview() { + DekuTheme { + HomeScreen( + title = SPLASH_HOME_TITLE, + products = ProductCatalog.initialProducts(), + onProductClick = {} + ) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/main/MainScreen.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/main/MainScreen.kt new file mode 100644 index 0000000..fd69228 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/main/MainScreen.kt @@ -0,0 +1,62 @@ +package com.example.deku.feature.main + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.example.deku.core.component.MainBottomBar +import com.example.deku.data.ProductCatalog +import com.example.deku.navigation.MainNavGraph +import com.example.deku.navigation.currentBaseRoute +import com.example.deku.navigation.navigateToBottomTab + +@Composable +fun MainScreen(homeTitle: String) { + val navController = rememberNavController() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route.currentBaseRoute() + // 위시 상태는 여러 탭에서 함께 쓰이므로 최상위 화면에 올려 단일 진실 공급원으로 관리합니다. + var products by remember { mutableStateOf(ProductCatalog.initialProducts()) } + + fun toggleWish(productId: Int) { + products = products.map { product -> + if (product.id == productId) { + product.copy(isWish = !product.isWish) + } else { + product + } + } + } + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = Color.White, + bottomBar = { + MainBottomBar( + currentRoute = currentRoute, + onTabSelected = { routeName -> + // BottomBar는 routeName만 올리고, 실제 Navigation 처리는 부모가 담당합니다. + navController.navigateToBottomTab(routeName, homeTitle) + } + ) + } + ) { innerPadding -> + MainNavGraph( + navController = navController, + homeTitle = homeTitle, + products = products, + onWishClick = { product -> toggleWish(product.id) }, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/product/ProductCards.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/product/ProductCards.kt new file mode 100644 index 0000000..f3792da --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/product/ProductCards.kt @@ -0,0 +1,239 @@ +package com.example.deku.feature.product + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +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.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.deku.R +import com.example.deku.core.designsystem.ColorFrameBackground +import com.example.deku.core.designsystem.ColorTextPrimary +import com.example.deku.core.designsystem.ColorTextSecondary +import com.example.deku.data.ProductItem + +@Composable +fun HomeProductCard( + product: ProductItem, + onClick: (ProductItem) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .width(220.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable { onClick(product) } + .padding(14.dp) + ) { + ProductImage( + product = product, + modifier = Modifier + .fillMaxWidth() + .height(140.dp), + imagePadding = 20.dp + ) + Spacer(modifier = Modifier.height(14.dp)) + Text( + text = product.name, + color = ColorTextPrimary, + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Bold, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = product.price, + color = ColorTextSecondary, + fontSize = 14.sp, + lineHeight = 18.sp + ) + } +} + +@Composable +fun ProductListRow( + product: ProductItem, + onClick: (ProductItem) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .clickable { onClick(product) } + .padding(vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ProductImage( + product = product, + modifier = Modifier.size(width = 112.dp, height = 92.dp), + imagePadding = 14.dp + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = product.name, + color = ColorTextPrimary, + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Bold, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = product.category, + color = ColorTextSecondary, + fontSize = 13.sp, + lineHeight = 17.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "${product.colorCount} Color", + color = ColorTextSecondary, + fontSize = 13.sp, + lineHeight = 17.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = product.price, + color = ColorTextPrimary, + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Bold + ) + } + } +} + +@Composable +fun ProductGridCard( + product: ProductItem, + onClick: (ProductItem) -> Unit, + onWishClick: (ProductItem) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { onClick(product) } + .padding(12.dp) + ) { + Box { + ProductImage( + product = product, + modifier = Modifier + .fillMaxWidth() + .height(132.dp), + imagePadding = 18.dp + ) + Surface( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(10.dp) + .size(26.dp) + .clip(CircleShape) + .clickable { onWishClick(product) }, + shape = CircleShape, + color = Color.White + ) { + Icon( + painter = painterResource( + id = if (product.isWish) { + R.drawable.heart_filled + } else { + R.drawable.heart + } + ), + contentDescription = if (product.isWish) { + "위시리스트 제거" + } else { + "위시리스트 추가" + }, + tint = Color.Unspecified, + modifier = Modifier.padding(6.dp) + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = product.name, + color = ColorTextPrimary, + fontSize = 15.sp, + lineHeight = 19.sp, + fontWeight = FontWeight.Bold, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = product.category, + color = ColorTextSecondary, + fontSize = 12.sp, + lineHeight = 16.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "${product.colorCount} Color", + color = ColorTextSecondary, + fontSize = 12.sp, + lineHeight = 16.sp + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = product.price, + color = ColorTextPrimary, + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +fun ProductImage( + product: ProductItem, + modifier: Modifier = Modifier, + imagePadding: androidx.compose.ui.unit.Dp = 18.dp +) { + Box( + modifier = modifier.background(ColorFrameBackground), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = product.imageResId), + contentDescription = product.name, + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxSize() + .padding(imagePadding) + ) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/product/ProductDetailScreen.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/product/ProductDetailScreen.kt new file mode 100644 index 0000000..cfa6b5f --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/product/ProductDetailScreen.kt @@ -0,0 +1,235 @@ +package com.example.deku.feature.product + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedButton +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.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.deku.R +import com.example.deku.core.designsystem.ColorDivider +import com.example.deku.core.designsystem.ColorTextPrimary +import com.example.deku.core.designsystem.ColorTextSecondary +import com.example.deku.core.designsystem.theme.DekuTheme +import com.example.deku.data.ProductCatalog +import com.example.deku.data.ProductItem + +@Composable +fun ProductDetailScreen( + product: ProductItem?, + onBackClick: () -> Unit, + onWishClick: (ProductItem) -> Unit, + modifier: Modifier = Modifier +) { + if (product == null) { + ProductNotFoundScreen( + onBackClick = onBackClick, + modifier = modifier + ) + return + } + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(Color.White), + contentPadding = PaddingValues( + start = 24.dp, + top = 28.dp, + end = 24.dp, + bottom = 32.dp + ) + ) { + item(contentType = "detail") { + ProductDetailTopBar( + title = product.name, + onBackClick = onBackClick + ) + Spacer(modifier = Modifier.height(28.dp)) + ProductImage( + product = product, + modifier = Modifier + .fillMaxWidth() + .height(320.dp), + imagePadding = 18.dp + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = product.name, + color = ColorTextPrimary, + fontSize = 28.sp, + lineHeight = 34.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = product.price, + color = ColorTextSecondary, + fontSize = 16.sp, + lineHeight = 22.sp + ) + Spacer(modifier = Modifier.height(28.dp)) + Text( + text = product.description, + color = ColorTextSecondary, + fontSize = 15.sp, + lineHeight = 24.sp + ) + Spacer(modifier = Modifier.height(28.dp)) + DetailSecondaryButton( + text = "사이즈 선택", + onClick = {} + ) + Spacer(modifier = Modifier.height(28.dp)) + Button( + onClick = {}, + modifier = Modifier + .fillMaxWidth() + .height(54.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Color.Black, + contentColor = Color.White + ), + shape = RoundedCornerShape(14.dp) + ) { + Text( + text = "장바구니 추가", + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + Spacer(modifier = Modifier.height(28.dp)) + DetailSecondaryButton( + text = if (product.isWish) { + "위시리스트 제거" + } else { + "위시리스트 추가" + }, + onClick = { onWishClick(product) } + ) + } + } +} + +@Composable +private fun ProductDetailTopBar( + title: String, + onBackClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(40.dp), + contentAlignment = Alignment.Center + ) { + IconButton( + onClick = onBackClick, + modifier = Modifier + .align(Alignment.CenterStart) + .size(40.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back), + contentDescription = "뒤로 가기", + tint = ColorTextPrimary + ) + } + Text( + text = title, + color = ColorTextPrimary, + fontSize = 20.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 48.dp) + ) + } +} + +@Composable +private fun DetailSecondaryButton( + text: String, + onClick: () -> Unit +) { + OutlinedButton( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .height(54.dp), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Color.White, + contentColor = ColorTextPrimary + ), + border = BorderStroke(1.dp, ColorDivider), + shape = RoundedCornerShape(14.dp) + ) { + Text( + text = text, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } +} + +@Composable +private fun ProductNotFoundScreen( + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White) + .padding(start = 24.dp, top = 28.dp, end = 24.dp) + ) { + ProductDetailTopBar( + title = "상품 상세", + onBackClick = onBackClick + ) + Text( + text = "상품을 찾을 수 없습니다.", + color = ColorTextSecondary, + fontSize = 16.sp, + modifier = Modifier.align(Alignment.Center) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ProductDetailScreenPreview() { + DekuTheme { + ProductDetailScreen( + product = ProductCatalog.initialProducts().first(), + onBackClick = {}, + onWishClick = {} + ) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/profile/ProfileScreen.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/profile/ProfileScreen.kt new file mode 100644 index 0000000..897589c --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/profile/ProfileScreen.kt @@ -0,0 +1,28 @@ +package com.example.deku.feature.profile + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +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.unit.sp + +@Composable +fun ProfileScreen(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .background(Color.White), + contentAlignment = Alignment.TopStart + ) { + Text( + text = "Hello blank fragment", + color = MaterialTheme.colorScheme.onBackground, + fontSize = 16.sp + ) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/shop/ShopScreen.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/shop/ShopScreen.kt new file mode 100644 index 0000000..9089776 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/shop/ShopScreen.kt @@ -0,0 +1,125 @@ +package com.example.deku.feature.shop + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +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.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.deku.core.designsystem.ColorNavUnselected +import com.example.deku.core.designsystem.ColorTextPrimary +import com.example.deku.core.designsystem.theme.DekuTheme +import com.example.deku.data.ProductCatalog +import com.example.deku.data.ProductItem +import com.example.deku.feature.product.ProductGridCard + +@Composable +fun ShopScreen( + products: List, + onProductClick: (ProductItem) -> Unit, + onWishClick: (ProductItem) -> Unit, + modifier: Modifier = Modifier +) { + val tabs = listOf( + "전체" to null, + ProductCatalog.CATEGORY_TOPS to ProductCatalog.CATEGORY_TOPS, + ProductCatalog.CATEGORY_SHOES to ProductCatalog.CATEGORY_SHOES + ) + var selectedTabIndex by remember { mutableStateOf(0) } + val selectedCategory = tabs[selectedTabIndex].second + val filteredProducts = ProductCatalog.productsByCategory(products, selectedCategory) + + Column( + modifier = modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 24.dp) + .padding(top = 40.dp) + ) { + TabRow( + selectedTabIndex = selectedTabIndex, + containerColor = Color.White, + contentColor = ColorTextPrimary + ) { + tabs.forEachIndexed { index, tab -> + val title = tab.first + + Tab( + selected = selectedTabIndex == index, + onClick = { selectedTabIndex = index }, + selectedContentColor = ColorTextPrimary, + unselectedContentColor = ColorNavUnselected, + text = { + Text( + text = title, + fontSize = if (title.length > 8) 13.sp else 14.sp, + lineHeight = 18.sp, + fontWeight = if (selectedTabIndex == index) { + FontWeight.Bold + } else { + FontWeight.Normal + }, + maxLines = 1 + ) + } + ) + } + } + + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier + .fillMaxSize() + .padding(top = 20.dp), + contentPadding = PaddingValues( + start = 12.dp, + end = 12.dp, + bottom = 24.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + // Grid에서도 items + key를 지정해 상품 순서가 바뀌어도 아이템 상태가 흔들리지 않게 합니다. + items( + items = filteredProducts, + key = { product -> product.id }, + contentType = { "shopProduct" } + ) { product -> + ProductGridCard( + product = product, + onClick = onProductClick, + onWishClick = onWishClick + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ShopScreenPreview() { + DekuTheme { + ShopScreen( + products = ProductCatalog.initialProducts(), + onProductClick = {}, + onWishClick = {} + ) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/splash/SplashScreen.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/splash/SplashScreen.kt new file mode 100644 index 0000000..6f1461c --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/splash/SplashScreen.kt @@ -0,0 +1,64 @@ +package com.example.deku.feature.splash + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.res.stringResource +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 +import com.example.deku.R +import com.example.deku.core.common.SPLASH_HOME_TITLE +import com.example.deku.core.designsystem.theme.DekuTheme +import kotlinx.coroutines.delay + +@Composable +fun SplashScreen(onTimeout: () -> Unit) { + LaunchedEffect(Unit) { + delay(2_000) + onTimeout() + } + + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = SPLASH_HOME_TITLE, + color = Color(0xFF111111), + fontSize = 50.sp, + lineHeight = 58.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(16.dp)) + Image( + painter = painterResource(id = R.drawable.splash_logo), + contentDescription = stringResource(id = R.string.splash_brand_logo), + modifier = Modifier.size(width = 100.dp, height = 80.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SplashScreenPreview() { + DekuTheme { + SplashScreen(onTimeout = {}) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/feature/wishlist/WishListScreen.kt b/Week08/Deku/app/src/main/java/com/example/deku/feature/wishlist/WishListScreen.kt new file mode 100644 index 0000000..a851b63 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/feature/wishlist/WishListScreen.kt @@ -0,0 +1,113 @@ +package com.example.deku.feature.wishlist + +import androidx.compose.foundation.background +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.deku.core.designsystem.ColorTextPrimary +import com.example.deku.core.designsystem.ColorTextSecondary +import com.example.deku.core.designsystem.theme.DekuTheme +import com.example.deku.data.ProductCatalog +import com.example.deku.data.ProductItem +import com.example.deku.feature.product.ProductGridCard + +@Composable +fun WishListScreen( + products: List, + onProductClick: (ProductItem) -> Unit, + onWishClick: (ProductItem) -> Unit, + modifier: Modifier = Modifier +) { + val wishProducts = products.filter { it.isWish } + + Column( + modifier = modifier + .fillMaxSize() + .background(Color.White) + .padding(horizontal = 24.dp) + .padding(top = 70.dp) + ) { + Text( + text = "위시리스트", + color = ColorTextPrimary, + fontSize = 30.sp, + lineHeight = 36.sp, + fontWeight = FontWeight.Bold + ) + + if (wishProducts.isEmpty()) { + Spacer(modifier = Modifier.height(120.dp)) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + Text( + text = "관심 상품이 없습니다\n마음에 드는 상품을 저장해보세요.", + color = ColorTextSecondary, + textAlign = TextAlign.Center, + fontSize = 16.sp, + lineHeight = 24.sp + ) + } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier + .fillMaxSize() + .padding(top = 20.dp), + contentPadding = PaddingValues( + start = 12.dp, + end = 12.dp, + bottom = 24.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + // 위시리스트는 원본 상품 상태에서 파생된 목록이므로 id를 key로 사용합니다. + items( + items = wishProducts, + key = { product -> product.id }, + contentType = { "wishProduct" } + ) { product -> + ProductGridCard( + product = product, + onClick = onProductClick, + onWishClick = onWishClick + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun WishListScreenPreview() { + DekuTheme { + WishListScreen( + products = ProductCatalog.initialProducts().mapIndexed { index, product -> + if (index < 3) product.copy(isWish = true) else product + }, + onProductClick = {}, + onWishClick = {} + ) + } +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/navigation/MainNavGraph.kt b/Week08/Deku/app/src/main/java/com/example/deku/navigation/MainNavGraph.kt new file mode 100644 index 0000000..0fa1070 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/navigation/MainNavGraph.kt @@ -0,0 +1,109 @@ +package com.example.deku.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavController +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.example.deku.data.ProductItem +import com.example.deku.feature.cart.CartScreen +import com.example.deku.feature.home.HomeScreen +import com.example.deku.feature.profile.ProfileScreen +import com.example.deku.feature.product.ProductDetailScreen +import com.example.deku.feature.shop.ShopScreen +import com.example.deku.feature.wishlist.WishListScreen + +@Composable +fun MainNavGraph( + navController: NavHostController, + homeTitle: String, + products: List, + onWishClick: (ProductItem) -> Unit, + modifier: Modifier = Modifier +) { + NavHost( + navController = navController, + startDestination = MainRoute.Home(title = homeTitle), + modifier = modifier + ) { + composable { backStackEntry -> + // Type-safe Navigation으로 전달된 Home title 인자를 꺼냅니다. + val route = backStackEntry.toRoute() + HomeScreen( + title = route.title, + products = products, + onProductClick = { product -> + // 상품 id만 상세 route에 넘기고, 상세 화면에서는 현재 상품 목록에서 다시 조회합니다. + navController.navigate(MainRoute.ProductDetail(productId = product.id)) + } + ) + } + composable { + ShopScreen( + products = products, + onProductClick = { product -> + navController.navigate(MainRoute.ProductDetail(productId = product.id)) + }, + onWishClick = onWishClick + ) + } + composable { + WishListScreen( + products = products, + onProductClick = { product -> + navController.navigate(MainRoute.ProductDetail(productId = product.id)) + }, + onWishClick = onWishClick + ) + } + composable { + CartScreen( + onOrderClick = { + // 장바구니의 주문하기 버튼은 구매하기 탭으로 전환되어야 합니다. + navController.navigateToBottomTab(MainRouteName.SHOP, homeTitle) + } + ) + } + composable { + ProfileScreen() + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + ProductDetailScreen( + product = products.find { product -> product.id == route.productId }, + onBackClick = { navController.navigateUp() }, + onWishClick = onWishClick + ) + } + } +} + +fun NavController.navigateToBottomTab(routeName: String, homeTitle: String) { + when (routeName) { + MainRouteName.HOME -> navigateBottom(MainRoute.Home(title = homeTitle)) + MainRouteName.SHOP -> navigateBottom(MainRoute.Shop) + MainRouteName.WISH_LIST -> navigateBottom(MainRoute.WishList) + MainRouteName.CART -> navigateBottom(MainRoute.Cart) + MainRouteName.PROFILE -> navigateBottom(MainRoute.Profile) + } +} + +private fun NavController.navigateBottom(route: T) { + navigate(route) { + // 탭 전환 시 시작 destination까지만 남기고 각 탭의 상태는 가능한 복원합니다. + popUpTo(graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } +} + +fun String?.currentBaseRoute(): String? { + return this + ?.substringBefore("/") + ?.substringBefore("?") +} diff --git a/Week08/Deku/app/src/main/java/com/example/deku/navigation/MainRoute.kt b/Week08/Deku/app/src/main/java/com/example/deku/navigation/MainRoute.kt new file mode 100644 index 0000000..adc7524 --- /dev/null +++ b/Week08/Deku/app/src/main/java/com/example/deku/navigation/MainRoute.kt @@ -0,0 +1,40 @@ +package com.example.deku.navigation + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +object MainRouteName { + const val HOME = "home" + const val SHOP = "shop" + const val WISH_LIST = "wish_list" + const val CART = "cart" + const val PROFILE = "profile" + const val PRODUCT_DETAIL = "product_detail" +} + +// 문자열 route 대신 Serializable 타입으로 목적지를 정의해 Navigation 인자 오타를 컴파일 단계에서 줄입니다. +sealed interface MainRoute { + @Serializable + @SerialName(MainRouteName.HOME) + data class Home(val title: String) : MainRoute + + @Serializable + @SerialName(MainRouteName.SHOP) + data object Shop : MainRoute + + @Serializable + @SerialName(MainRouteName.WISH_LIST) + data object WishList : MainRoute + + @Serializable + @SerialName(MainRouteName.CART) + data object Cart : MainRoute + + @Serializable + @SerialName(MainRouteName.PROFILE) + data object Profile : MainRoute + + @Serializable + @SerialName(MainRouteName.PRODUCT_DETAIL) + data class ProductDetail(val productId: Int) : MainRoute +} diff --git a/Week08/Deku/app/src/main/res/drawable/cart.xml b/Week08/Deku/app/src/main/res/drawable/cart.xml new file mode 100644 index 0000000..3b6e816 --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/cart.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/Week08/Deku/app/src/main/res/drawable/heart.xml b/Week08/Deku/app/src/main/res/drawable/heart.xml new file mode 100644 index 0000000..6597a4a --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/heart.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/Week08/Deku/app/src/main/res/drawable/heart_filled.xml b/Week08/Deku/app/src/main/res/drawable/heart_filled.xml new file mode 100644 index 0000000..fd31096 --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/heart_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/Week08/Deku/app/src/main/res/drawable/home.xml b/Week08/Deku/app/src/main/res/drawable/home.xml new file mode 100644 index 0000000..20cb4d6 --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/home.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Week08/Deku/app/src/main/res/drawable/ic_arrow_back.xml b/Week08/Deku/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 0000000..ae8ecfa --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/Week08/Deku/app/src/main/res/drawable/ic_launcher_background.xml b/Week08/Deku/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Week08/Deku/app/src/main/res/drawable/ic_launcher_foreground.xml b/Week08/Deku/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Week08/Deku/app/src/main/res/drawable/nike_item1.png b/Week08/Deku/app/src/main/res/drawable/nike_item1.png new file mode 100644 index 0000000..cf18b6c Binary files /dev/null and b/Week08/Deku/app/src/main/res/drawable/nike_item1.png differ diff --git a/Week08/Deku/app/src/main/res/drawable/nike_item2.png b/Week08/Deku/app/src/main/res/drawable/nike_item2.png new file mode 100644 index 0000000..f4e6c33 Binary files /dev/null and b/Week08/Deku/app/src/main/res/drawable/nike_item2.png differ diff --git a/Week08/Deku/app/src/main/res/drawable/nike_item3.png b/Week08/Deku/app/src/main/res/drawable/nike_item3.png new file mode 100644 index 0000000..d2791e4 Binary files /dev/null and b/Week08/Deku/app/src/main/res/drawable/nike_item3.png differ diff --git a/Week08/Deku/app/src/main/res/drawable/nike_item4.png b/Week08/Deku/app/src/main/res/drawable/nike_item4.png new file mode 100644 index 0000000..1dea905 Binary files /dev/null and b/Week08/Deku/app/src/main/res/drawable/nike_item4.png differ diff --git a/Week08/Deku/app/src/main/res/drawable/nike_item5.png b/Week08/Deku/app/src/main/res/drawable/nike_item5.png new file mode 100644 index 0000000..93bca59 Binary files /dev/null and b/Week08/Deku/app/src/main/res/drawable/nike_item5.png differ diff --git a/Week08/Deku/app/src/main/res/drawable/nike_item6.png b/Week08/Deku/app/src/main/res/drawable/nike_item6.png new file mode 100644 index 0000000..21fa5d1 Binary files /dev/null and b/Week08/Deku/app/src/main/res/drawable/nike_item6.png differ diff --git a/Week08/Deku/app/src/main/res/drawable/nike_logo.png b/Week08/Deku/app/src/main/res/drawable/nike_logo.png new file mode 100644 index 0000000..d73dd96 Binary files /dev/null and b/Week08/Deku/app/src/main/res/drawable/nike_logo.png differ diff --git a/Week08/Deku/app/src/main/res/drawable/profile.xml b/Week08/Deku/app/src/main/res/drawable/profile.xml new file mode 100644 index 0000000..1fc97b8 --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/profile.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/Week08/Deku/app/src/main/res/drawable/shop.xml b/Week08/Deku/app/src/main/res/drawable/shop.xml new file mode 100644 index 0000000..483d117 --- /dev/null +++ b/Week08/Deku/app/src/main/res/drawable/shop.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/Week08/Deku/app/src/main/res/drawable/splash_logo.png b/Week08/Deku/app/src/main/res/drawable/splash_logo.png new file mode 100644 index 0000000..d6215d1 Binary files /dev/null and b/Week08/Deku/app/src/main/res/drawable/splash_logo.png differ diff --git a/Week08/Deku/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Week08/Deku/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/Week08/Deku/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Week08/Deku/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Week08/Deku/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/Week08/Deku/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Week08/Deku/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Week08/Deku/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Week08/Deku/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Week08/Deku/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Week08/Deku/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Week08/Deku/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Week08/Deku/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Week08/Deku/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Week08/Deku/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Week08/Deku/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/Week08/Deku/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Week08/Deku/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/Week08/Deku/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/Week08/Deku/app/src/main/res/values/colors.xml b/Week08/Deku/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/Week08/Deku/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/Week08/Deku/app/src/main/res/values/strings.xml b/Week08/Deku/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..193be95 --- /dev/null +++ b/Week08/Deku/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Deku + NIKE logo + NIKE splash logo + diff --git a/Week08/Deku/app/src/main/res/values/themes.xml b/Week08/Deku/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..2356cbe --- /dev/null +++ b/Week08/Deku/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +