diff --git a/.editorConfig b/.editorConfig new file mode 100644 index 0000000..931bfa6 --- /dev/null +++ b/.editorConfig @@ -0,0 +1,2 @@ +[*.{kt,kts}] +ktlint_function_naming_ignore_when_annotated_with=Composable \ No newline at end of file diff --git a/README.md b/README.md index b0d9ac6..a4d5511 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ -# android-shopping-cart \ No newline at end of file +# android-shopping-cart + +# step1 + +## 기능 요구 사항 +- [x] 상품 목록 화면을 구현한다. + +## 프로그래밍 요구 사항 +- [x] viewModel, Hilt 등은 장바구니 미션에서 활용하지 않는다. +- [x] 상품 목록 화면을 구현할 때 Lazy 컴포넌트를 활용한다. +- [x] 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다. + - [x] 의미있는 단위의 함수를 모아 별도의 파일로 분리해본다. + +# step2 + +## 기능 요구 사항 +- [x] 상품 상세 화면을 구현한다. + - [x] 상품 목록에서 상품을 누르면 상세 화면으로 이동한다. + - [x] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 이동한다. +- [x] 장바구니 화면의 빈 껍데기를 연결한다. + - [x] 상품 목록에서 장바구니 아이콘을 누르면 장바구니 화면으로 이동한다. + - [x] 뒤로가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. + - [x] 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. + +# step3 + +## 기능 요구사항 +- [x] 상품을 장바구니 담는 기능을 구현한다. +- [x] 장바구니 화면을 구현한다. + - [x] 담긴 상품의 수량을 조절할 수 있어야 한다. + - [x] 수량을 1보다 작게 하면 장바구니에서 상품이 제거된다. + - [x] 담긴 상품 가격의 총합이 주문하기 버튼에 표시된다. + +## 프로그래밍 요구사항 +- [x] 상품을 주문하는 기능에 대해서는 구현하지 않아도 된다. +- [x] 장바구니 화면에 대한 테스트코드를 작성한다. \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1e47716..ac1268b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.ktlint) } android { @@ -25,7 +26,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -66,4 +67,5 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + implementation(libs.bundles.coil) } diff --git a/app/src/androidTest/java/nextstep/shoppingcart/CartScreenTest.kt b/app/src/androidTest/java/nextstep/shoppingcart/CartScreenTest.kt new file mode 100644 index 0000000..90d7b6d --- /dev/null +++ b/app/src/androidTest/java/nextstep/shoppingcart/CartScreenTest.kt @@ -0,0 +1,87 @@ +package nextstep.shoppingcart + +import androidx.compose.ui.test.isDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onAllNodesWithContentDescription +import androidx.compose.ui.test.onFirst +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import nextstep.shoppingcart.domain.CartProductRepository +import nextstep.shoppingcart.domain.Product +import nextstep.shoppingcart.ui.cart.CartScreen +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class CartScreenTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun setUp() { + if (CartProductRepository.cartItems.isEmpty()) { + CartProductRepository.addItem( + Product( + id = 0, + name = "레아 사랑해요", + imageUrl = "히히", + price = 1004, + ), + ) + } + composeTestRule.setContent { + CartScreen() + } + } + + @Test + fun `수량_증가_버튼을_누르면_상품의_수량이_수량_단위만큼_증가한다`() { + // given + val cartItem = CartProductRepository.cartItems.first() + // when + composeTestRule.onAllNodesWithContentDescription( + "수량 증가 버튼", + ).onFirst().performClick() + + // then + composeTestRule.onNodeWithText( + (cartItem.quantity.currentValue + cartItem.countInterval).toString(), + ).isDisplayed() + } + + @Test + fun `수량_감소_버튼을_누르면_상품의_수량이_수량_단위만큼_감소한다`() { + // given + val cartItem = CartProductRepository.cartItems.first() + + // when + composeTestRule.onAllNodesWithContentDescription( + "수량 감소 버튼", + ).onFirst().performClick() + + // then + composeTestRule.onNodeWithText( + (cartItem.quantity.currentValue - cartItem.countInterval).toString(), + ).isDisplayed() + } + + @Test + fun `상품의_수량을_증가시키면_변경된_가격이_버튼에_표시된다`() { + // given + val cartItem = CartProductRepository.cartItems.first() + val previousPrice = CartProductRepository.totalPrice() + composeTestRule.onNodeWithText( + "주문하기(${previousPrice}원)", + ).isDisplayed() + + // when + composeTestRule.onAllNodesWithContentDescription( + "수량 증가 버튼", + ).onFirst().performClick() + + // then + composeTestRule.onNodeWithText( + "주문하기(${previousPrice + cartItem.product.price}원)", + ).isDisplayed() + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99ac770..24d383c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + diff --git a/app/src/main/java/nextstep/shoppingcart/MainActivity.kt b/app/src/main/java/nextstep/shoppingcart/MainActivity.kt deleted file mode 100644 index 5bbb116..0000000 --- a/app/src/main/java/nextstep/shoppingcart/MainActivity.kt +++ /dev/null @@ -1,46 +0,0 @@ -package nextstep.shoppingcart - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import nextstep.shoppingcart.ui.theme.ShoppingCartTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - ShoppingCartTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Greeting("Android") - } - } - } - } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - ShoppingCartTheme { - Greeting("Android") - } -} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/CartProduct.kt b/app/src/main/java/nextstep/shoppingcart/domain/CartProduct.kt new file mode 100644 index 0000000..6d2bbda --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/CartProduct.kt @@ -0,0 +1,40 @@ +package nextstep.shoppingcart.domain + +data class CartProduct( + val product: Product, + val quantity: Quantity = Quantity(currentValue = INITIAL_COUNT), + val countInterval: Int = DEFAULT_COUNT_INTERVAL, +) { + val totalPrice: Int = product.price * quantity.currentValue + + fun increaseCount(): CartQuantityUpdateResult = + if ((quantity.maxValue != null && quantity.currentValue < quantity.maxValue) || quantity.maxValue == null) { + CartQuantityUpdateResult.Success( + this.copy( + quantity = quantity.copy(currentValue = quantity.currentValue + countInterval), + ), + ) + } else { + CartQuantityUpdateResult.MaxFail + } + + fun decreaseCount(): CartQuantityUpdateResult = + if (quantity.currentValue > quantity.minValue) { + CartQuantityUpdateResult.Success( + this.copy( + quantity = quantity.copy(currentValue = quantity.currentValue - countInterval), + ), + ) + } else { + CartQuantityUpdateResult.MinFail + } + + companion object { + private const val DEFAULT_COUNT_INTERVAL = 1 + private const val INITIAL_COUNT = 1 + val dummy = + CartProduct( + ProductRepository.dummy.first(), + ) + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/CartProductRepository.kt b/app/src/main/java/nextstep/shoppingcart/domain/CartProductRepository.kt new file mode 100644 index 0000000..1c3ef61 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/CartProductRepository.kt @@ -0,0 +1,51 @@ +package nextstep.shoppingcart.domain + +object CartProductRepository { + private val _cartItems: MutableList = mutableListOf() + val cartItems: List get() = _cartItems.toList() + + fun addItem(product: Product) { + if (_cartItems.all { it.product.id != product.id }) { + _cartItems.add( + CartProduct( + product = product, + ), + ) + } else { + plusItemCount(product.id) + } + } + + fun minusItemCount(productId: Long) { + val cartProduct = findItem(productId) + val index = _cartItems.indexOf(cartProduct) + when (val result = cartProduct.decreaseCount()) { + CartQuantityUpdateResult.MaxFail -> throw IllegalArgumentException("수량 감소시에는 수량 증가 실패 이벤트가 발생할 수 없습니다.") + CartQuantityUpdateResult.MinFail -> deleteCartItem(productId) + is CartQuantityUpdateResult.Success -> _cartItems[index] = result.product + } + } + + fun plusItemCount(productId: Long): CartQuantityUpdateResult { + val cartProduct = findItem(productId) + val index = _cartItems.indexOf(cartProduct) + val result = cartProduct.increaseCount() + if (result is CartQuantityUpdateResult.Success) { + _cartItems[index] = result.product + } + return result + } + + fun deleteCartItem(productId: Long) { + _cartItems.removeIf { + it.product.id == productId + } + } + + fun findItem(productId: Long): CartProduct = + _cartItems.firstOrNull { + it.product.id == productId + } ?: throw IllegalArgumentException("장바구니에 $productId 에 대한 상품이 없습니다.") + + fun totalPrice(): Int = _cartItems.sumOf { it.totalPrice } +} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/CartQuantityUpdateResult.kt b/app/src/main/java/nextstep/shoppingcart/domain/CartQuantityUpdateResult.kt new file mode 100644 index 0000000..45f930c --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/CartQuantityUpdateResult.kt @@ -0,0 +1,9 @@ +package nextstep.shoppingcart.domain + +sealed interface CartQuantityUpdateResult { + data class Success(val product: CartProduct) : CartQuantityUpdateResult + + data object MinFail : CartQuantityUpdateResult + + data object MaxFail : CartQuantityUpdateResult +} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/Product.kt b/app/src/main/java/nextstep/shoppingcart/domain/Product.kt new file mode 100644 index 0000000..0cd4036 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/Product.kt @@ -0,0 +1,8 @@ +package nextstep.shoppingcart.domain + +data class Product( + val id: Long, + val name: String, + val price: Int, + val imageUrl: String, +) diff --git a/app/src/main/java/nextstep/shoppingcart/domain/ProductRepository.kt b/app/src/main/java/nextstep/shoppingcart/domain/ProductRepository.kt new file mode 100644 index 0000000..545ea0a --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/ProductRepository.kt @@ -0,0 +1,69 @@ +package nextstep.shoppingcart.domain + +object ProductRepository { + val dummy: List = + listOf( + Product( + 0, + "미우미우 - 오드리햅번ver", + 27000, + "https://github.com/user-attachments/assets/5470cb47-32c7-485f-9941-85eb12fc8361", + ), + Product( + 1, + "아기 복숭아 - 173cm", + 50000, + "https://github.com/user-attachments/assets/3e627034-236d-4516-9689-bd6d6f33971b", + ), + Product( + 2, + "궁극의 아이돌 - 콘서트", + 72000, + "https://github.com/user-attachments/assets/bec777ce-ac0c-48b7-8d40-997babc8a02c", + ), + Product( + 3, + "탤마뿌럼못날뤼가 - smart", + 25400, + "https://github.com/user-attachments/assets/f9576cfd-6b8a-47a3-ac3d-6ae267373bfa", + ), + Product( + 4, + "스빠이쉬쓰빠이쉬쓰빠이쉬쓰빠이쉬 - spicy", + 123456, + "https://github.com/user-attachments/assets/c9f30f84-3d73-420a-9f0f-52f4df47a36d", + ), + Product( + 5, + "이부자리 - 팬싸템ver", + 73920, + "https://github.com/user-attachments/assets/a1088a04-7e00-4c86-b5ac-bfff1a825910", + ), + Product( + 6, + "아기사막여우 - 키스오브라이프뜻이인공호흡이래요", + 353534, + "https://github.com/user-attachments/assets/358cf2a6-7b4a-40a2-b6e5-ffa6303eab27", + ), + Product( + 7, + "위플래쉬 - 개띵곡", + 5383, + "https://github.com/user-attachments/assets/bdbe4e39-3852-4fd8-8e7c-5173bfa716fa", + ), + Product( + 8, + "삐끼삐끼삐끼삐끼 - 프미나", + 23452363, + "https://github.com/user-attachments/assets/7d69b1b6-bf6c-4bc9-af82-88613ac32ac6", + ), + Product( + 9, + "천년의 아이돌 - 미스터츄", + 3582398, + "https://github.com/user-attachments/assets/50ee770a-6155-4021-a849-27a5a4078065", + ), + ) + + fun productById(id: Long): Product = dummy.find { it.id == id } ?: throw IllegalArgumentException("$id 에 해당하는 값이 없습니다.") +} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/Quantity.kt b/app/src/main/java/nextstep/shoppingcart/domain/Quantity.kt new file mode 100644 index 0000000..606340e --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/Quantity.kt @@ -0,0 +1,23 @@ +package nextstep.shoppingcart.domain + +data class Quantity( + val currentValue: Int, + val minValue: Int = DEFAULT_MINIMUM_COUNT, + val maxValue: Int? = null, +) { + init { + require(currentValue >= minValue) { + "$currentValue : 장바구니 상품의 개수는 $minValue 이상이어야 합니다." + } + + maxValue?.let { + require(currentValue <= maxValue) { + "$currentValue : 장바구니 상품의 개수는 $maxValue 이하여야 합니다." + } + } + } + + companion object { + private const val DEFAULT_MINIMUM_COUNT = 1 + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/cart/CartActivity.kt b/app/src/main/java/nextstep/shoppingcart/ui/cart/CartActivity.kt new file mode 100644 index 0000000..0a2328b --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/cart/CartActivity.kt @@ -0,0 +1,111 @@ +package nextstep.shoppingcart.ui.cart + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +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.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import nextstep.shoppingcart.domain.CartProductRepository +import nextstep.shoppingcart.ui.component.BackNavigationTopBar +import nextstep.shoppingcart.ui.component.SubmitButton +import nextstep.shoppingcart.ui.theme.ShoppingCartTheme +import nextstep.shoppingcart.ui.util.findActivity +import nextstep.signup.R + +class CartActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + ShoppingCartTheme { + CartScreen() + } + } + } + + companion object { + fun newIntent(context: Context): Intent = Intent(context, CartActivity::class.java) + } +} + +@Composable +fun CartScreen() { + val context = LocalContext.current + var totalPrice by remember { mutableIntStateOf(CartProductRepository.totalPrice()) } + var cartItems by remember { mutableStateOf(CartProductRepository.cartItems) } + + fun updateInfo() { + totalPrice = CartProductRepository.totalPrice() + cartItems = CartProductRepository.cartItems + } + + Scaffold( + topBar = { + BackNavigationTopBar( + title = stringResource(R.string.cart_title), + ) { + context.findActivity().finish() + } + }, + ) { innerPadding -> + Box( + modifier = + Modifier + .padding(innerPadding) + .fillMaxSize(), + ) { + CartList( + cartItems = cartItems, + onCountMinus = { cartProduct -> + CartProductRepository.minusItemCount(cartProduct.product.id) + updateInfo() + }, + onCountPlus = { cartProduct -> + CartProductRepository.plusItemCount(cartProduct.product.id) + updateInfo() + }, + onItemDelete = { cartProduct -> + CartProductRepository.deleteCartItem(cartProduct.product.id) + updateInfo() + }, + ) + + SubmitButton( + label = + stringResource( + R.string.cart_order_button, + totalPrice, + ), + onClick = {}, + modifier = Modifier.align(Alignment.BottomCenter), + ) + } + } +} + +@Preview( + showBackground = true, + showSystemUi = true, +) +@Composable +private fun CartScreenPreview() { + ShoppingCartTheme { + CartScreen() + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/cart/CartItem.kt b/app/src/main/java/nextstep/shoppingcart/ui/cart/CartItem.kt new file mode 100644 index 0000000..1bc2bb3 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/cart/CartItem.kt @@ -0,0 +1,135 @@ +package nextstep.shoppingcart.ui.cart + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedCard +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.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import nextstep.shoppingcart.domain.CartProduct +import nextstep.shoppingcart.ui.component.CountUpdateButton +import nextstep.shoppingcart.ui.theme.Gray10 +import nextstep.shoppingcart.ui.theme.ShoppingCartTheme +import nextstep.shoppingcart.ui.theme.Typography +import nextstep.signup.R + +@Composable +fun CartItem( + cartProduct: CartProduct, + onCountMinus: () -> Unit, + onCountPlus: () -> Unit, + onItemDelete: () -> Unit, +) { + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + border = BorderStroke(1.dp, Gray10), + colors = CardDefaults.cardColors().copy(containerColor = Color.White), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 18.dp, end = 6.dp, top = 6.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + modifier = + Modifier + .align(Alignment.CenterVertically) + .weight(1f), + text = cartProduct.product.name, + style = Typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + IconButton( + modifier = + Modifier + .height(48.dp) + .width(48.dp), + onClick = onItemDelete, + ) { + Icon( + modifier = + Modifier + .height(24.dp) + .width(24.dp), + imageVector = Icons.Filled.Close, + contentDescription = stringResource(R.string.product_list_cart_button_desc), + ) + } + } + + Row( + modifier = + Modifier + .padding(horizontal = 18.dp) + .padding(bottom = 18.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + AsyncImage( + model = cartProduct.product.imageUrl, + modifier = + Modifier + .width(136.dp) + .height(84.dp), + contentScale = ContentScale.Crop, + contentDescription = stringResource(R.string.cart_product_image_desc), + ) + + Column( + modifier = Modifier.align(Alignment.Bottom), + ) { + Text( + modifier = Modifier.align(Alignment.End), + text = + stringResource( + R.string.product_price_format, + cartProduct.totalPrice, + ), + ) + + CountUpdateButton( + count = cartProduct.quantity.currentValue, + onCountMinus = onCountMinus, + onCountPlus = onCountPlus, + ) + } + } + } + } +} + +@Preview( + showBackground = true, +) +@Composable +private fun CartItemPreview() { + ShoppingCartTheme { + CartItem(CartProduct.dummy, { }, {}, {}) + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/cart/CartList.kt b/app/src/main/java/nextstep/shoppingcart/ui/cart/CartList.kt new file mode 100644 index 0000000..fbe4200 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/cart/CartList.kt @@ -0,0 +1,60 @@ +package nextstep.shoppingcart.ui.cart + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import nextstep.shoppingcart.domain.CartProduct + +@Composable +fun CartList( + cartItems: List, + onCountMinus: (CartProduct) -> Unit, + onCountPlus: (CartProduct) -> Unit, + onItemDelete: (CartProduct) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + contentPadding = PaddingValues(18.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items( + items = cartItems, + ) { item -> + CartItem( + cartProduct = item, + onCountMinus = { + onCountMinus(item) + }, + onCountPlus = { + onCountPlus(item) + }, + onItemDelete = { + onItemDelete(item) + }, + ) + } + } +} + +@Preview( + showBackground = true, + showSystemUi = true, +) +@Composable +private fun CartListPreview() { + CartList( + listOf( + CartProduct.dummy, + CartProduct.dummy, + ), + {}, + {}, + {}, + ) +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/component/BackNavigationTopBar.kt b/app/src/main/java/nextstep/shoppingcart/ui/component/BackNavigationTopBar.kt new file mode 100644 index 0000000..5db7591 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/component/BackNavigationTopBar.kt @@ -0,0 +1,39 @@ +package nextstep.shoppingcart.ui.component + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import nextstep.shoppingcart.ui.theme.Typography +import nextstep.signup.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BackNavigationTopBar( + title: String, + onClickNavigationIcon: () -> Unit, +) { + TopAppBar( + title = { + Text( + text = title, + style = Typography.headlineMedium, + ) + }, + navigationIcon = { + IconButton( + onClick = onClickNavigationIcon, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.product_detail_back_button_desc), + ) + } + }, + ) +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/component/CountUpdateButton.kt b/app/src/main/java/nextstep/shoppingcart/ui/component/CountUpdateButton.kt new file mode 100644 index 0000000..b28b2c9 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/component/CountUpdateButton.kt @@ -0,0 +1,72 @@ +package nextstep.shoppingcart.ui.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import nextstep.shoppingcart.ui.theme.Typography +import nextstep.signup.R + +@Composable +fun CountUpdateButton( + count: Int, + onCountMinus: () -> Unit, + onCountPlus: () -> Unit, +) { + Row { + IconButton( + modifier = + Modifier + .width(42.dp) + .height(42.dp), + onClick = onCountMinus, + ) { + Icon( + painter = painterResource(R.drawable.ic_minus), + contentDescription = stringResource(R.string.count_minus_desc), + ) + } + + Text( + text = count.toString(), + style = Typography.displayMedium, + modifier = + Modifier + .wrapContentHeight() + .padding(horizontal = 12.dp) + .align(Alignment.CenterVertically), + ) + + IconButton( + modifier = + Modifier + .width(42.dp) + .height(42.dp), + onClick = onCountPlus, + ) { + Icon( + painter = painterResource(R.drawable.ic_plus), + contentDescription = stringResource(R.string.count_plus_desc), + ) + } + } +} + +@Preview( + showBackground = true, +) +@Composable +private fun CountUpdateButtonPreview() { + CountUpdateButton(1, {}, {}) +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/component/SubmitButton.kt b/app/src/main/java/nextstep/shoppingcart/ui/component/SubmitButton.kt new file mode 100644 index 0000000..cb0a5cc --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/component/SubmitButton.kt @@ -0,0 +1,41 @@ +package nextstep.shoppingcart.ui.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import nextstep.shoppingcart.ui.theme.Blue50 +import nextstep.shoppingcart.ui.theme.Gray10 +import nextstep.shoppingcart.ui.theme.Gray50 +import nextstep.shoppingcart.ui.theme.Typography + +@Composable +fun SubmitButton( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + shape = RectangleShape, + modifier = + modifier + .fillMaxWidth(), + colors = + ButtonColors( + containerColor = Blue50, + contentColor = Color.White, + disabledContentColor = Gray10, + disabledContainerColor = Gray50, + ), + onClick = onClick, + ) { + Text( + text = label, + style = Typography.labelMedium, + ) + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/productdetail/ProductDetailActivity.kt b/app/src/main/java/nextstep/shoppingcart/ui/productdetail/ProductDetailActivity.kt new file mode 100644 index 0000000..d03cc5a --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/productdetail/ProductDetailActivity.kt @@ -0,0 +1,113 @@ +package nextstep.shoppingcart.ui.productdetail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import nextstep.shoppingcart.domain.CartProductRepository +import nextstep.shoppingcart.domain.Product +import nextstep.shoppingcart.domain.ProductRepository +import nextstep.shoppingcart.ui.component.BackNavigationTopBar +import nextstep.shoppingcart.ui.component.SubmitButton +import nextstep.shoppingcart.ui.productdetail.ProductDetailActivity.Companion.EXTRA_PRODUCT_ID +import nextstep.shoppingcart.ui.productdetail.ProductDetailActivity.Companion.INVALID_PRODUCT_ID +import nextstep.shoppingcart.ui.theme.ShoppingCartTheme +import nextstep.shoppingcart.ui.util.findActivity +import nextstep.signup.R + +class ProductDetailActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + ShoppingCartTheme { + ProductDetailScreen() + } + } + } + + companion object { + const val EXTRA_PRODUCT_ID = "productId" + const val INVALID_PRODUCT_ID = -1L + + fun newIntent( + context: Context, + productId: Long, + ): Intent = + Intent(context, ProductDetailActivity::class.java).apply { + putExtra(EXTRA_PRODUCT_ID, productId) + } + } +} + +@Composable +fun ProductDetailScreen() { + val activity = LocalContext.current.findActivity() + + val product: Product = + ProductRepository.productById( + activity.intent.getLongExtra( + EXTRA_PRODUCT_ID, + INVALID_PRODUCT_ID, + ), + ) + + Scaffold( + topBar = { + BackNavigationTopBar( + title = stringResource(R.string.product_detail_title), + onClickNavigationIcon = { + activity.finish() + }, + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> + + Box( + modifier = + Modifier + .fillMaxSize() + .padding(innerPadding), + ) { + ProductDetailContent( + product, + ) + + SubmitButton( + label = stringResource(R.string.product_detail_cart_button), + onClick = { + CartProductRepository.addItem(product) + }, + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + ) + } + } +} + +@Preview( + showBackground = true, + showSystemUi = true, +) +@Composable +private fun ProductDetailScreenPreview() { + ShoppingCartTheme { + ProductDetailScreen() + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/productdetail/ProductDetailContent.kt b/app/src/main/java/nextstep/shoppingcart/ui/productdetail/ProductDetailContent.kt new file mode 100644 index 0000000..ee7ec29 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/productdetail/ProductDetailContent.kt @@ -0,0 +1,88 @@ +package nextstep.shoppingcart.ui.productdetail + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import nextstep.shoppingcart.domain.Product +import nextstep.shoppingcart.domain.ProductRepository +import nextstep.shoppingcart.ui.theme.Gray10 +import nextstep.shoppingcart.ui.theme.Gray20 +import nextstep.shoppingcart.ui.theme.ShoppingCartTheme +import nextstep.shoppingcart.ui.theme.Typography +import nextstep.signup.R + +@Composable +fun ProductDetailContent( + product: Product, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .fillMaxWidth(), + ) { + AsyncImage( + model = product.imageUrl, + contentDescription = stringResource(R.string.product_detail_image_desc), + contentScale = ContentScale.Crop, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f), + ) + + Text( + text = product.name, + color = Gray20, + modifier = Modifier.padding(18.dp), + style = Typography.titleLarge, + ) + + HorizontalDivider( + thickness = 1.dp, + color = Gray10, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.product_detail_price), + color = Gray20, + modifier = Modifier.padding(18.dp), + style = Typography.bodyMedium, + ) + Text( + text = stringResource(R.string.product_price_format, product.price), + color = Gray20, + style = Typography.bodyMedium, + modifier = Modifier.padding(18.dp), + ) + } + } +} + +@Preview( + showBackground = true, +) +@Composable +private fun ProductDetailContentPreview() { + ShoppingCartTheme { + ProductDetailContent( + ProductRepository.dummy.first(), + ) + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductList.kt b/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductList.kt new file mode 100644 index 0000000..58a37ce --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductList.kt @@ -0,0 +1,35 @@ +package nextstep.shoppingcart.ui.productlist + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import nextstep.shoppingcart.domain.Product + +@Composable +fun ProductList( + products: List, + onItemClick: (Product) -> Unit, + modifier: Modifier = Modifier, +) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(18.dp), + ) { + items( + items = products, + key = { product -> + product.id + }, + ) { item -> + ProductListItem(item, onItemClick) + } + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListActivity.kt b/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListActivity.kt new file mode 100644 index 0000000..d089343 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListActivity.kt @@ -0,0 +1,62 @@ +package nextstep.shoppingcart.ui.productlist + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +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.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import nextstep.shoppingcart.domain.ProductRepository +import nextstep.shoppingcart.ui.cart.CartActivity +import nextstep.shoppingcart.ui.productdetail.ProductDetailActivity +import nextstep.shoppingcart.ui.theme.ShoppingCartTheme + +class ProductListActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + ShoppingCartTheme { + ProductListScreen() + } + } + } +} + +@Composable +fun ProductListScreen() { + val context = LocalContext.current + + Scaffold( + topBar = { + ProductListTopBar( + onCartClick = { + context.startActivity(CartActivity.newIntent(context)) + }, + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> + + ProductList( + products = ProductRepository.dummy, + onItemClick = { product -> + context.startActivity(ProductDetailActivity.newIntent(context, product.id)) + }, + modifier = Modifier.padding(innerPadding), + ) + } +} + +@Preview(showBackground = true, showSystemUi = true) +@Composable +fun ProductListScreenPreview() { + ShoppingCartTheme { + ProductListScreen() + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListItem.kt b/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListItem.kt new file mode 100644 index 0000000..a0227a9 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListItem.kt @@ -0,0 +1,63 @@ +package nextstep.shoppingcart.ui.productlist + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import coil3.compose.AsyncImage +import nextstep.shoppingcart.domain.Product +import nextstep.shoppingcart.domain.ProductRepository +import nextstep.shoppingcart.ui.theme.Typography +import nextstep.signup.R + +@Composable +fun ProductListItem( + product: Product, + onClick: (Product) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = + modifier + .fillMaxSize() + .clickable { + onClick(product) + }, + ) { + AsyncImage( + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f), + model = product.imageUrl, + contentDescription = stringResource(R.string.product_list_image_desc, product.name), + contentScale = ContentScale.Crop, + ) + Text( + text = product.name, + style = Typography.titleSmall, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + Text( + text = stringResource(R.string.product_price_format, product.price), + style = Typography.bodySmall, + overflow = TextOverflow.Visible, + maxLines = 1, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun ProductListItemPreview() { + ProductListItem(ProductRepository.dummy.first(), {}) +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListTopBar.kt b/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListTopBar.kt new file mode 100644 index 0000000..55b7c43 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/productlist/ProductListTopBar.kt @@ -0,0 +1,54 @@ +package nextstep.shoppingcart.ui.productlist + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import nextstep.shoppingcart.ui.theme.Typography +import nextstep.signup.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProductListTopBar( + onCartClick: () -> Unit, + modifier: Modifier = Modifier, +) { + CenterAlignedTopAppBar( + title = { + Text( + stringResource(R.string.product_list_screen_title), + style = Typography.headlineMedium, + modifier = Modifier.wrapContentHeight().padding(vertical = 18.dp), + ) + }, + actions = { + IconButton(onCartClick, modifier = Modifier.padding(end = 8.dp)) { + Icon( + imageVector = Icons.Filled.ShoppingCart, + contentDescription = stringResource(R.string.product_list_cart_button_desc), + ) + } + }, + modifier = + modifier.fillMaxWidth(), + ) +} + +@Preview( + showBackground = true, +) +@Composable +private fun ProductListTopBarPreview() { + ProductListTopBar({}) +} diff --git a/app/src/main/java/nextstep/shoppingcart/ui/theme/Color.kt b/app/src/main/java/nextstep/shoppingcart/ui/theme/Color.kt index 8b22251..e1e9e44 100644 --- a/app/src/main/java/nextstep/shoppingcart/ui/theme/Color.kt +++ b/app/src/main/java/nextstep/shoppingcart/ui/theme/Color.kt @@ -8,4 +8,10 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) + +val Gray10 = Color(0xFFAAAAAA) +val Gray20 = Color(0xFF333333) +val Gray50 = Color(0xFF1D1B20) + +val Blue50 = Color(0xFF2196F3) diff --git a/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt b/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt index 3e5e697..7cfef6f 100644 --- a/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt +++ b/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt @@ -1,31 +1,26 @@ package nextstep.shoppingcart.ui.theme -import android.app.Activity -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.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat +import androidx.compose.ui.graphics.Color -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 +private val DarkColorScheme = + darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80, + background = Color.Black, + ) +private val LightColorScheme = + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + background = Color.White, /* Other default colors to override background = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE), @@ -34,29 +29,23 @@ private val LightColorScheme = lightColorScheme( onTertiary = Color.White, onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), - */ -) + */ + ) @Composable fun ShoppingCartTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit + 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) + val colorScheme = + when { + darkTheme -> DarkColorScheme + else -> LightColorScheme } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } diff --git a/app/src/main/java/nextstep/shoppingcart/ui/theme/Type.kt b/app/src/main/java/nextstep/shoppingcart/ui/theme/Type.kt index 75be968..e2dfbf7 100644 --- a/app/src/main/java/nextstep/shoppingcart/ui/theme/Type.kt +++ b/app/src/main/java/nextstep/shoppingcart/ui/theme/Type.kt @@ -1,34 +1,82 @@ package nextstep.shoppingcart.ui.theme import androidx.compose.material3.Typography +import androidx.compose.ui.graphics.Color 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 // Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp +val Typography = + Typography( + displayMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W400, + fontSize = 22.sp, + lineHeight = 18.sp, + color = Color.Black, + ), + headlineMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + color = Gray50, + ), + titleLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W700, + fontSize = 24.sp, + lineHeight = 28.sp, + letterSpacing = 0.5.sp, + color = Gray20, + ), + titleMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W700, + fontSize = 20.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + color = Color.Black, + ), + titleSmall = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W700, + fontSize = 16.sp, + lineHeight = 14.sp, + letterSpacing = 0.5.sp, + color = Gray20, + ), + bodyMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 20.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + color = Gray20, + ), + bodySmall = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 20.sp, + letterSpacing = 0.5.sp, + color = Gray20, + ), + labelMedium = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W700, + fontSize = 20.sp, + lineHeight = 24.sp, + color = Color.White, + ), ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/app/src/main/java/nextstep/shoppingcart/ui/util/FindActivity.kt b/app/src/main/java/nextstep/shoppingcart/ui/util/FindActivity.kt new file mode 100644 index 0000000..e4dc7d8 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/ui/util/FindActivity.kt @@ -0,0 +1,14 @@ +package nextstep.shoppingcart.ui.util + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper + +fun Context.findActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + throw IllegalStateException("Activity를 찾을 수 없습니다.") +} diff --git a/app/src/main/res/drawable/ic_minus.xml b/app/src/main/res/drawable/ic_minus.xml new file mode 100644 index 0000000..fc61c6e --- /dev/null +++ b/app/src/main/res/drawable/ic_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000..92489cf --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc5b76c..f8ad02c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,20 @@ Shopping Cart + 상품 목록 + + %,d원 + 장바구니 버튼 + $s 상품 이미지 + + 상품 상세 + 상품 상세 이미지 + 뒤로가기 버튼 + 금액 + 장바구니 담기 + + 장바구니 + 주문하기(%,d원) + 장바구니 상품 이미지 + 수량 증가 버튼 + 수량 감소 버튼 \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c1e23bc..ee34f02 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,4 +2,5 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false + alias(libs.plugins.ktlint) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5080245..fc0e1bd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,9 @@ espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.4" activityCompose = "1.9.1" composeBom = "2024.06.00" +ktlint = "12.1.0" +coil = "3.0.0-rc01" + [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -23,6 +26,13 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } +coil-network = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } + +[bundles] +coil = ["coil-compose", "coil-network"] + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }