From 71a229c66160b0556af7a811aca7b015c4a938a5 Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Tue, 5 Nov 2024 15:42:12 +0900 Subject: [PATCH 01/12] style: avoid ktlint to composable functions --- .editor.config | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .editor.config diff --git a/.editor.config b/.editor.config new file mode 100644 index 0000000..518d9c6 --- /dev/null +++ b/.editor.config @@ -0,0 +1,2 @@ +[*.{kt,kts}] +ktlint_function_naming_ignore_when_annotated_with=Composable, Test From e4c0cadc2aa4609e2291882b6004b8db596a749e Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Tue, 5 Nov 2024 15:42:32 +0900 Subject: [PATCH 02/12] docs: update README --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b0d9ac6..d856c1a 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -# android-shopping-cart \ No newline at end of file +# android-shopping-cart + +## Step1 +### 기능 요구 사항 +- [ ] 상품 목록 화면을 구현한다. + +### 프로그래밍 요구 사항 +- ViewModel, Hilt 등은 장바구니 미션에서 활용하지 않는다. 컴포즈 학습에 집중하자. +- 상품 목록 화면을 구현할 때 Lazy 컴포넌트를 활용한다. +- 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다. +- 의미있는 단위의 함수를 모아 별도의 파일로 분리해본다. From d417681f0b1e4ad554d24e41c67cf6e8ae57c041 Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Tue, 5 Nov 2024 15:43:03 +0900 Subject: [PATCH 03/12] feat: implement step1 --- README.md | 2 +- app/build.gradle.kts | 4 + app/src/main/AndroidManifest.xml | 9 +- .../data/CachedProductDataSource.kt | 67 ++++++++++++++ .../data/CachedProductRepository.kt | 11 +++ .../shoppingcart/data/ProductDataSource.kt | 8 ++ .../shoppingcart/data/ProductRepository.kt | 8 ++ .../shoppingcart/domain/model/Product.kt | 13 +++ .../DetailActivity.kt} | 27 +++--- .../shoppingcart/presentation/MainActivity.kt | 28 ++++++ .../presentation/components/ListLayout.kt | 53 +++++++++++ .../presentation/components/ProductList.kt | 90 +++++++++++++++++++ .../presentation/components/TopBar.kt | 52 +++++++++++ .../presentation/ui/theme/Color.kt | 11 +++ .../presentation/ui/theme/Theme.kt | 58 ++++++++++++ .../presentation/ui/theme/Type.kt | 34 +++++++ .../nextstep/shoppingcart/ui/theme/Theme.kt | 4 +- app/src/main/res/values/strings.xml | 1 + build.gradle.kts | 1 + gradle/libs.versions.toml | 5 ++ 20 files changed, 469 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/nextstep/shoppingcart/data/CachedProductDataSource.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/data/ProductDataSource.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/data/ProductRepository.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt rename app/src/main/java/nextstep/shoppingcart/{MainActivity.kt => presentation/DetailActivity.kt} (54%) create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/MainActivity.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/ListLayout.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/TopBar.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Color.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Theme.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Type.kt diff --git a/README.md b/README.md index d856c1a..2a3b567 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Step1 ### 기능 요구 사항 -- [ ] 상품 목록 화면을 구현한다. +- [x] 상품 목록 화면을 구현한다. ### 프로그래밍 요구 사항 - ViewModel, Hilt 등은 장바구니 미션에서 활용하지 않는다. 컴포즈 학습에 집중하자. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1e47716..1591f5c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,4 +66,8 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) + + // coil + implementation(libs.coil) + implementation(libs.coil.compose) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99ac770..49e5cfd 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/data/CachedProductDataSource.kt b/app/src/main/java/nextstep/shoppingcart/data/CachedProductDataSource.kt new file mode 100644 index 0000000..360e1c2 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/data/CachedProductDataSource.kt @@ -0,0 +1,67 @@ +package nextstep.shoppingcart.data + +import nextstep.shoppingcart.domain.model.Product + +class CachedProductDataSource : ProductDataSource { + private val products: MutableList = + mutableListOf( + Product( + id = 1, + name = "PET보틀-정사각형 용기 (1L)", + price = 10_000, + imageUrl = "https://s3-alpha-sig.figma.com/img/05ef/e578/d81445480aff1872344a6b1b35323488?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=KNMX7pZxzQz578ovhuWIBF2ilsQACu~sph~2fwFZyhO3pX9vhCd-kXHJmW5lpogb2IhP6bvH1N8qCKX~jHluc-tDSLD4EbUsGI9yJqanYsdOSRY6KKPyrh1SekXEH8u4-A77xal1TEj99wTnMMGKGXlI4UJhQJhg6hhBRL3ONv5Y1N5~2yMw4M54bGJi4HQYh3eG5rcPrK96KWonhd7BtZDwo4MMwK1TQ0FjuIBMdoabOACAwRwzE-EJ8znP71oTiOsZES6wASv21meilUhN~A8C72-qqNhyybuDTNqZQfQIHWT7kh-Lm0McKRQUg7~OSs3nWVJR7O-USQCwHhhclQ__", + ), + Product( + id = 2, + name = "PET보틀-밀크티 용기", + price = 12_000, + imageUrl = "https://s3-alpha-sig.figma.com/img/f081/71c9/4a1459fb4310f704c34be19a15f662a4?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ZKYEUR3xhp1aA4aULlmTfL1WE3xPvkK14FKZ6ih6JJyFdt54d3srSsB3OG2qUffmjzxsN-SgL4vtdmJgAyVeUh4JZlgBrrDOeBnsvqx3E3myqzO3iBZMsqG54zici8-pht8vl22MhbV7rzqNA5WSbSGbVztUhS1J8Rm9u9wDH5RY9vfbf-92vvfkRzakG84NghCmq~8fNusILMtEMlUBM~aY02owPo7MIsDLmwHBhGSThU5mnNCawKmmZc1ESwvjzeYhgY6HY~U9v24M1nlKL6KFA4wu8KuIfDv9G1-pve6P3i4DZKbgir19Q46pHGp0VErbA2yNhpWwu4MvYKJ~5w__", + ), + Product( + id = 3, + name = "PET보틀-정사각형 용기 (500ml)", + price = 10_000, + imageUrl = "https://s3-alpha-sig.figma.com/img/ff9f/d83e/b40e670bfc38dbcddbcec8b3ca363d50?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=DzhgzJzS4g6I3SO2RtuhKgWdng1dPjM2aPUASlWNq8VvjOZMd9tfYaruZLALMCKtx~cmq~Q6~hWr1vAnbHBZsRW~8EIwneiuNLE-fxFuTjFBV24G2u8LpY5n91oMgGYzNqxtuTVVqE28~GUhDHsxQt5BL5tUukHeER0-mUg4wUP-4uWA-zN9m864LZwYpTLxzcNCOAuvgFAR2HHsGMJCDyC~Fzd7lSLVuFQVwxF7kolB9ErqqVUA2SO0wjfCoHoWJUwuTlSr9jSGW4-h4lJafTpGvYJCS4foasIYy3ODeeOgBOCgnXkHdrJivZlkR0gNPCOOx2x~KDWV547AsrO5YA__", + ), + Product( + id = 4, + name = "PET보틀-납작 용기 (500ml)", + price = 12_000, + imageUrl = "https://s3-alpha-sig.figma.com/img/b8b6/a740/d8661b91e8210779ce9db930d260230d?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=lYXFy7NZnn2Pe7vn29qkin2d9HPRXteVaHuLXIH2pxjspZP1GMyr8d9It1WcYND8apF~LTd13z-ckQ9WrZO-ExycyxF6H1oVSOQmDQfxAgzpGDfmJlNiEh7BoFllbnQVPsHwfFYaGxSFr93zYkzNKfwQGUTltozO8fElpK0dQePtyIzuZpmyHegnizh~hqdlx9YlyJdfeuV-AlAtyKZjSFDcphz3RJ5YujI0xzf5XEGoYGBN5piSwr-41UWXY664LcmUVRZF9Ge8hZdyV1vu2Coty54x7ns5FbTu3GUhcZ3bSrTYaQcDzlvggtYXel~SPoV20d8xPwXMFRCbSuUxhw__", + ), + Product( + id = 5, + name= "PET보틀-원형 용기 (500ml)", + price = 10_000, + imageUrl = "https://s3-alpha-sig.figma.com/img/8f83/c0e8/c17365cdfe5a00fa17c0283d520f4a99?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=Pt91VGcwcypqZ0ZiBhcXNVlgJ7x7yrNsh0ucdfSZ1Ct12CR0eTe7G4a4doBtthWGWIL-Tmj4VYvqbMNMT3McEbGmgR~jvfIYhAO1rQaSO6Ry69kSV1K3qpaqNyW7WkfoKi2-qj4w9665rOYY~ss-H3Le7YAkpnbEKbZUBfGFodaHSEwRplcT5V~ECB65DGoevOqMg4ArLdtJSDNpOdbb166Q8LSzcmZ0sOksimWDBLKegKjWtwi30Fe6WBJzmLD5i7TBXpxKgOOlL92M0zT~kgLIAIEM2C4g8qvJWqE74d4NX~y2c-AsU7pcLQqrHOU7oElPyPXhlnZkxvPd86cyIw__", + ), + Product( + id = 6, + name = "PET보틀-납작 용기 (200mL)", + price = 12_000, + imageUrl = "https://s3-alpha-sig.figma.com/img/b9f2/403d/b915b1b22edac0877abb7b97129296b6?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=YUOtsuzD0QhxPGoaGPNVb3IfJu015CI0LkvUaywB3qcTHOmBclM6YsRCba5lGrMq3PY5MbG7U4VPbbhq8z5RBcGyxIb9M31qTYIEYTstE1-C2zq0IR3YXwMqaV5t9A7hjClBnoZl~jT2DTH4k5YxrIRy2D~qqR57C-4TF-Doo3Xhs~jNvRCyYYZBvFqg0PerspsQHnruNK9sJCVvWB4dC9DN290uU7jmbOrO6ku0SCtFp7BSCAP2l4FwW3LcIGAJpPRZSAqZu-z-n8QnEFJnWWDBqGynQqQQq9jQqxEW15gOna8a3X0nTM4RrTR6gZX2aKWMhOtRcciMoLwaiar5bw__", + ), + Product( + id = 7, + name = "name1", + price = 1000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png", + ), + Product( + id = 8, + name = "name2", + price = 2000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg", + ), + Product( + id = 9, + name = "name3", + price = 3000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg", + ), + ) + + override fun fetchProducts(): List = products.toList() + + override fun findProduct(id: Long): Product = products.firstOrNull { it.id == id } ?: Product.NULL_PRODUCT +} diff --git a/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt b/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt new file mode 100644 index 0000000..c45d652 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt @@ -0,0 +1,11 @@ +package nextstep.shoppingcart.data + +import nextstep.shoppingcart.domain.model.Product + +class CachedProductRepository( + private val productDataSource: ProductDataSource, +) : ProductRepository { + override fun fetchProducts(): List = productDataSource.fetchProducts() + + override fun getProduct(id: Long): Product = productDataSource.findProduct(id) +} diff --git a/app/src/main/java/nextstep/shoppingcart/data/ProductDataSource.kt b/app/src/main/java/nextstep/shoppingcart/data/ProductDataSource.kt new file mode 100644 index 0000000..f3278f5 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/data/ProductDataSource.kt @@ -0,0 +1,8 @@ +package nextstep.shoppingcart.data + +import nextstep.shoppingcart.domain.model.Product + +interface ProductDataSource { + fun fetchProducts(): List + fun findProduct(id: Long): Product +} diff --git a/app/src/main/java/nextstep/shoppingcart/data/ProductRepository.kt b/app/src/main/java/nextstep/shoppingcart/data/ProductRepository.kt new file mode 100644 index 0000000..f7673c0 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/data/ProductRepository.kt @@ -0,0 +1,8 @@ +package nextstep.shoppingcart.data + +import nextstep.shoppingcart.domain.model.Product + +interface ProductRepository { + fun fetchProducts(): List + fun getProduct(id: Long): Product +} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt b/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt new file mode 100644 index 0000000..883470b --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt @@ -0,0 +1,13 @@ +package nextstep.shoppingcart.domain.model + +data class Product( + val id: Long, + val name: String, + val price: Int, + val imageUrl: String, + val isAddedToCart: Boolean = false, +) { + companion object { + val NULL_PRODUCT = Product(-1, "", -1, "", false) + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/MainActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt similarity index 54% rename from app/src/main/java/nextstep/shoppingcart/MainActivity.kt rename to app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt index 5bbb116..be92de0 100644 --- a/app/src/main/java/nextstep/shoppingcart/MainActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt @@ -1,28 +1,29 @@ -package nextstep.shoppingcart +package nextstep.shoppingcart.presentation 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.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold 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 +import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme -class MainActivity : ComponentActivity() { +class DetailActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() setContent { - ShoppingCartTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Greeting("Android") + AndroidshoppingcartTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Greeting( + name = "Android", + modifier = Modifier.padding(innerPadding) + ) } } } @@ -40,7 +41,7 @@ fun Greeting(name: String, modifier: Modifier = Modifier) { @Preview(showBackground = true) @Composable fun GreetingPreview() { - ShoppingCartTheme { + AndroidshoppingcartTheme { Greeting("Android") } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/MainActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/MainActivity.kt new file mode 100644 index 0000000..ed62c60 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/MainActivity.kt @@ -0,0 +1,28 @@ +package nextstep.shoppingcart.presentation + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import nextstep.shoppingcart.data.CachedProductDataSource +import nextstep.shoppingcart.data.CachedProductRepository +import nextstep.shoppingcart.presentation.components.ListLayout +import nextstep.shoppingcart.ui.theme.ShoppingCartTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val repository = CachedProductRepository(CachedProductDataSource()) + + setContent { + ShoppingCartTheme { + // A surface container using the 'background' color from the theme + /*Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + }*/ + ListLayout(products = repository.fetchProducts()) + } + } + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ListLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ListLayout.kt new file mode 100644 index 0000000..58518b0 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ListLayout.kt @@ -0,0 +1,53 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +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.model.Product + +@Composable +fun ListLayout( + products: List, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + TopBar(modifier = Modifier) + ProductList( + items = products, + modifier = Modifier.padding(top = 18.dp, start = 13.dp, end = 13.dp), + ) + } +} + +@Composable +@Preview(showBackground = true) +fun ListLayoutPreview() { + val products = + listOf( + Product( + id = 1, + name = "name1", + price = 1000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png", + ), + Product( + id = 2, + name = "name2", + price = 2000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg", + ), + Product( + id = 3, + name = "name3", + price = 3000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg", + ), + ) + + ListLayout(products = products) +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt new file mode 100644 index 0000000..7b171c2 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt @@ -0,0 +1,90 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +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.Modifier +import androidx.compose.ui.text.TextStyle +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 coil.compose.AsyncImage +import nextstep.shoppingcart.domain.model.Product + +@Composable +fun ProductList( + items: List, + modifier: Modifier = Modifier, +) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.padding(12.dp), + ) { + items(items) { item -> + Item(product = item) + } + } +} + +@Composable +fun Item(product: Product) { + Column( + modifier = Modifier.padding(top = 20.dp, start = 6.dp, end = 6.dp), + ) { + AsyncImage( + model = product.imageUrl, + contentDescription = product.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f), + ) + Text( + text = product.name, + modifier = Modifier.padding(top = 8.dp), + style = + TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ), + ) + Text(text = product.price.toString(), style = TextStyle( + fontSize = 16.sp, + ), modifier = Modifier.padding(top = 2.dp)) + } +} + +@Preview(showBackground = true) +@Composable +fun MyListPreview() { + ProductList( + items = + listOf( + Product( + id = 1, + name = "name1", + price = 1000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png", + ), + Product( + id = 2, + name = "name2", + price = 2000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg", + ), + Product( + id = 3, + name = "name3", + price = 3000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg", + ), + ), + ) +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBar.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBar.kt new file mode 100644 index 0000000..ccb910a --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBar.kt @@ -0,0 +1,52 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.layout.Column +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.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBar( + name: String = "상품목록", + modifier: Modifier = Modifier, +) { + // set text to center + Column { + CenterAlignedTopAppBar( + title = { + Text( + text = name, + style = TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + ) + ) }, + modifier = modifier, + actions = { + IconButton(onClick = {}) { + Icon( + imageVector = Icons.Filled.ShoppingCart, + contentDescription = "Shopping Cart", + ) + } + }, + ) + } +} + +@Preview +@Composable +fun BarLayoutPreview() { + TopBar() +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Color.kt b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Color.kt new file mode 100644 index 0000000..535d1d0 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package nextstep.shoppingcart.presentation.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Theme.kt b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Theme.kt new file mode 100644 index 0000000..21ecdf0 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package nextstep.shoppingcart.presentation.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.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun AndroidshoppingcartTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + 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/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Type.kt b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Type.kt new file mode 100644 index 0000000..f0bdf1e --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package nextstep.shoppingcart.presentation.ui.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 + +// 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 + ) + /* 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/theme/Theme.kt b/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt index 3e5e697..ec6074b 100644 --- a/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt +++ b/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt @@ -42,7 +42,7 @@ 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 -> { @@ -57,6 +57,6 @@ fun ShoppingCartTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = content, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc5b76c..516c1cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Shopping Cart + DetailActivity \ 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..fc3561f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.5.1" +coil = "2.2.2" kotlin = "1.9.24" coreKtx = "1.13.1" junit = "4.13.2" @@ -8,8 +9,11 @@ espressoCore = "3.6.1" lifecycleRuntimeKtx = "2.8.4" activityCompose = "1.9.1" composeBom = "2024.06.00" +ktlint = "11.6.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +coil = { module = "io.coil-kt:coil", version.ref = "coil" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -26,3 +30,4 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3" [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" } From 103821621184af982462f0cb6389fd1f9657a2f3 Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Tue, 5 Nov 2024 15:47:16 +0900 Subject: [PATCH 04/12] docs: update Readme(step2) --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 2a3b567..e032599 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,20 @@ - 상품 목록 화면을 구현할 때 Lazy 컴포넌트를 활용한다. - 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다. - 의미있는 단위의 함수를 모아 별도의 파일로 분리해본다. + + +## Step 2 +### 기능 요구 사항 +- [ ] 상품 상세 화면을 구현한다. +- [ ] 상품 목록에서 상품을 누르면 상품 상세 화면으로 이동한다. +- [ ] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. +- [ ] 장바구니 화면의 빈 껍데기를 연결한다. +- [ ] 상품 목록에서 장바구니 아이콘을 누르면 장바구니 화면으로 이동한다. +- [ ] 상품 상세에서 장바구니 담기 버튼을 누르면 장바구니 화면으로 이동한다. +- [ ] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. +- [ ] 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. + +### 프로그래밍 요구 사항 +- 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. +- 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다. +- 의미있는 단위의 함수를 모아 별도의 파일로 분리해본다. From 29b1d187d12e1b135d27fb44874a5782291823d0 Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Wed, 6 Nov 2024 00:40:18 +0900 Subject: [PATCH 05/12] feat: implement step2 --- README.md | 16 +-- app/src/main/AndroidManifest.xml | 7 +- .../data/CachedProductRepository.kt | 2 +- .../shoppingcart/data/ProductRepository.kt | 2 +- .../shoppingcart/domain/model/Price.kt | 10 ++ .../shoppingcart/domain/model/Product.kt | 2 +- .../shoppingcart/presentation/CartActivity.kt | 34 +++++ .../presentation/DetailActivity.kt | 62 +++++---- .../{MainActivity.kt => HomeActivity.kt} | 16 ++- .../presentation/components/CartLayout.kt | 28 ++++ .../presentation/components/DetailLayout.kt | 122 ++++++++++++++++++ .../{ListLayout.kt => HomeLayout.kt} | 5 +- .../presentation/components/ProductList.kt | 66 ++++++---- .../presentation/components/TopBar.kt | 52 -------- .../components/TopBarWithAction.kt | 96 ++++++++++++++ app/src/main/res/values/strings.xml | 1 + 16 files changed, 402 insertions(+), 119 deletions(-) create mode 100644 app/src/main/java/nextstep/shoppingcart/domain/model/Price.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt rename app/src/main/java/nextstep/shoppingcart/presentation/{MainActivity.kt => HomeActivity.kt} (64%) create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt rename app/src/main/java/nextstep/shoppingcart/presentation/components/{ListLayout.kt => HomeLayout.kt} (91%) delete mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/TopBar.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt diff --git a/README.md b/README.md index e032599..ebfde36 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,14 @@ ## Step 2 ### 기능 요구 사항 -- [ ] 상품 상세 화면을 구현한다. -- [ ] 상품 목록에서 상품을 누르면 상품 상세 화면으로 이동한다. -- [ ] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. -- [ ] 장바구니 화면의 빈 껍데기를 연결한다. -- [ ] 상품 목록에서 장바구니 아이콘을 누르면 장바구니 화면으로 이동한다. -- [ ] 상품 상세에서 장바구니 담기 버튼을 누르면 장바구니 화면으로 이동한다. -- [ ] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. -- [ ] 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. +- [x] 상품 상세 화면을 구현한다. +- [x] 상품 목록에서 상품을 누르면 상품 상세 화면으로 이동한다. +- [x] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. +- [x] 장바구니 화면의 빈 껍데기를 연결한다. +- [x] 상품 목록에서 장바구니 아이콘을 누르면 장바구니 화면으로 이동한다. +- [x] 상품 상세에서 장바구니 담기 버튼을 누르면 장바구니 화면으로 이동한다. +- [x] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. +- [x] 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. ### 프로그래밍 요구 사항 - 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 49e5cfd..c094758 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,13 +14,18 @@ android:supportsRtl="true" android:theme="@style/Theme.ShoppingCart" tools:targetApi="31"> + diff --git a/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt b/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt index c45d652..144597e 100644 --- a/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt +++ b/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt @@ -7,5 +7,5 @@ class CachedProductRepository( ) : ProductRepository { override fun fetchProducts(): List = productDataSource.fetchProducts() - override fun getProduct(id: Long): Product = productDataSource.findProduct(id) + override fun findProduct(id: Long): Product = productDataSource.findProduct(id) } diff --git a/app/src/main/java/nextstep/shoppingcart/data/ProductRepository.kt b/app/src/main/java/nextstep/shoppingcart/data/ProductRepository.kt index f7673c0..88dfa1f 100644 --- a/app/src/main/java/nextstep/shoppingcart/data/ProductRepository.kt +++ b/app/src/main/java/nextstep/shoppingcart/data/ProductRepository.kt @@ -4,5 +4,5 @@ import nextstep.shoppingcart.domain.model.Product interface ProductRepository { fun fetchProducts(): List - fun getProduct(id: Long): Product + fun findProduct(id: Long): Product } diff --git a/app/src/main/java/nextstep/shoppingcart/domain/model/Price.kt b/app/src/main/java/nextstep/shoppingcart/domain/model/Price.kt new file mode 100644 index 0000000..82d568a --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/model/Price.kt @@ -0,0 +1,10 @@ +package nextstep.shoppingcart.domain.model + +import java.text.DecimalFormat + +data class Price (private val amount : Int) { + fun format(): String { + val decimal = DecimalFormat("#,###") + return decimal.format(amount)+"원" + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt b/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt index 883470b..d5bc0ae 100644 --- a/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt +++ b/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt @@ -8,6 +8,6 @@ data class Product( val isAddedToCart: Boolean = false, ) { companion object { - val NULL_PRODUCT = Product(-1, "", -1, "", false) + val NULL_PRODUCT = Product(-1, "Null Product", -1000000, "", false) } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt new file mode 100644 index 0000000..831da66 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt @@ -0,0 +1,34 @@ +package nextstep.shoppingcart.presentation + +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.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.ui.Modifier +import nextstep.shoppingcart.presentation.components.CartLayout +import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme + +class CartActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AndroidshoppingcartTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + CartLayout( + navigation = { finish() }, + modifier = Modifier.padding(innerPadding), + ) + } + } + } + } + + companion object { + fun createIntent(context: Context): Intent = Intent(context, CartActivity::class.java) + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt index be92de0..c52a372 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt @@ -1,47 +1,53 @@ package nextstep.shoppingcart.presentation +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.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -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.data.CachedProductDataSource +import nextstep.shoppingcart.data.CachedProductRepository +import nextstep.shoppingcart.domain.model.Product +import nextstep.shoppingcart.presentation.components.DetailLayout import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme class DetailActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge() setContent { AndroidshoppingcartTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) - } + DetailLayout( + product = fetchProduct(), + navigation = { navigateBack() }, + action = { navigateToCart() }, + ) } } } -} -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} + private fun fetchProduct(): Product { + val productId = + intent.getStringExtra(PRODUCT_ID) + ?: throw IllegalArgumentException("Product ID is required") + val repository = CachedProductRepository(CachedProductDataSource()) + return repository.findProduct(productId.toLong()) + } + + private fun navigateToCart() { + startActivity(CartActivity.createIntent(this)) + } + + private fun navigateBack() = finish() -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - AndroidshoppingcartTheme { - Greeting("Android") + companion object { + private const val PRODUCT_ID = "PRODUCT_ID" + + fun createIntent( + productId: String, + context: Context, + ): Intent = + Intent(context, DetailActivity::class.java).apply { + putExtra(PRODUCT_ID, productId) + } } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/MainActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt similarity index 64% rename from app/src/main/java/nextstep/shoppingcart/presentation/MainActivity.kt rename to app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt index ed62c60..0501b76 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/MainActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt @@ -8,7 +8,7 @@ import nextstep.shoppingcart.data.CachedProductRepository import nextstep.shoppingcart.presentation.components.ListLayout import nextstep.shoppingcart.ui.theme.ShoppingCartTheme -class MainActivity : ComponentActivity() { +class HomeActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val repository = CachedProductRepository(CachedProductDataSource()) @@ -21,8 +21,20 @@ class MainActivity : ComponentActivity() { color = MaterialTheme.colorScheme.background, ) { }*/ - ListLayout(products = repository.fetchProducts()) + ListLayout( + products = repository.fetchProducts(), + action = { navigateToCart() }, + onItemClick = { productId -> navigateToDetail(productId) } + ) } } } + + private fun navigateToCart() { + startActivity(CartActivity.createIntent(this)) + } + + private fun navigateToDetail(productId: Long) { + startActivity(DetailActivity.createIntent(productId.toString(), this)) + } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt new file mode 100644 index 0000000..6f3595a --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt @@ -0,0 +1,28 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun CartLayout( + navigation: () -> Unit, + modifier: Modifier = Modifier, +) { + Column { + Surface { + TopBarWithNavigation( + name = "장바구니", + navigation = navigation, + ) + } + } +} + +@Composable +@Preview(showBackground = true) +fun CartLayoutPreview() { + CartLayout(navigation = {}) +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt new file mode 100644 index 0000000..d44eaba --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt @@ -0,0 +1,122 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider +import androidx.compose.material3.Scaffold +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.TextStyle +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 coil.compose.AsyncImage +import nextstep.shoppingcart.domain.model.Price +import nextstep.shoppingcart.domain.model.Product + +@Composable +fun DetailLayout( + navigation: () -> Unit = {}, + action: () -> Unit = {}, + product: Product, + modifier: Modifier = Modifier, +) { + Scaffold( + bottomBar = { + Box( + modifier = + Modifier + .fillMaxWidth() + .background(Color(0xFF2196F3)) + .padding(16.dp) + .clickable { action() }, + contentAlignment = Alignment.Center, + ) { + Text( + text = "장바구니 담기", + fontSize = 20.sp, + style = + TextStyle( + color = Color.White, + fontWeight = FontWeight.Bold, + ), + ) + } + }, + ) { innerPadding -> + Column(modifier = modifier.padding(innerPadding)) { + TopBarWithNavigation( + name = "상품 상세", + navigation = { navigation() }, + modifier = modifier, + ) + AsyncImage( + model = product.imageUrl, + contentDescription = product.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f), + ) + Text( + text = product.name, + modifier = + Modifier + .fillMaxWidth() + .padding(18.dp), + style = + TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + ), + ) + + Divider( + color = Color.Gray, + thickness = 1.dp, + modifier = + Modifier + .fillMaxWidth(), + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "금액", + fontSize = 20.sp, + modifier = + Modifier + .padding(18.dp) + .align(Alignment.CenterVertically), + ) + Text( + text = Price(product.price).format(), + fontSize = 20.sp, + modifier = + Modifier + .padding(18.dp) + .align(Alignment.CenterVertically), + ) + } + } + } +} + +@Composable +@Preview(showBackground = true) +fun DetailLayoutPreview() { + DetailLayout(product = Product.NULL_PRODUCT) +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ListLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt similarity index 91% rename from app/src/main/java/nextstep/shoppingcart/presentation/components/ListLayout.kt rename to app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt index 58518b0..03ee7e0 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/ListLayout.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt @@ -11,14 +11,17 @@ import nextstep.shoppingcart.domain.model.Product @Composable fun ListLayout( products: List, + action: () -> Unit = {}, + onItemClick: (Long) -> Unit = {}, modifier: Modifier = Modifier, ) { Column( modifier = modifier, ) { - TopBar(modifier = Modifier) + TopBarWithAction(modifier = Modifier, action = action) ProductList( items = products, + onItemClick = onItemClick, modifier = Modifier.padding(top = 18.dp, start = 13.dp, end = 13.dp), ) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt index 7b171c2..ef1962a 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt @@ -7,6 +7,7 @@ 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.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -21,6 +22,7 @@ import nextstep.shoppingcart.domain.model.Product @Composable fun ProductList( items: List, + onItemClick: (Long) -> Unit = {}, modifier: Modifier = Modifier, ) { LazyVerticalGrid( @@ -28,36 +30,52 @@ fun ProductList( modifier = Modifier.padding(12.dp), ) { items(items) { item -> - Item(product = item) + Item( + product = item, + onItemClick = onItemClick, + ) } } } @Composable -fun Item(product: Product) { - Column( - modifier = Modifier.padding(top = 20.dp, start = 6.dp, end = 6.dp), +fun Item( + product: Product, + onItemClick: (Long) -> Unit, +) { + Surface( + modifier = Modifier.fillMaxWidth(), + onClick = { onItemClick(product.id) }, ) { - AsyncImage( - model = product.imageUrl, - contentDescription = product.name, - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f), - ) - Text( - text = product.name, - modifier = Modifier.padding(top = 8.dp), - style = - TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - ), - ) - Text(text = product.price.toString(), style = TextStyle( - fontSize = 16.sp, - ), modifier = Modifier.padding(top = 2.dp)) + Column( + modifier = Modifier.padding(top = 20.dp, start = 6.dp, end = 6.dp), + ) { + AsyncImage( + model = product.imageUrl, + contentDescription = product.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f), + ) + Text( + text = product.name, + modifier = Modifier.padding(top = 8.dp), + style = + TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + ), + ) + Text( + text = product.price.toString(), + style = + TextStyle( + fontSize = 16.sp, + ), + modifier = Modifier.padding(top = 2.dp), + ) + } } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBar.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBar.kt deleted file mode 100644 index ccb910a..0000000 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBar.kt +++ /dev/null @@ -1,52 +0,0 @@ -package nextstep.shoppingcart.presentation.components - -import androidx.compose.foundation.layout.Column -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.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TopBar( - name: String = "상품목록", - modifier: Modifier = Modifier, -) { - // set text to center - Column { - CenterAlignedTopAppBar( - title = { - Text( - text = name, - style = TextStyle( - fontSize = 22.sp, - fontWeight = FontWeight.Bold, - ) - ) }, - modifier = modifier, - actions = { - IconButton(onClick = {}) { - Icon( - imageVector = Icons.Filled.ShoppingCart, - contentDescription = "Shopping Cart", - ) - } - }, - ) - } -} - -@Preview -@Composable -fun BarLayoutPreview() { - TopBar() -} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt new file mode 100644 index 0000000..10c4bd4 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt @@ -0,0 +1,96 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowBack +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.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +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 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBarWithAction( + name: String = "상품목록", + action: () -> Unit = {}, + modifier: Modifier = Modifier, +) { + Column { + CenterAlignedTopAppBar( + title = { + Text( + text = name, + style = + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + ), + ) + }, + modifier = modifier, + actions = { + IconButton(onClick = { action() }) { + Icon( + imageVector = Icons.Filled.ShoppingCart, + contentDescription = "Shopping Cart", + ) + } + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBarWithNavigation( + name: String = "상품 상세", + navigation: () -> Unit = {}, + modifier: Modifier = Modifier, +) { + Column { + TopAppBar( + title = { + Text( + text = name, + style = + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + ), + ) + }, + modifier = modifier, + navigationIcon = { + IconButton( + onClick = { navigation() }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + ) + } + }, + ) + } +} + +@Preview +@Composable +fun BarLayoutPreview() { + Column { + TopBarWithAction() + TopBarWithNavigation(name = "상품 상세") + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 516c1cc..8fb61f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ Shopping Cart DetailActivity + CartActivity \ No newline at end of file From efdf548e124a96a05aa55a63ecc8ecba7f23f04e Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Wed, 6 Nov 2024 00:44:32 +0900 Subject: [PATCH 06/12] docs: update README(step3) --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index ebfde36..06ac66c 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,16 @@ - 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. - 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다. - 의미있는 단위의 함수를 모아 별도의 파일로 분리해본다. + + +## Step3 +### 기능 요구 사항 +- [ ] 상품을 장바구니에 담는 기능을 구현한다. +- [ ] 장바구니 화면을 구현한다. + - [ ] 담긴 상품의 수량을 조절할 수 있어야 한다. + - [ ] 수량을 1보다 작게 하면 장바구니에서 상품이 제거된다 + - [ ] 담긴 상품 가격의 총합이 주문하기 버튼에 표시된다. + +### 프로그래밍 요구 사항 +- 상품을 주문하는 기능에 대해서는 구현하지 않아도 된다. +- 장바구니 화면에 대한 테스트 코드를 작성한다. From bb07f93f6858609349e46a3999708eb58df9011d Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Sun, 10 Nov 2024 20:01:31 +0900 Subject: [PATCH 07/12] feat: implement feature(step3) --- README.md | 12 +- app/build.gradle.kts | 1 + .../data/CachedProductDataSource.kt | 22 +- .../data/CachedProductRepository.kt | 2 +- .../java/nextstep/shoppingcart/data/Cart.kt | 41 ++++ .../shoppingcart/domain/model/CartItem.kt | 8 + .../shoppingcart/domain/model/Price.kt | 6 +- .../shoppingcart/domain/model/Product.kt | 2 +- .../shoppingcart/presentation/CartActivity.kt | 33 ++- .../presentation/DetailActivity.kt | 13 +- .../presentation/components/CartItemList.kt | 195 ++++++++++++++++++ .../presentation/components/CartLayout.kt | 51 ++++- .../presentation/components/CounterForm.kt | 73 +++++++ .../presentation/components/DetailLayout.kt | 74 +++---- .../presentation/components/HomeLayout.kt | 16 +- .../presentation/components/ProductList.kt | 77 +++---- .../components/TopBarWithAction.kt | 38 ++-- .../presentation/ui/theme/Color.kt | 2 +- .../presentation/ui/theme/Theme.kt | 1 - .../presentation/ui/theme/Type.kt | 2 +- .../nextstep/shoppingcart/ui/theme/Color.kt | 2 +- .../nextstep/shoppingcart/ui/theme/Theme.kt | 9 +- .../nextstep/shoppingcart/ui/theme/Type.kt | 2 +- app/src/main/res/values/strings.xml | 10 +- 24 files changed, 547 insertions(+), 145 deletions(-) create mode 100644 app/src/main/java/nextstep/shoppingcart/data/Cart.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/CounterForm.kt diff --git a/README.md b/README.md index 06ac66c..4f8a51e 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,12 @@ ## Step3 ### 기능 요구 사항 -- [ ] 상품을 장바구니에 담는 기능을 구현한다. -- [ ] 장바구니 화면을 구현한다. - - [ ] 담긴 상품의 수량을 조절할 수 있어야 한다. - - [ ] 수량을 1보다 작게 하면 장바구니에서 상품이 제거된다 - - [ ] 담긴 상품 가격의 총합이 주문하기 버튼에 표시된다. +- [x] 상품을 장바구니에 담는 기능을 구현한다. +- [x] 장바구니 화면을 구현한다. + - [x] 담긴 상품의 수량을 조절할 수 있어야 한다. + - [x] 수량을 1보다 작게 하면 장바구니에서 상품이 제거된다 + - [x] 담긴 상품 가격의 총합이 주문하기 버튼에 표시된다. ### 프로그래밍 요구 사항 - 상품을 주문하는 기능에 대해서는 구현하지 않아도 된다. -- 장바구니 화면에 대한 테스트 코드를 작성한다. +- ~~장바구니 화면에 대한 테스트 코드를 작성한다.~~ 😅 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1591f5c..6b6c051 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 { diff --git a/app/src/main/java/nextstep/shoppingcart/data/CachedProductDataSource.kt b/app/src/main/java/nextstep/shoppingcart/data/CachedProductDataSource.kt index 360e1c2..ffe39ba 100644 --- a/app/src/main/java/nextstep/shoppingcart/data/CachedProductDataSource.kt +++ b/app/src/main/java/nextstep/shoppingcart/data/CachedProductDataSource.kt @@ -9,56 +9,56 @@ class CachedProductDataSource : ProductDataSource { id = 1, name = "PET보틀-정사각형 용기 (1L)", price = 10_000, - imageUrl = "https://s3-alpha-sig.figma.com/img/05ef/e578/d81445480aff1872344a6b1b35323488?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=KNMX7pZxzQz578ovhuWIBF2ilsQACu~sph~2fwFZyhO3pX9vhCd-kXHJmW5lpogb2IhP6bvH1N8qCKX~jHluc-tDSLD4EbUsGI9yJqanYsdOSRY6KKPyrh1SekXEH8u4-A77xal1TEj99wTnMMGKGXlI4UJhQJhg6hhBRL3ONv5Y1N5~2yMw4M54bGJi4HQYh3eG5rcPrK96KWonhd7BtZDwo4MMwK1TQ0FjuIBMdoabOACAwRwzE-EJ8znP71oTiOsZES6wASv21meilUhN~A8C72-qqNhyybuDTNqZQfQIHWT7kh-Lm0McKRQUg7~OSs3nWVJR7O-USQCwHhhclQ__", + imageUrl = "https://s3-alpha-sig.figma.com/img/05ef/e578/d81445480aff1872344a6b1b35323488?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=KNMX7pZxzQz578ovhuWIBF2ilsQACu~sph~2fwFZyhO3pX9vhCd-kXHJmW5lpogb2IhP6bvH1N8qCKX~jHluc-tDSLD4EbUsGI9yJqanYsdOSRY6KKPyrh1SekXEH8u4-A77xal1TEj99wTnMMGKGXlI4UJhQJhg6hhBRL3ONv5Y1N5~2yMw4M54bGJi4HQYh3eG5rcPrK96KWonhd7BtZDwo4MMwK1TQ0FjuIBMdoabOACAwRwzE-EJ8znP71oTiOsZES6wASv21meilUhN~A8C72-qqNhyybuDTNqZQfQIHWT7kh-Lm0McKRQUg7~OSs3nWVJR7O-USQCwHhhclQ__" ), Product( id = 2, name = "PET보틀-밀크티 용기", price = 12_000, - imageUrl = "https://s3-alpha-sig.figma.com/img/f081/71c9/4a1459fb4310f704c34be19a15f662a4?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ZKYEUR3xhp1aA4aULlmTfL1WE3xPvkK14FKZ6ih6JJyFdt54d3srSsB3OG2qUffmjzxsN-SgL4vtdmJgAyVeUh4JZlgBrrDOeBnsvqx3E3myqzO3iBZMsqG54zici8-pht8vl22MhbV7rzqNA5WSbSGbVztUhS1J8Rm9u9wDH5RY9vfbf-92vvfkRzakG84NghCmq~8fNusILMtEMlUBM~aY02owPo7MIsDLmwHBhGSThU5mnNCawKmmZc1ESwvjzeYhgY6HY~U9v24M1nlKL6KFA4wu8KuIfDv9G1-pve6P3i4DZKbgir19Q46pHGp0VErbA2yNhpWwu4MvYKJ~5w__", + imageUrl = "https://s3-alpha-sig.figma.com/img/f081/71c9/4a1459fb4310f704c34be19a15f662a4?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=ZKYEUR3xhp1aA4aULlmTfL1WE3xPvkK14FKZ6ih6JJyFdt54d3srSsB3OG2qUffmjzxsN-SgL4vtdmJgAyVeUh4JZlgBrrDOeBnsvqx3E3myqzO3iBZMsqG54zici8-pht8vl22MhbV7rzqNA5WSbSGbVztUhS1J8Rm9u9wDH5RY9vfbf-92vvfkRzakG84NghCmq~8fNusILMtEMlUBM~aY02owPo7MIsDLmwHBhGSThU5mnNCawKmmZc1ESwvjzeYhgY6HY~U9v24M1nlKL6KFA4wu8KuIfDv9G1-pve6P3i4DZKbgir19Q46pHGp0VErbA2yNhpWwu4MvYKJ~5w__" ), Product( id = 3, name = "PET보틀-정사각형 용기 (500ml)", price = 10_000, - imageUrl = "https://s3-alpha-sig.figma.com/img/ff9f/d83e/b40e670bfc38dbcddbcec8b3ca363d50?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=DzhgzJzS4g6I3SO2RtuhKgWdng1dPjM2aPUASlWNq8VvjOZMd9tfYaruZLALMCKtx~cmq~Q6~hWr1vAnbHBZsRW~8EIwneiuNLE-fxFuTjFBV24G2u8LpY5n91oMgGYzNqxtuTVVqE28~GUhDHsxQt5BL5tUukHeER0-mUg4wUP-4uWA-zN9m864LZwYpTLxzcNCOAuvgFAR2HHsGMJCDyC~Fzd7lSLVuFQVwxF7kolB9ErqqVUA2SO0wjfCoHoWJUwuTlSr9jSGW4-h4lJafTpGvYJCS4foasIYy3ODeeOgBOCgnXkHdrJivZlkR0gNPCOOx2x~KDWV547AsrO5YA__", + imageUrl = "https://s3-alpha-sig.figma.com/img/ff9f/d83e/b40e670bfc38dbcddbcec8b3ca363d50?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=DzhgzJzS4g6I3SO2RtuhKgWdng1dPjM2aPUASlWNq8VvjOZMd9tfYaruZLALMCKtx~cmq~Q6~hWr1vAnbHBZsRW~8EIwneiuNLE-fxFuTjFBV24G2u8LpY5n91oMgGYzNqxtuTVVqE28~GUhDHsxQt5BL5tUukHeER0-mUg4wUP-4uWA-zN9m864LZwYpTLxzcNCOAuvgFAR2HHsGMJCDyC~Fzd7lSLVuFQVwxF7kolB9ErqqVUA2SO0wjfCoHoWJUwuTlSr9jSGW4-h4lJafTpGvYJCS4foasIYy3ODeeOgBOCgnXkHdrJivZlkR0gNPCOOx2x~KDWV547AsrO5YA__" ), Product( id = 4, name = "PET보틀-납작 용기 (500ml)", price = 12_000, - imageUrl = "https://s3-alpha-sig.figma.com/img/b8b6/a740/d8661b91e8210779ce9db930d260230d?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=lYXFy7NZnn2Pe7vn29qkin2d9HPRXteVaHuLXIH2pxjspZP1GMyr8d9It1WcYND8apF~LTd13z-ckQ9WrZO-ExycyxF6H1oVSOQmDQfxAgzpGDfmJlNiEh7BoFllbnQVPsHwfFYaGxSFr93zYkzNKfwQGUTltozO8fElpK0dQePtyIzuZpmyHegnizh~hqdlx9YlyJdfeuV-AlAtyKZjSFDcphz3RJ5YujI0xzf5XEGoYGBN5piSwr-41UWXY664LcmUVRZF9Ge8hZdyV1vu2Coty54x7ns5FbTu3GUhcZ3bSrTYaQcDzlvggtYXel~SPoV20d8xPwXMFRCbSuUxhw__", + imageUrl = "https://s3-alpha-sig.figma.com/img/b8b6/a740/d8661b91e8210779ce9db930d260230d?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=lYXFy7NZnn2Pe7vn29qkin2d9HPRXteVaHuLXIH2pxjspZP1GMyr8d9It1WcYND8apF~LTd13z-ckQ9WrZO-ExycyxF6H1oVSOQmDQfxAgzpGDfmJlNiEh7BoFllbnQVPsHwfFYaGxSFr93zYkzNKfwQGUTltozO8fElpK0dQePtyIzuZpmyHegnizh~hqdlx9YlyJdfeuV-AlAtyKZjSFDcphz3RJ5YujI0xzf5XEGoYGBN5piSwr-41UWXY664LcmUVRZF9Ge8hZdyV1vu2Coty54x7ns5FbTu3GUhcZ3bSrTYaQcDzlvggtYXel~SPoV20d8xPwXMFRCbSuUxhw__" ), Product( id = 5, - name= "PET보틀-원형 용기 (500ml)", + name = "PET보틀-원형 용기 (500ml)", price = 10_000, - imageUrl = "https://s3-alpha-sig.figma.com/img/8f83/c0e8/c17365cdfe5a00fa17c0283d520f4a99?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=Pt91VGcwcypqZ0ZiBhcXNVlgJ7x7yrNsh0ucdfSZ1Ct12CR0eTe7G4a4doBtthWGWIL-Tmj4VYvqbMNMT3McEbGmgR~jvfIYhAO1rQaSO6Ry69kSV1K3qpaqNyW7WkfoKi2-qj4w9665rOYY~ss-H3Le7YAkpnbEKbZUBfGFodaHSEwRplcT5V~ECB65DGoevOqMg4ArLdtJSDNpOdbb166Q8LSzcmZ0sOksimWDBLKegKjWtwi30Fe6WBJzmLD5i7TBXpxKgOOlL92M0zT~kgLIAIEM2C4g8qvJWqE74d4NX~y2c-AsU7pcLQqrHOU7oElPyPXhlnZkxvPd86cyIw__", + imageUrl = "https://s3-alpha-sig.figma.com/img/8f83/c0e8/c17365cdfe5a00fa17c0283d520f4a99?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=Pt91VGcwcypqZ0ZiBhcXNVlgJ7x7yrNsh0ucdfSZ1Ct12CR0eTe7G4a4doBtthWGWIL-Tmj4VYvqbMNMT3McEbGmgR~jvfIYhAO1rQaSO6Ry69kSV1K3qpaqNyW7WkfoKi2-qj4w9665rOYY~ss-H3Le7YAkpnbEKbZUBfGFodaHSEwRplcT5V~ECB65DGoevOqMg4ArLdtJSDNpOdbb166Q8LSzcmZ0sOksimWDBLKegKjWtwi30Fe6WBJzmLD5i7TBXpxKgOOlL92M0zT~kgLIAIEM2C4g8qvJWqE74d4NX~y2c-AsU7pcLQqrHOU7oElPyPXhlnZkxvPd86cyIw__" ), Product( id = 6, name = "PET보틀-납작 용기 (200mL)", price = 12_000, - imageUrl = "https://s3-alpha-sig.figma.com/img/b9f2/403d/b915b1b22edac0877abb7b97129296b6?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=YUOtsuzD0QhxPGoaGPNVb3IfJu015CI0LkvUaywB3qcTHOmBclM6YsRCba5lGrMq3PY5MbG7U4VPbbhq8z5RBcGyxIb9M31qTYIEYTstE1-C2zq0IR3YXwMqaV5t9A7hjClBnoZl~jT2DTH4k5YxrIRy2D~qqR57C-4TF-Doo3Xhs~jNvRCyYYZBvFqg0PerspsQHnruNK9sJCVvWB4dC9DN290uU7jmbOrO6ku0SCtFp7BSCAP2l4FwW3LcIGAJpPRZSAqZu-z-n8QnEFJnWWDBqGynQqQQq9jQqxEW15gOna8a3X0nTM4RrTR6gZX2aKWMhOtRcciMoLwaiar5bw__", + imageUrl = "https://s3-alpha-sig.figma.com/img/b9f2/403d/b915b1b22edac0877abb7b97129296b6?Expires=1731888000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=YUOtsuzD0QhxPGoaGPNVb3IfJu015CI0LkvUaywB3qcTHOmBclM6YsRCba5lGrMq3PY5MbG7U4VPbbhq8z5RBcGyxIb9M31qTYIEYTstE1-C2zq0IR3YXwMqaV5t9A7hjClBnoZl~jT2DTH4k5YxrIRy2D~qqR57C-4TF-Doo3Xhs~jNvRCyYYZBvFqg0PerspsQHnruNK9sJCVvWB4dC9DN290uU7jmbOrO6ku0SCtFp7BSCAP2l4FwW3LcIGAJpPRZSAqZu-z-n8QnEFJnWWDBqGynQqQQq9jQqxEW15gOna8a3X0nTM4RrTR6gZX2aKWMhOtRcciMoLwaiar5bw__" ), Product( id = 7, name = "name1", price = 1000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png", + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png" ), Product( id = 8, name = "name2", price = 2000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg", + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg" ), Product( id = 9, name = "name3", price = 3000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg", - ), + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg" + ) ) override fun fetchProducts(): List = products.toList() diff --git a/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt b/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt index 144597e..d8105f0 100644 --- a/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt +++ b/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt @@ -3,7 +3,7 @@ package nextstep.shoppingcart.data import nextstep.shoppingcart.domain.model.Product class CachedProductRepository( - private val productDataSource: ProductDataSource, + private val productDataSource: ProductDataSource ) : ProductRepository { override fun fetchProducts(): List = productDataSource.fetchProducts() diff --git a/app/src/main/java/nextstep/shoppingcart/data/Cart.kt b/app/src/main/java/nextstep/shoppingcart/data/Cart.kt new file mode 100644 index 0000000..9721fbd --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/data/Cart.kt @@ -0,0 +1,41 @@ +package nextstep.shoppingcart.data + +import nextstep.shoppingcart.domain.model.CartItem +import nextstep.shoppingcart.domain.model.Product + +object Cart { + private val _items: MutableList = mutableListOf() + val items: List get() = _items.toList() + + val totalPrice: Int get() = _items.sumOf { it.totalPrice } + + fun addOne(product: Product): List { + val item = _items.find { it.product == product } + if (item == null) { + _items.add(CartItem(product, 1)) + } else { + val index = _items.indexOf(item) + _items[index] = item.copy(count = item.count + 1) + } + return items + } + + fun removeOne(product: Product): List { + _items + .find { it.product == product } + ?.let { item -> + if (item.count > 1) { + val index = _items.indexOf(item) + _items[index] = item.copy(count = item.count - 1) + } else { + _items.remove(item) + } + } + return items + } + + fun removeAll(product: Product): List { + _items.removeAll { it.product == product } + return items + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt b/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt new file mode 100644 index 0000000..89f0dde --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt @@ -0,0 +1,8 @@ +package nextstep.shoppingcart.domain.model + +data class CartItem( + val product: Product, + val count: Int +) { + val totalPrice: Int get() = product.price * count +} diff --git a/app/src/main/java/nextstep/shoppingcart/domain/model/Price.kt b/app/src/main/java/nextstep/shoppingcart/domain/model/Price.kt index 82d568a..550c58b 100644 --- a/app/src/main/java/nextstep/shoppingcart/domain/model/Price.kt +++ b/app/src/main/java/nextstep/shoppingcart/domain/model/Price.kt @@ -2,9 +2,11 @@ package nextstep.shoppingcart.domain.model import java.text.DecimalFormat -data class Price (private val amount : Int) { +data class Price( + private val amount: Int +) { fun format(): String { val decimal = DecimalFormat("#,###") - return decimal.format(amount)+"원" + return decimal.format(amount) + "원" } } diff --git a/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt b/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt index d5bc0ae..c6b20d3 100644 --- a/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt +++ b/app/src/main/java/nextstep/shoppingcart/domain/model/Product.kt @@ -5,7 +5,7 @@ data class Product( val name: String, val price: Int, val imageUrl: String, - val isAddedToCart: Boolean = false, + val isAddedToCart: Boolean = false ) { companion object { val NULL_PRODUCT = Product(-1, "Null Product", -1000000, "", false) diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt index 831da66..2dd7acf 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt @@ -5,11 +5,16 @@ 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.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold +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 nextstep.shoppingcart.data.Cart +import nextstep.shoppingcart.domain.model.CartItem import nextstep.shoppingcart.presentation.components.CartLayout import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme @@ -17,17 +22,43 @@ class CartActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + var cartItems by remember { mutableStateOf(Cart.items) } AndroidshoppingcartTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> CartLayout( navigation = { finish() }, modifier = Modifier.padding(innerPadding), + cartItems = cartItems, + onIncrease = { + onIncrease(it) + cartItems = Cart.items + }, + onDecrease = { + onDecrease(it) + cartItems = Cart.items + }, + onClear = { + onRemove(it) + cartItems = Cart.items + } ) } } } } + private fun onIncrease(item: CartItem) { + Cart.addOne(item.product) + } + + private fun onDecrease(item: CartItem) { + Cart.removeOne(item.product) + } + + private fun onRemove(item: CartItem) { + Cart.removeAll(item.product) + } + companion object { fun createIntent(context: Context): Intent = Intent(context, CartActivity::class.java) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt index c52a372..233cc51 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt @@ -7,6 +7,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import nextstep.shoppingcart.data.CachedProductDataSource import nextstep.shoppingcart.data.CachedProductRepository +import nextstep.shoppingcart.data.Cart import nextstep.shoppingcart.domain.model.Product import nextstep.shoppingcart.presentation.components.DetailLayout import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme @@ -14,12 +15,13 @@ import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme class DetailActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val product = fetchProduct() setContent { AndroidshoppingcartTheme { DetailLayout( - product = fetchProduct(), + product = product, navigation = { navigateBack() }, - action = { navigateToCart() }, + action = { addToCart(product = product) } ) } } @@ -33,6 +35,11 @@ class DetailActivity : ComponentActivity() { return repository.findProduct(productId.toLong()) } + private fun addToCart(product: Product) { + Cart.addOne(product) + navigateToCart() + } + private fun navigateToCart() { startActivity(CartActivity.createIntent(this)) } @@ -44,7 +51,7 @@ class DetailActivity : ComponentActivity() { fun createIntent( productId: String, - context: Context, + context: Context ): Intent = Intent(context, DetailActivity::class.java).apply { putExtra(PRODUCT_ID, productId) diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt new file mode 100644 index 0000000..59eba2b --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt @@ -0,0 +1,195 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +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.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +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.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +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 coil.compose.AsyncImage +import nextstep.shoppingcart.domain.model.CartItem +import nextstep.shoppingcart.domain.model.Price +import nextstep.shoppingcart.domain.model.Product +import nextstep.signup.R + +@Composable +fun ShoppingCartScreen( + items: List = emptyList(), + onIncrease: (CartItem) -> Unit = {}, + onDecrease: (CartItem) -> Unit = {}, + onClear: (CartItem) -> Unit = {}, + modifier: Modifier = Modifier +) { + val totalPrice = items.sumOf { it.product.price * it.count } + + Scaffold( + bottomBar = { + Box( + modifier = + Modifier + .fillMaxWidth() + .background(Color(0xFF2196F3)) + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.order, Price(totalPrice).format()), + fontSize = 20.sp, + style = + TextStyle( + color = Color.White, + fontWeight = FontWeight.Bold + ) + ) + } + } + ) { innerPadding -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + LazyColumn { + items(items) { item -> + CartItemView( + item = item, + onIncrease = onIncrease, + onDecrease = onDecrease, + onClear = onClear + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + } + } +} + +@Composable +fun CartItemView( + item: CartItem, + onIncrease: (CartItem) -> Unit = {}, + onDecrease: (CartItem) -> Unit = {}, + onClear: (CartItem) -> Unit = {}, + modifier: Modifier = Modifier +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(8.dp) + .border(1.dp, Color.Gray, shape = RoundedCornerShape(4.dp)), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier.padding(horizontal = 18.dp, vertical = 12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + ) { + Text( + text = item.product.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = + TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Bold + ) + ) + Spacer(modifier = Modifier.weight(1f)) + IconButton( + onClick = { onClear(item) } + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "Clear" + ) + } + } + Row { + AsyncImage( + model = item.product.imageUrl, + contentDescription = item.product.name, + modifier = + Modifier + .width(136.dp) + .height(84.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.Bottom + ) { + Text( + text = Price(item.product.price).format(), + fontSize = 16.sp + ) + CounterForm( + count = item.count, + onIncrease = { onIncrease(item) }, + onDecrease = { onDecrease(item) } + ) + } + } + } + } +} + +@Composable +@Preview(showBackground = true) +private fun ShoppingCartPreview() { + val items = + listOf( + CartItem( + Product( + id = 1L, + name = "[든든] 동원 스위트콘", + price = 42200, + imageUrl = "https://thumbnail7" + ), + count = 1 + ), + CartItem( + Product( + id = 2L, + name = "PET보틀-원형(500ml)", + price = 84400, + imageUrl = "" + ), + count = 2 + ) + ) + ShoppingCartScreen(items) +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt index 6f3595a..eff0b55 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt @@ -4,25 +4,62 @@ import androidx.compose.foundation.layout.Column import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import nextstep.shoppingcart.domain.model.CartItem +import nextstep.shoppingcart.domain.model.Product +import nextstep.signup.R @Composable fun CartLayout( navigation: () -> Unit, - modifier: Modifier = Modifier, + cartItems: List, + onIncrease: (CartItem) -> Unit = {}, + onDecrease: (CartItem) -> Unit = {}, + onClear: (CartItem) -> Unit = {}, + modifier: Modifier = Modifier ) { Column { Surface { - TopBarWithNavigation( - name = "장바구니", - navigation = navigation, - ) + Column { + TopBarWithNavigation( + name = stringResource(R.string.title_shopping_cart), + navigation = navigation + ) + ShoppingCartScreen( + items = cartItems, + onIncrease = onIncrease, + onDecrease = onDecrease, + onClear = onClear + ) + } } } } @Composable @Preview(showBackground = true) -fun CartLayoutPreview() { - CartLayout(navigation = {}) +private fun CartLayoutPreview() { + val items = + listOf( + CartItem( + Product( + id = 1L, + name = "[든든] 동원 스위트콘", + price = 42200, + imageUrl = "https://thumbnail7" + ), + count = 1 + ), + CartItem( + Product( + id = 2L, + name = "PET보틀-원형(500ml)", + price = 84400, + imageUrl = "" + ), + count = 2 + ) + ) + CartLayout(navigation = {}, cartItems = items) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CounterForm.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CounterForm.kt new file mode 100644 index 0000000..8a1f4a6 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CounterForm.kt @@ -0,0 +1,73 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +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.stringResource +import androidx.compose.ui.text.TextStyle +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 nextstep.signup.R + +@Composable +fun CounterForm( + count: Int, + onIncrease: () -> Unit, + onDecrease: () -> Unit, + modifier: Modifier = Modifier +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = + Modifier + .clickable { onDecrease() } + ) { + Text( + text = stringResource(R.string.minus_mark), + style = + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold + ), + modifier = Modifier.padding(12.dp) + ) + } + Text( + text = count.toString(), + modifier = Modifier.padding(horizontal = 8.dp), + style = TextStyle(fontSize = 22.sp) + ) + Box( + modifier = + Modifier + .clickable { onIncrease() } + ) { + Text( + text = stringResource(R.string.plus_mark), + style = + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold + ), + modifier = Modifier.padding(12.dp) + ) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun CounterFormPreview() { + CounterForm( + count = 1, + onIncrease = {}, + onDecrease = {} + ) +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt index d44eaba..f331696 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt @@ -16,6 +16,7 @@ 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.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -24,91 +25,92 @@ import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import nextstep.shoppingcart.domain.model.Price import nextstep.shoppingcart.domain.model.Product +import nextstep.signup.R @Composable fun DetailLayout( navigation: () -> Unit = {}, action: () -> Unit = {}, product: Product, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Scaffold( bottomBar = { Box( modifier = - Modifier - .fillMaxWidth() - .background(Color(0xFF2196F3)) - .padding(16.dp) - .clickable { action() }, - contentAlignment = Alignment.Center, + Modifier + .fillMaxWidth() + .background(Color(0xFF2196F3)) + .padding(16.dp) + .clickable { action() }, + contentAlignment = Alignment.Center ) { Text( - text = "장바구니 담기", + text = stringResource(R.string.add_to_cart), fontSize = 20.sp, style = - TextStyle( - color = Color.White, - fontWeight = FontWeight.Bold, - ), + TextStyle( + color = Color.White, + fontWeight = FontWeight.Bold + ) ) } - }, + } ) { innerPadding -> Column(modifier = modifier.padding(innerPadding)) { TopBarWithNavigation( - name = "상품 상세", + name = stringResource(R.string.title_product_detail), navigation = { navigation() }, - modifier = modifier, + modifier = modifier ) AsyncImage( model = product.imageUrl, contentDescription = product.name, modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f), + Modifier + .fillMaxWidth() + .aspectRatio(1f) ) Text( text = product.name, modifier = - Modifier - .fillMaxWidth() - .padding(18.dp), + Modifier + .fillMaxWidth() + .padding(18.dp), style = - TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - ), + TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) ) Divider( color = Color.Gray, thickness = 1.dp, modifier = - Modifier - .fillMaxWidth(), + Modifier + .fillMaxWidth() ) Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.SpaceBetween ) { Text( - text = "금액", + text = stringResource(R.string.price), fontSize = 20.sp, modifier = - Modifier - .padding(18.dp) - .align(Alignment.CenterVertically), + Modifier + .padding(18.dp) + .align(Alignment.CenterVertically) ) Text( text = Price(product.price).format(), fontSize = 20.sp, modifier = - Modifier - .padding(18.dp) - .align(Alignment.CenterVertically), + Modifier + .padding(18.dp) + .align(Alignment.CenterVertically) ) } } @@ -117,6 +119,6 @@ fun DetailLayout( @Composable @Preview(showBackground = true) -fun DetailLayoutPreview() { +private fun DetailLayoutPreview() { DetailLayout(product = Product.NULL_PRODUCT) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt index 03ee7e0..d19e0ab 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt @@ -13,43 +13,43 @@ fun ListLayout( products: List, action: () -> Unit = {}, onItemClick: (Long) -> Unit = {}, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Column( - modifier = modifier, + modifier = modifier ) { TopBarWithAction(modifier = Modifier, action = action) ProductList( items = products, onItemClick = onItemClick, - modifier = Modifier.padding(top = 18.dp, start = 13.dp, end = 13.dp), + modifier = Modifier.padding(top = 18.dp, start = 13.dp, end = 13.dp) ) } } @Composable @Preview(showBackground = true) -fun ListLayoutPreview() { +private fun ListLayoutPreview() { val products = listOf( Product( id = 1, name = "name1", price = 1000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png", + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png" ), Product( id = 2, name = "name2", price = 2000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg", + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg" ), Product( id = 3, name = "name3", price = 3000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg", - ), + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg" + ) ) ListLayout(products = products) diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt index ef1962a..60ac7c4 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight +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 @@ -23,16 +24,16 @@ import nextstep.shoppingcart.domain.model.Product fun ProductList( items: List, onItemClick: (Long) -> Unit = {}, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { LazyVerticalGrid( columns = GridCells.Fixed(2), - modifier = Modifier.padding(12.dp), + modifier = Modifier.padding(12.dp) ) { items(items) { item -> Item( product = item, - onItemClick = onItemClick, + onItemClick = onItemClick ) } } @@ -41,39 +42,41 @@ fun ProductList( @Composable fun Item( product: Product, - onItemClick: (Long) -> Unit, + onItemClick: (Long) -> Unit ) { Surface( modifier = Modifier.fillMaxWidth(), - onClick = { onItemClick(product.id) }, + onClick = { onItemClick(product.id) } ) { Column( - modifier = Modifier.padding(top = 20.dp, start = 6.dp, end = 6.dp), + modifier = Modifier.padding(top = 20.dp, start = 6.dp, end = 6.dp) ) { AsyncImage( model = product.imageUrl, contentDescription = product.name, modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f), + Modifier + .fillMaxWidth() + .aspectRatio(1f) ) Text( text = product.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, modifier = Modifier.padding(top = 8.dp), style = - TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Bold, - ), + TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) ) Text( text = product.price.toString(), style = - TextStyle( - fontSize = 16.sp, - ), - modifier = Modifier.padding(top = 2.dp), + TextStyle( + fontSize = 16.sp + ), + modifier = Modifier.padding(top = 2.dp) ) } } @@ -81,28 +84,28 @@ fun Item( @Preview(showBackground = true) @Composable -fun MyListPreview() { +private fun MyListPreview() { ProductList( items = - listOf( - Product( - id = 1, - name = "name1", - price = 1000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png", - ), - Product( - id = 2, - name = "name2", - price = 2000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg", - ), - Product( - id = 3, - name = "name3", - price = 3000, - imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg", - ), + listOf( + Product( + id = 1, + name = "name1", + price = 1000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/retail/images/1263603036762773-f6291401-9c64-4944-8189-86e5aead6049.png" + ), + Product( + id = 2, + name = "name2", + price = 2000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/vendor_inventory/e294/d78edd81a8f38ae32984e9ad3393a840bd9ccdc91161838a8036f4d90434.jpg" ), + Product( + id = 3, + name = "name3", + price = 3000, + imageUrl = "https://thumbnail7.coupangcdn.com/thumbnails/remote/292x292ex/image/1025_amir_coupang_oct_80k/e308/9c53df34079cb2e6a8123f93355a796ae18b7979bc61bd360da0793314af.jpg" + ) + ) ) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt index 10c4bd4..36b53f2 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt @@ -1,7 +1,6 @@ package nextstep.shoppingcart.presentation.components import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack @@ -14,18 +13,19 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle 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 nextstep.signup.R @OptIn(ExperimentalMaterial3Api::class) @Composable fun TopBarWithAction( - name: String = "상품목록", + name: String = stringResource(R.string.title_product_list), action: () -> Unit = {}, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Column { CenterAlignedTopAppBar( @@ -33,10 +33,10 @@ fun TopBarWithAction( Text( text = name, style = - TextStyle( - fontSize = 22.sp, - fontWeight = FontWeight.Bold, - ), + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold + ) ) }, modifier = modifier, @@ -44,10 +44,10 @@ fun TopBarWithAction( IconButton(onClick = { action() }) { Icon( imageVector = Icons.Filled.ShoppingCart, - contentDescription = "Shopping Cart", + contentDescription = "Shopping Cart" ) } - }, + } ) } } @@ -57,7 +57,7 @@ fun TopBarWithAction( fun TopBarWithNavigation( name: String = "상품 상세", navigation: () -> Unit = {}, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { Column { TopAppBar( @@ -65,30 +65,30 @@ fun TopBarWithNavigation( Text( text = name, style = - TextStyle( - fontSize = 22.sp, - fontWeight = FontWeight.Bold, - ), + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold + ) ) }, modifier = modifier, navigationIcon = { IconButton( - onClick = { navigation() }, + onClick = { navigation() } ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", + contentDescription = "Back" ) } - }, + } ) } } @Preview @Composable -fun BarLayoutPreview() { +private fun BarLayoutPreview() { Column { TopBarWithAction() TopBarWithNavigation(name = "상품 상세") diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Color.kt b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Color.kt index 535d1d0..5482a2f 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Color.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Color.kt @@ -8,4 +8,4 @@ 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) diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Theme.kt b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Theme.kt index 21ecdf0..2991c28 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Theme.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Theme.kt @@ -1,6 +1,5 @@ package nextstep.shoppingcart.presentation.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Type.kt b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Type.kt index f0bdf1e..7fca560 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Type.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/ui/theme/Type.kt @@ -31,4 +31,4 @@ val Typography = Typography( letterSpacing = 0.5.sp ) */ -) \ No newline at end of file +) 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..47cfa5c 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,4 @@ 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) 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 ec6074b..6c48f57 100644 --- a/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt +++ b/app/src/main/java/nextstep/shoppingcart/ui/theme/Theme.kt @@ -1,6 +1,5 @@ package nextstep.shoppingcart.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -9,11 +8,7 @@ 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 private val DarkColorScheme = darkColorScheme( primary = Purple80, @@ -42,7 +37,7 @@ 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 -> { @@ -57,6 +52,6 @@ fun ShoppingCartTheme( 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..f29af30 100644 --- a/app/src/main/java/nextstep/shoppingcart/ui/theme/Type.kt +++ b/app/src/main/java/nextstep/shoppingcart/ui/theme/Type.kt @@ -31,4 +31,4 @@ val Typography = Typography( letterSpacing = 0.5.sp ) */ -) \ No newline at end of file +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fb61f2..1f5dbdf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,12 @@ Shopping Cart DetailActivity CartActivity - \ No newline at end of file + 장바구니 담기 + 상품 상세 + 금액 + 주문하기(%1$s) + 장바구니 + - + + + 상품목록 + From f81c672be638fbf67ec682eb8f4021451d85bb08 Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Sun, 10 Nov 2024 20:07:18 +0900 Subject: [PATCH 08/12] fix: change price format --- .../java/nextstep/shoppingcart/presentation/HomeActivity.kt | 6 ------ .../shoppingcart/presentation/components/ProductList.kt | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt index 0501b76..1dbb3ec 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt @@ -15,12 +15,6 @@ class HomeActivity : ComponentActivity() { setContent { ShoppingCartTheme { - // A surface container using the 'background' color from the theme - /*Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background, - ) { - }*/ ListLayout( products = repository.fetchProducts(), action = { navigateToCart() }, diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt index 60ac7c4..9715d8f 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage +import nextstep.shoppingcart.domain.model.Price import nextstep.shoppingcart.domain.model.Product @Composable @@ -71,7 +72,7 @@ fun Item( ) ) Text( - text = product.price.toString(), + text = Price(product.price).format(), style = TextStyle( fontSize = 16.sp From 959a34df3bf717135f5ff2c807771d4eaa2f5cf7 Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Fri, 15 Nov 2024 13:34:55 +0900 Subject: [PATCH 09/12] refactor(components): optimize component design --- .../shoppingcart/presentation/CartActivity.kt | 6 +- .../presentation/DetailActivity.kt | 4 +- .../shoppingcart/presentation/HomeActivity.kt | 4 +- .../presentation/components/BottomBar.kt | 41 ++++++++ .../presentation/components/CartItemList.kt | 75 ++++----------- .../presentation/components/CartLayout.kt | 65 ------------- .../presentation/components/CounterForm.kt | 5 +- .../presentation/components/ProductList.kt | 74 +++++++------- .../components/TopBarWithAction.kt | 96 ------------------- .../components/screens/CartScreen.kt | 93 ++++++++++++++++++ .../DetailScreen.kt} | 34 +++---- .../{HomeLayout.kt => screens/HomeScreen.kt} | 10 +- .../components/topbars/TopBarWithAction.kt | 56 +++++++++++ .../topbars/TopBarWithNavigation.kt | 57 +++++++++++ .../nextstep/shoppingcart/ui/theme/Color.kt | 2 + 15 files changed, 335 insertions(+), 287 deletions(-) create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/BottomBar.kt delete mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt delete mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt rename app/src/main/java/nextstep/shoppingcart/presentation/components/{DetailLayout.kt => screens/DetailScreen.kt} (81%) rename app/src/main/java/nextstep/shoppingcart/presentation/components/{HomeLayout.kt => screens/HomeScreen.kt} (85%) create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithAction.kt create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithNavigation.kt diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt index 2dd7acf..9d943c4 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt @@ -15,20 +15,22 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import nextstep.shoppingcart.data.Cart import nextstep.shoppingcart.domain.model.CartItem -import nextstep.shoppingcart.presentation.components.CartLayout +import nextstep.shoppingcart.presentation.components.screens.CartScreen import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme class CartActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { + var cart by remember { mutableStateOf(Cart) } var cartItems by remember { mutableStateOf(Cart.items) } AndroidshoppingcartTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - CartLayout( + CartScreen( navigation = { finish() }, modifier = Modifier.padding(innerPadding), cartItems = cartItems, + cart = cart, onIncrease = { onIncrease(it) cartItems = Cart.items diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt index 233cc51..2ab48b0 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt @@ -9,7 +9,7 @@ import nextstep.shoppingcart.data.CachedProductDataSource import nextstep.shoppingcart.data.CachedProductRepository import nextstep.shoppingcart.data.Cart import nextstep.shoppingcart.domain.model.Product -import nextstep.shoppingcart.presentation.components.DetailLayout +import nextstep.shoppingcart.presentation.components.screens.DetailScreen import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme class DetailActivity : ComponentActivity() { @@ -18,7 +18,7 @@ class DetailActivity : ComponentActivity() { val product = fetchProduct() setContent { AndroidshoppingcartTheme { - DetailLayout( + DetailScreen( product = product, navigation = { navigateBack() }, action = { addToCart(product = product) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt index 1dbb3ec..b85ccbb 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt @@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import nextstep.shoppingcart.data.CachedProductDataSource import nextstep.shoppingcart.data.CachedProductRepository -import nextstep.shoppingcart.presentation.components.ListLayout +import nextstep.shoppingcart.presentation.components.screens.HomeScreen import nextstep.shoppingcart.ui.theme.ShoppingCartTheme class HomeActivity : ComponentActivity() { @@ -15,7 +15,7 @@ class HomeActivity : ComponentActivity() { setContent { ShoppingCartTheme { - ListLayout( + HomeScreen( products = repository.fetchProducts(), action = { navigateToCart() }, onItemClick = { productId -> navigateToDetail(productId) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/BottomBar.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/BottomBar.kt new file mode 100644 index 0000000..4c35ea5 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/BottomBar.kt @@ -0,0 +1,41 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.foundation.layout.Box +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.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import nextstep.signup.R + +@Composable +fun BottomBar( + text: String = stringResource(R.string.add_to_cart), + modifier: Modifier = Modifier +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + ) { + Text( + text = text, + fontSize = 20.sp, + style = + TextStyle( + color = Color.White, + fontWeight = FontWeight.Bold + ) + ) + } +} + +@Preview +@Composable +private fun BottomBarPreview() { + BottomBar() +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt index 59eba2b..136685f 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt @@ -1,13 +1,10 @@ package nextstep.shoppingcart.presentation.components -import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement -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 @@ -19,13 +16,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold 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.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -36,64 +31,36 @@ import coil.compose.AsyncImage import nextstep.shoppingcart.domain.model.CartItem import nextstep.shoppingcart.domain.model.Price import nextstep.shoppingcart.domain.model.Product -import nextstep.signup.R @Composable -fun ShoppingCartScreen( +fun CartItemList( items: List = emptyList(), onIncrease: (CartItem) -> Unit = {}, onDecrease: (CartItem) -> Unit = {}, onClear: (CartItem) -> Unit = {}, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - val totalPrice = items.sumOf { it.product.price * it.count } - - Scaffold( - bottomBar = { - Box( - modifier = - Modifier + LazyColumn( + modifier = modifier + ) { + items(items) { item -> + CartItem( + item = item, + onIncrease = onIncrease, + onDecrease = onDecrease, + onClear = onClear, + modifier = Modifier .fillMaxWidth() - .background(Color(0xFF2196F3)) - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(R.string.order, Price(totalPrice).format()), - fontSize = 20.sp, - style = - TextStyle( - color = Color.White, - fontWeight = FontWeight.Bold - ) - ) - } - } - ) { innerPadding -> - Column( - modifier = - Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - LazyColumn { - items(items) { item -> - CartItemView( - item = item, - onIncrease = onIncrease, - onDecrease = onDecrease, - onClear = onClear - ) - } - } - - Spacer(modifier = Modifier.weight(1f)) + .padding(8.dp) + .border(1.dp, Color.Gray, shape = RoundedCornerShape(4.dp)), + ) } } } + @Composable -fun CartItemView( +fun CartItem( item: CartItem, onIncrease: (CartItem) -> Unit = {}, onDecrease: (CartItem) -> Unit = {}, @@ -101,11 +68,7 @@ fun CartItemView( modifier: Modifier = Modifier ) { Row( - modifier = - Modifier - .fillMaxWidth() - .padding(8.dp) - .border(1.dp, Color.Gray, shape = RoundedCornerShape(4.dp)), + modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { @@ -191,5 +154,5 @@ private fun ShoppingCartPreview() { count = 2 ) ) - ShoppingCartScreen(items) + CartItemList(items) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt deleted file mode 100644 index eff0b55..0000000 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartLayout.kt +++ /dev/null @@ -1,65 +0,0 @@ -package nextstep.shoppingcart.presentation.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import nextstep.shoppingcart.domain.model.CartItem -import nextstep.shoppingcart.domain.model.Product -import nextstep.signup.R - -@Composable -fun CartLayout( - navigation: () -> Unit, - cartItems: List, - onIncrease: (CartItem) -> Unit = {}, - onDecrease: (CartItem) -> Unit = {}, - onClear: (CartItem) -> Unit = {}, - modifier: Modifier = Modifier -) { - Column { - Surface { - Column { - TopBarWithNavigation( - name = stringResource(R.string.title_shopping_cart), - navigation = navigation - ) - ShoppingCartScreen( - items = cartItems, - onIncrease = onIncrease, - onDecrease = onDecrease, - onClear = onClear - ) - } - } - } -} - -@Composable -@Preview(showBackground = true) -private fun CartLayoutPreview() { - val items = - listOf( - CartItem( - Product( - id = 1L, - name = "[든든] 동원 스위트콘", - price = 42200, - imageUrl = "https://thumbnail7" - ), - count = 1 - ), - CartItem( - Product( - id = 2L, - name = "PET보틀-원형(500ml)", - price = 84400, - imageUrl = "" - ), - count = 2 - ) - ) - CartLayout(navigation = {}, cartItems = items) -} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CounterForm.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CounterForm.kt index 8a1f4a6..f9b2282 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/CounterForm.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CounterForm.kt @@ -23,7 +23,10 @@ fun CounterForm( onDecrease: () -> Unit, modifier: Modifier = Modifier ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + ) { Box( modifier = Modifier diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt index 9715d8f..63c60a4 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt @@ -1,5 +1,6 @@ package nextstep.shoppingcart.presentation.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth @@ -7,7 +8,6 @@ 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.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -29,10 +29,10 @@ fun ProductList( ) { LazyVerticalGrid( columns = GridCells.Fixed(2), - modifier = Modifier.padding(12.dp) + modifier = modifier, ) { items(items) { item -> - Item( + ProductItem( product = item, onItemClick = onItemClick ) @@ -41,45 +41,43 @@ fun ProductList( } @Composable -fun Item( +fun ProductItem( product: Product, - onItemClick: (Long) -> Unit + onItemClick: (Long) -> Unit, ) { - Surface( - modifier = Modifier.fillMaxWidth(), - onClick = { onItemClick(product.id) } + Column( + modifier = Modifier + .fillMaxWidth() + .clickable { onItemClick(product.id) } + .padding(top = 20.dp, start = 6.dp, end = 6.dp) ) { - Column( - modifier = Modifier.padding(top = 20.dp, start = 6.dp, end = 6.dp) - ) { - AsyncImage( - model = product.imageUrl, - contentDescription = product.name, - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f) - ) - Text( - text = product.name, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(top = 8.dp), - style = - TextStyle( - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) - ) - Text( - text = Price(product.price).format(), - style = - TextStyle( - fontSize = 16.sp - ), - modifier = Modifier.padding(top = 2.dp) + AsyncImage( + model = product.imageUrl, + contentDescription = product.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + ) + Text( + text = product.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 8.dp), + style = + TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold ) - } + ) + Text( + text = Price(product.price).format(), + style = + TextStyle( + fontSize = 16.sp + ), + modifier = Modifier.padding(top = 2.dp) + ) } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt deleted file mode 100644 index 36b53f2..0000000 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/TopBarWithAction.kt +++ /dev/null @@ -1,96 +0,0 @@ -package nextstep.shoppingcart.presentation.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowBack -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.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp -import nextstep.signup.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TopBarWithAction( - name: String = stringResource(R.string.title_product_list), - action: () -> Unit = {}, - modifier: Modifier = Modifier -) { - Column { - CenterAlignedTopAppBar( - title = { - Text( - text = name, - style = - TextStyle( - fontSize = 22.sp, - fontWeight = FontWeight.Bold - ) - ) - }, - modifier = modifier, - actions = { - IconButton(onClick = { action() }) { - Icon( - imageVector = Icons.Filled.ShoppingCart, - contentDescription = "Shopping Cart" - ) - } - } - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TopBarWithNavigation( - name: String = "상품 상세", - navigation: () -> Unit = {}, - modifier: Modifier = Modifier -) { - Column { - TopAppBar( - title = { - Text( - text = name, - style = - TextStyle( - fontSize = 22.sp, - fontWeight = FontWeight.Bold - ) - ) - }, - modifier = modifier, - navigationIcon = { - IconButton( - onClick = { navigation() } - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - } - ) - } -} - -@Preview -@Composable -private fun BarLayoutPreview() { - Column { - TopBarWithAction() - TopBarWithNavigation(name = "상품 상세") - } -} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt new file mode 100644 index 0000000..07b0e30 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt @@ -0,0 +1,93 @@ +package nextstep.shoppingcart.presentation.components.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import nextstep.shoppingcart.data.Cart +import nextstep.shoppingcart.domain.model.CartItem +import nextstep.shoppingcart.domain.model.Price +import nextstep.shoppingcart.domain.model.Product +import nextstep.shoppingcart.presentation.components.BottomBar +import nextstep.shoppingcart.presentation.components.CartItemList +import nextstep.shoppingcart.presentation.components.topbars.TopBarWithNavigation +import nextstep.shoppingcart.ui.theme.Blue50 +import nextstep.signup.R + +@Composable +fun CartScreen( + navigation: () -> Unit, + cartItems: List, + cart: Cart, + onIncrease: (CartItem) -> Unit = {}, + onDecrease: (CartItem) -> Unit = {}, + onClear: (CartItem) -> Unit = {}, + modifier: Modifier = Modifier, +) { + val totalPrice = cartItems.sumOf { it.product.price * it.count } + + Scaffold( + modifier = modifier, + topBar = { + TopBarWithNavigation( + name = stringResource(R.string.title_shopping_cart), + navigation = navigation, + ) + }, + bottomBar = { + BottomBar( + text = stringResource(R.string.order, Price(totalPrice).format()), + modifier = + Modifier + .fillMaxWidth() + .background(Blue50) + .padding(16.dp), + ) + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding), + ) { + CartItemList( + items = cartItems, + onIncrease = onIncrease, + onDecrease = onDecrease, + onClear = onClear, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun CartScreenPreview() { + val items = + listOf( + CartItem( + Product( + id = 1L, + name = "[든든] 동원 스위트콘", + price = 42200, + imageUrl = "https://thumbnail7", + ), + count = 1, + ), + CartItem( + Product( + id = 2L, + name = "PET보틀-원형(500ml)", + price = 84400, + imageUrl = "", + ), + count = 2, + ), + ) + CartScreen(navigation = {}, cartItems = items, cart = Cart) +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/DetailScreen.kt similarity index 81% rename from app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt rename to app/src/main/java/nextstep/shoppingcart/presentation/components/screens/DetailScreen.kt index f331696..4073442 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/DetailLayout.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/DetailScreen.kt @@ -1,9 +1,8 @@ -package nextstep.shoppingcart.presentation.components +package nextstep.shoppingcart.presentation.components.screens import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio @@ -25,36 +24,29 @@ import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import nextstep.shoppingcart.domain.model.Price import nextstep.shoppingcart.domain.model.Product +import nextstep.shoppingcart.presentation.components.BottomBar +import nextstep.shoppingcart.presentation.components.topbars.TopBarWithNavigation +import nextstep.shoppingcart.ui.theme.Blue50 import nextstep.signup.R @Composable -fun DetailLayout( +fun DetailScreen( navigation: () -> Unit = {}, action: () -> Unit = {}, product: Product, modifier: Modifier = Modifier ) { Scaffold( + modifier = modifier, bottomBar = { - Box( - modifier = - Modifier + BottomBar( + text = stringResource(R.string.add_to_cart), + modifier = Modifier .fillMaxWidth() - .background(Color(0xFF2196F3)) + .background(Blue50) .padding(16.dp) - .clickable { action() }, - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(R.string.add_to_cart), - fontSize = 20.sp, - style = - TextStyle( - color = Color.White, - fontWeight = FontWeight.Bold - ) - ) - } + .clickable { action() } + ) } ) { innerPadding -> Column(modifier = modifier.padding(innerPadding)) { @@ -120,5 +112,5 @@ fun DetailLayout( @Composable @Preview(showBackground = true) private fun DetailLayoutPreview() { - DetailLayout(product = Product.NULL_PRODUCT) + DetailScreen(product = Product.NULL_PRODUCT) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt similarity index 85% rename from app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt rename to app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt index d19e0ab..157ac7e 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/HomeLayout.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt @@ -1,4 +1,4 @@ -package nextstep.shoppingcart.presentation.components +package nextstep.shoppingcart.presentation.components.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -7,9 +7,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import nextstep.shoppingcart.domain.model.Product +import nextstep.shoppingcart.presentation.components.ProductList +import nextstep.shoppingcart.presentation.components.topbars.TopBarWithAction @Composable -fun ListLayout( +fun HomeScreen( products: List, action: () -> Unit = {}, onItemClick: (Long) -> Unit = {}, @@ -29,7 +31,7 @@ fun ListLayout( @Composable @Preview(showBackground = true) -private fun ListLayoutPreview() { +private fun HomeScreenPreview() { val products = listOf( Product( @@ -52,5 +54,5 @@ private fun ListLayoutPreview() { ) ) - ListLayout(products = products) + HomeScreen(products = products) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithAction.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithAction.kt new file mode 100644 index 0000000..ccef038 --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithAction.kt @@ -0,0 +1,56 @@ +package nextstep.shoppingcart.presentation.components.topbars + +import androidx.compose.foundation.layout.Column +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.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import nextstep.signup.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBarWithAction( + name: String = stringResource(R.string.title_product_list), + action: () -> Unit = {}, + modifier: Modifier = Modifier +) { + CenterAlignedTopAppBar( + title = { + Text( + text = name, + style = + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold + ) + ) + }, + modifier = modifier, + actions = { + IconButton(onClick = { action() }) { + Icon( + imageVector = Icons.Filled.ShoppingCart, + contentDescription = "Shopping Cart" + ) + } + } + ) +} + +@Preview +@Composable +private fun BarLayoutPreview() { + Column { + TopBarWithAction() + } +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithNavigation.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithNavigation.kt new file mode 100644 index 0000000..a23621f --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithNavigation.kt @@ -0,0 +1,57 @@ +package nextstep.shoppingcart.presentation.components.topbars + +import androidx.compose.foundation.layout.Column +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.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopBarWithNavigation( + name: String = "상품 상세", + navigation: () -> Unit = {}, + modifier: Modifier = Modifier +) { + TopAppBar( + title = { + Text( + text = name, + style = + TextStyle( + fontSize = 22.sp, + fontWeight = FontWeight.Bold + ) + ) + }, + modifier = modifier, + navigationIcon = { + IconButton( + onClick = { navigation() } + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + } + ) +} + + +@Preview +@Composable +private fun BarLayoutPreview() { + Column { + TopBarWithNavigation(name = "상품 상세") + } +} 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 47cfa5c..368d701 100644 --- a/app/src/main/java/nextstep/shoppingcart/ui/theme/Color.kt +++ b/app/src/main/java/nextstep/shoppingcart/ui/theme/Color.kt @@ -9,3 +9,5 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) + +val Blue50 = Color(0xFF2196F3) From 0882ffd9ba697fffbcd24b50876ee0c81618cb3e Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Wed, 20 Nov 2024 01:24:41 +0900 Subject: [PATCH 10/12] refactor: apply reviews (murjune) --- .../java/nextstep/shoppingcart/data/Cart.kt | 13 +- ...ository.kt => DefaultProductRepository.kt} | 2 +- .../shoppingcart/domain/model/CartItem.kt | 1 + .../shoppingcart/presentation/CartActivity.kt | 43 +++--- .../presentation/DetailActivity.kt | 26 ++-- .../shoppingcart/presentation/HomeActivity.kt | 17 +-- .../presentation/components/CartItemList.kt | 21 +-- .../presentation/components/ProductImage.kt | 19 +++ .../presentation/components/ProductList.kt | 12 +- .../components/screens/CartScreen.kt | 62 +++++---- .../components/screens/DetailScreen.kt | 124 +++++++++--------- .../components/screens/HomeScreen.kt | 21 +-- 12 files changed, 197 insertions(+), 164 deletions(-) rename app/src/main/java/nextstep/shoppingcart/data/{CachedProductRepository.kt => DefaultProductRepository.kt} (91%) create mode 100644 app/src/main/java/nextstep/shoppingcart/presentation/components/ProductImage.kt diff --git a/app/src/main/java/nextstep/shoppingcart/data/Cart.kt b/app/src/main/java/nextstep/shoppingcart/data/Cart.kt index 9721fbd..8086724 100644 --- a/app/src/main/java/nextstep/shoppingcart/data/Cart.kt +++ b/app/src/main/java/nextstep/shoppingcart/data/Cart.kt @@ -9,10 +9,19 @@ object Cart { val totalPrice: Int get() = _items.sumOf { it.totalPrice } + private var maxCartItemId: Long = 0L + fun addOne(product: Product): List { val item = _items.find { it.product == product } if (item == null) { - _items.add(CartItem(product, 1)) + _items.add( + CartItem( + id = fetchMaxCartItemId(), + product = product, + count = 1, + ) + ) + } else { val index = _items.indexOf(item) _items[index] = item.copy(count = item.count + 1) @@ -20,6 +29,8 @@ object Cart { return items } + private fun fetchMaxCartItemId(): Long = maxCartItemId++ + fun removeOne(product: Product): List { _items .find { it.product == product } diff --git a/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt b/app/src/main/java/nextstep/shoppingcart/data/DefaultProductRepository.kt similarity index 91% rename from app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt rename to app/src/main/java/nextstep/shoppingcart/data/DefaultProductRepository.kt index d8105f0..a62b778 100644 --- a/app/src/main/java/nextstep/shoppingcart/data/CachedProductRepository.kt +++ b/app/src/main/java/nextstep/shoppingcart/data/DefaultProductRepository.kt @@ -2,7 +2,7 @@ package nextstep.shoppingcart.data import nextstep.shoppingcart.domain.model.Product -class CachedProductRepository( +class DefaultProductRepository( private val productDataSource: ProductDataSource ) : ProductRepository { override fun fetchProducts(): List = productDataSource.fetchProducts() diff --git a/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt b/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt index 89f0dde..6181537 100644 --- a/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt +++ b/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt @@ -1,6 +1,7 @@ package nextstep.shoppingcart.domain.model data class CartItem( + val id: Long, val product: Product, val count: Int ) { diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt index 9d943c4..e5b5446 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt @@ -5,18 +5,13 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold 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 nextstep.shoppingcart.data.Cart import nextstep.shoppingcart.domain.model.CartItem import nextstep.shoppingcart.presentation.components.screens.CartScreen -import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme class CartActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -24,31 +19,27 @@ class CartActivity : ComponentActivity() { setContent { var cart by remember { mutableStateOf(Cart) } var cartItems by remember { mutableStateOf(Cart.items) } - AndroidshoppingcartTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - CartScreen( - navigation = { finish() }, - modifier = Modifier.padding(innerPadding), - cartItems = cartItems, - cart = cart, - onIncrease = { - onIncrease(it) - cartItems = Cart.items - }, - onDecrease = { - onDecrease(it) - cartItems = Cart.items - }, - onClear = { - onRemove(it) - cartItems = Cart.items - } - ) + CartScreen( + navigation = { finish() }, + cartItems = cartItems, + cart = cart, + onIncrease = { + onIncrease(it) + cartItems = Cart.items + }, + onDecrease = { + onDecrease(it) + cartItems = Cart.items + }, + onClear = { + onRemove(it) + cartItems = Cart.items } - } + ) } } + private fun onIncrease(item: CartItem) { Cart.addOne(item.product) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt index 2ab48b0..1099a8c 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/DetailActivity.kt @@ -5,25 +5,28 @@ import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import nextstep.shoppingcart.data.CachedProductDataSource -import nextstep.shoppingcart.data.CachedProductRepository import nextstep.shoppingcart.data.Cart +import nextstep.shoppingcart.data.DefaultProductRepository import nextstep.shoppingcart.domain.model.Product import nextstep.shoppingcart.presentation.components.screens.DetailScreen -import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme class DetailActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val product = fetchProduct() + var product by mutableStateOf(fetchProduct()) setContent { - AndroidshoppingcartTheme { - DetailScreen( - product = product, - navigation = { navigateBack() }, - action = { addToCart(product = product) } - ) - } + DetailScreen( + product = product, + navigation = { navigateBack() }, + action = { + addToCart(product = product) + navigateToCart() + } + ) } } @@ -31,13 +34,12 @@ class DetailActivity : ComponentActivity() { val productId = intent.getStringExtra(PRODUCT_ID) ?: throw IllegalArgumentException("Product ID is required") - val repository = CachedProductRepository(CachedProductDataSource()) + val repository = DefaultProductRepository(CachedProductDataSource()) return repository.findProduct(productId.toLong()) } private fun addToCart(product: Product) { Cart.addOne(product) - navigateToCart() } private fun navigateToCart() { diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt index b85ccbb..10c57db 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/HomeActivity.kt @@ -4,23 +4,20 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import nextstep.shoppingcart.data.CachedProductDataSource -import nextstep.shoppingcart.data.CachedProductRepository +import nextstep.shoppingcart.data.DefaultProductRepository import nextstep.shoppingcart.presentation.components.screens.HomeScreen -import nextstep.shoppingcart.ui.theme.ShoppingCartTheme class HomeActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val repository = CachedProductRepository(CachedProductDataSource()) + val repository = DefaultProductRepository(CachedProductDataSource()) setContent { - ShoppingCartTheme { - HomeScreen( - products = repository.fetchProducts(), - action = { navigateToCart() }, - onItemClick = { productId -> navigateToDetail(productId) } - ) - } + HomeScreen( + products = repository.fetchProducts(), + action = { navigateToCart() }, + onItemClick = { productId -> navigateToDetail(productId) } + ) } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt index 136685f..9739d26 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt @@ -27,7 +27,6 @@ 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 coil.compose.AsyncImage import nextstep.shoppingcart.domain.model.CartItem import nextstep.shoppingcart.domain.model.Price import nextstep.shoppingcart.domain.model.Product @@ -41,9 +40,14 @@ fun CartItemList( modifier: Modifier = Modifier, ) { LazyColumn( - modifier = modifier + modifier = modifier, ) { - items(items) { item -> + items( + items = items, + key = { item -> + item.id + } + ) { item -> CartItem( item = item, onIncrease = onIncrease, @@ -60,7 +64,7 @@ fun CartItemList( @Composable -fun CartItem( +private fun CartItem( item: CartItem, onIncrease: (CartItem) -> Unit = {}, onDecrease: (CartItem) -> Unit = {}, @@ -102,11 +106,10 @@ fun CartItem( } } Row { - AsyncImage( - model = item.product.imageUrl, + ProductImage( + imageUrl = item.product.imageUrl, contentDescription = item.product.name, - modifier = - Modifier + modifier = Modifier .width(136.dp) .height(84.dp) ) @@ -136,6 +139,7 @@ private fun ShoppingCartPreview() { val items = listOf( CartItem( + id = 0L, Product( id = 1L, name = "[든든] 동원 스위트콘", @@ -145,6 +149,7 @@ private fun ShoppingCartPreview() { count = 1 ), CartItem( + id = 1L, Product( id = 2L, name = "PET보틀-원형(500ml)", diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductImage.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductImage.kt new file mode 100644 index 0000000..c63007c --- /dev/null +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductImage.kt @@ -0,0 +1,19 @@ +package nextstep.shoppingcart.presentation.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import coil.compose.AsyncImage + +@Composable +fun ProductImage( + imageUrl: String, + contentDescription: String,modifier: Modifier = Modifier +) { + AsyncImage( + model = imageUrl, + contentDescription = contentDescription, + modifier = modifier, + contentScale = ContentScale.Crop + ) +} diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt index 63c60a4..1b647d9 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt @@ -17,7 +17,6 @@ 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 coil.compose.AsyncImage import nextstep.shoppingcart.domain.model.Price import nextstep.shoppingcart.domain.model.Product @@ -41,7 +40,7 @@ fun ProductList( } @Composable -fun ProductItem( +private fun ProductItem( product: Product, onItemClick: (Long) -> Unit, ) { @@ -51,13 +50,12 @@ fun ProductItem( .clickable { onItemClick(product.id) } .padding(top = 20.dp, start = 6.dp, end = 6.dp) ) { - AsyncImage( - model = product.imageUrl, + ProductImage( + imageUrl = product.imageUrl, contentDescription = product.name, - modifier = - Modifier + modifier = Modifier .fillMaxWidth() - .aspectRatio(1f) + .aspectRatio(1f), ) Text( text = product.name, diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt index 07b0e30..bcdeb7d 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt @@ -17,6 +17,7 @@ import nextstep.shoppingcart.domain.model.Product import nextstep.shoppingcart.presentation.components.BottomBar import nextstep.shoppingcart.presentation.components.CartItemList import nextstep.shoppingcart.presentation.components.topbars.TopBarWithNavigation +import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme import nextstep.shoppingcart.ui.theme.Blue50 import nextstep.signup.R @@ -31,36 +32,37 @@ fun CartScreen( modifier: Modifier = Modifier, ) { val totalPrice = cartItems.sumOf { it.product.price * it.count } - - Scaffold( - modifier = modifier, - topBar = { - TopBarWithNavigation( - name = stringResource(R.string.title_shopping_cart), - navigation = navigation, - ) - }, - bottomBar = { - BottomBar( - text = stringResource(R.string.order, Price(totalPrice).format()), - modifier = - Modifier - .fillMaxWidth() - .background(Blue50) - .padding(16.dp), + AndroidshoppingcartTheme { + Scaffold( + modifier = modifier, + topBar = { + TopBarWithNavigation( + name = stringResource(R.string.title_shopping_cart), + navigation = navigation, ) - } - ) { innerPadding -> - Column( - modifier = Modifier.padding(innerPadding), - ) { - CartItemList( - items = cartItems, - onIncrease = onIncrease, - onDecrease = onDecrease, - onClear = onClear, - modifier = Modifier.fillMaxWidth(), - ) + }, + bottomBar = { + BottomBar( + text = stringResource(R.string.order, Price(totalPrice).format()), + modifier = + Modifier + .fillMaxWidth() + .background(Blue50) + .padding(16.dp), + ) + } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding), + ) { + CartItemList( + items = cartItems, + onIncrease = onIncrease, + onDecrease = onDecrease, + onClear = onClear, + modifier = Modifier.fillMaxWidth(), + ) + } } } } @@ -71,6 +73,7 @@ private fun CartScreenPreview() { val items = listOf( CartItem( + id = 0L, Product( id = 1L, name = "[든든] 동원 스위트콘", @@ -80,6 +83,7 @@ private fun CartScreenPreview() { count = 1, ), CartItem( + id = 1L, Product( id = 2L, name = "PET보틀-원형(500ml)", diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/DetailScreen.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/DetailScreen.kt index 4073442..70e7d38 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/DetailScreen.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/DetailScreen.kt @@ -21,11 +21,12 @@ 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 coil.compose.AsyncImage import nextstep.shoppingcart.domain.model.Price import nextstep.shoppingcart.domain.model.Product import nextstep.shoppingcart.presentation.components.BottomBar +import nextstep.shoppingcart.presentation.components.ProductImage import nextstep.shoppingcart.presentation.components.topbars.TopBarWithNavigation +import nextstep.shoppingcart.presentation.ui.theme.AndroidshoppingcartTheme import nextstep.shoppingcart.ui.theme.Blue50 import nextstep.signup.R @@ -36,74 +37,75 @@ fun DetailScreen( product: Product, modifier: Modifier = Modifier ) { - Scaffold( - modifier = modifier, - bottomBar = { - BottomBar( - text = stringResource(R.string.add_to_cart), - modifier = Modifier - .fillMaxWidth() - .background(Blue50) - .padding(16.dp) - .clickable { action() } - ) - } - ) { innerPadding -> - Column(modifier = modifier.padding(innerPadding)) { - TopBarWithNavigation( - name = stringResource(R.string.title_product_detail), - navigation = { navigation() }, - modifier = modifier - ) - AsyncImage( - model = product.imageUrl, - contentDescription = product.name, - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f) - ) - Text( - text = product.name, - modifier = - Modifier - .fillMaxWidth() - .padding(18.dp), - style = - TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold + AndroidshoppingcartTheme { + Scaffold( + modifier = modifier, + bottomBar = { + BottomBar( + text = stringResource(R.string.add_to_cart), + modifier = Modifier + .fillMaxWidth() + .background(Blue50) + .clickable { action() } + .padding(16.dp) + ) + } + ) { innerPadding -> + Column(modifier = modifier.padding(innerPadding)) { + TopBarWithNavigation( + name = stringResource(R.string.title_product_detail), + navigation = { navigation() }, + modifier = modifier + ) + ProductImage( + imageUrl = product.imageUrl, + contentDescription = product.name, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) ) - ) - - Divider( - color = Color.Gray, - thickness = 1.dp, - modifier = - Modifier - .fillMaxWidth() - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { Text( - text = stringResource(R.string.price), - fontSize = 20.sp, + text = product.name, modifier = Modifier - .padding(18.dp) - .align(Alignment.CenterVertically) + .fillMaxWidth() + .padding(18.dp), + style = + TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) ) - Text( - text = Price(product.price).format(), - fontSize = 20.sp, + + Divider( + color = Color.Gray, + thickness = 1.dp, modifier = Modifier - .padding(18.dp) - .align(Alignment.CenterVertically) + .fillMaxWidth() ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.price), + fontSize = 20.sp, + modifier = + Modifier + .padding(18.dp) + .align(Alignment.CenterVertically) + ) + Text( + text = Price(product.price).format(), + fontSize = 20.sp, + modifier = + Modifier + .padding(18.dp) + .align(Alignment.CenterVertically) + ) + } } } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt index 157ac7e..b75c6c1 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.unit.dp import nextstep.shoppingcart.domain.model.Product import nextstep.shoppingcart.presentation.components.ProductList import nextstep.shoppingcart.presentation.components.topbars.TopBarWithAction +import nextstep.shoppingcart.ui.theme.ShoppingCartTheme @Composable fun HomeScreen( @@ -17,15 +18,17 @@ fun HomeScreen( onItemClick: (Long) -> Unit = {}, modifier: Modifier = Modifier ) { - Column( - modifier = modifier - ) { - TopBarWithAction(modifier = Modifier, action = action) - ProductList( - items = products, - onItemClick = onItemClick, - modifier = Modifier.padding(top = 18.dp, start = 13.dp, end = 13.dp) - ) + ShoppingCartTheme { + Column( + modifier = modifier + ) { + TopBarWithAction(modifier = Modifier, action = action) + ProductList( + items = products, + onItemClick = onItemClick, + modifier = Modifier.padding(top = 18.dp, start = 13.dp, end = 13.dp) + ) + } } } From f33d23090b1d22faef55e913b5d8f784c44c388b Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Wed, 20 Nov 2024 09:36:18 +0900 Subject: [PATCH 11/12] refactor: apply reviews (james) --- .../java/nextstep/shoppingcart/data/Cart.kt | 64 +++++++++---------- .../shoppingcart/domain/model/CartItem.kt | 6 +- .../shoppingcart/presentation/CartActivity.kt | 38 +++-------- .../presentation/components/CartItemList.kt | 2 + .../components/screens/CartScreen.kt | 5 +- .../components/screens/HomeScreen.kt | 2 +- 6 files changed, 48 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/nextstep/shoppingcart/data/Cart.kt b/app/src/main/java/nextstep/shoppingcart/data/Cart.kt index 8086724..0e064b5 100644 --- a/app/src/main/java/nextstep/shoppingcart/data/Cart.kt +++ b/app/src/main/java/nextstep/shoppingcart/data/Cart.kt @@ -1,52 +1,46 @@ package nextstep.shoppingcart.data +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.toMutableStateList import nextstep.shoppingcart.domain.model.CartItem import nextstep.shoppingcart.domain.model.Product object Cart { - private val _items: MutableList = mutableListOf() - val items: List get() = _items.toList() + val items: SnapshotStateList = emptyList().toMutableStateList() - val totalPrice: Int get() = _items.sumOf { it.totalPrice } - - private var maxCartItemId: Long = 0L - - fun addOne(product: Product): List { - val item = _items.find { it.product == product } - if (item == null) { - _items.add( - CartItem( - id = fetchMaxCartItemId(), - product = product, - count = 1, - ) - ) + val totalPrice = mutableIntStateOf(0) + fun addOne(product: Product) { + val item = items.find { it.product == product } + if (item != null) { + val index = items.indexOf(item) + items[index] = item.copy(count = item.count + 1) } else { - val index = _items.indexOf(item) - _items[index] = item.copy(count = item.count + 1) + items.add(CartItem(product = product, count = 1)) } - return items + updateState() } - private fun fetchMaxCartItemId(): Long = maxCartItemId++ - - fun removeOne(product: Product): List { - _items - .find { it.product == product } - ?.let { item -> - if (item.count > 1) { - val index = _items.indexOf(item) - _items[index] = item.copy(count = item.count - 1) - } else { - _items.remove(item) - } + fun removeOne(product: Product) { + val item = items.find { it.product == product } + if (item != null) { + if (item.count > 1) { + val index = items.indexOf(item) + items[index] = item.copy(count = item.count - 1) + } else { + items.remove(item) } - return items + updateState() + } + } + + fun removeAll(product: Product) { + items.removeAll { it.product == product } + updateState() } - fun removeAll(product: Product): List { - _items.removeAll { it.product == product } - return items + private fun updateState() { + totalPrice.value = items.sumOf { it.product.price * it.count } } } diff --git a/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt b/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt index 6181537..9e2fe92 100644 --- a/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt +++ b/app/src/main/java/nextstep/shoppingcart/domain/model/CartItem.kt @@ -1,9 +1,11 @@ package nextstep.shoppingcart.domain.model data class CartItem( - val id: Long, + val id: Long = maxCartItemId++, val product: Product, val count: Int ) { - val totalPrice: Int get() = product.price * count + companion object { + private var maxCartItemId: Long = 0L + } } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt index e5b5446..eec185d 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/CartActivity.kt @@ -6,51 +6,33 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import nextstep.shoppingcart.data.Cart -import nextstep.shoppingcart.domain.model.CartItem +import nextstep.shoppingcart.domain.model.Product import nextstep.shoppingcart.presentation.components.screens.CartScreen class CartActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - var cart by remember { mutableStateOf(Cart) } - var cartItems by remember { mutableStateOf(Cart.items) } + val cartItems = remember { Cart.items } + val totalPrice by Cart.totalPrice CartScreen( navigation = { finish() }, cartItems = cartItems, - cart = cart, - onIncrease = { - onIncrease(it) - cartItems = Cart.items - }, - onDecrease = { - onDecrease(it) - cartItems = Cart.items - }, - onClear = { - onRemove(it) - cartItems = Cart.items - } + totalPrice = totalPrice, + onIncrease = { addItem(it.product) }, + onDecrease = { subItem(it.product) }, + onClear = { removeItem(it.product) } ) } } + private fun addItem(item: Product) = Cart.addOne(item) - private fun onIncrease(item: CartItem) { - Cart.addOne(item.product) - } - - private fun onDecrease(item: CartItem) { - Cart.removeOne(item.product) - } + private fun subItem(item: Product) = Cart.removeOne(item) - private fun onRemove(item: CartItem) { - Cart.removeAll(item.product) - } + private fun removeItem(item: Product) = Cart.removeAll(item) companion object { fun createIntent(context: Context): Intent = Intent(context, CartActivity::class.java) diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt index 9739d26..e9fb165 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt @@ -3,6 +3,7 @@ package nextstep.shoppingcart.presentation.components import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -41,6 +42,7 @@ fun CartItemList( ) { LazyColumn( modifier = modifier, + contentPadding = PaddingValues(50.dp) ) { items( items = items, diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt index bcdeb7d..5b7b760 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt @@ -25,13 +25,12 @@ import nextstep.signup.R fun CartScreen( navigation: () -> Unit, cartItems: List, - cart: Cart, + totalPrice: Int, onIncrease: (CartItem) -> Unit = {}, onDecrease: (CartItem) -> Unit = {}, onClear: (CartItem) -> Unit = {}, modifier: Modifier = Modifier, ) { - val totalPrice = cartItems.sumOf { it.product.price * it.count } AndroidshoppingcartTheme { Scaffold( modifier = modifier, @@ -93,5 +92,5 @@ private fun CartScreenPreview() { count = 2, ), ) - CartScreen(navigation = {}, cartItems = items, cart = Cart) + CartScreen(navigation = {}, cartItems = items, totalPrice = items.sumOf { it.product.price * it.count }) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt index b75c6c1..e1f4086 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/HomeScreen.kt @@ -26,7 +26,7 @@ fun HomeScreen( ProductList( items = products, onItemClick = onItemClick, - modifier = Modifier.padding(top = 18.dp, start = 13.dp, end = 13.dp) + modifier = Modifier.padding(top = 8.dp, start = 13.dp, end = 13.dp) ) } } From b03e67d48e2d40ce8f28920bf86a3a79fa68f4aa Mon Sep 17 00:00:00 2001 From: Hogu59 Date: Wed, 20 Nov 2024 09:37:18 +0900 Subject: [PATCH 12/12] style: apply ktlint --- .../presentation/components/CartItemList.kt | 5 ++--- .../presentation/components/ProductImage.kt | 3 ++- .../presentation/components/ProductList.kt | 6 +++--- .../components/screens/CartScreen.kt | 21 +++++++++---------- .../topbars/TopBarWithNavigation.kt | 1 - 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt index e9fb165..0306fad 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/CartItemList.kt @@ -38,7 +38,7 @@ fun CartItemList( onIncrease: (CartItem) -> Unit = {}, onDecrease: (CartItem) -> Unit = {}, onClear: (CartItem) -> Unit = {}, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { LazyColumn( modifier = modifier, @@ -58,13 +58,12 @@ fun CartItemList( modifier = Modifier .fillMaxWidth() .padding(8.dp) - .border(1.dp, Color.Gray, shape = RoundedCornerShape(4.dp)), + .border(1.dp, Color.Gray, shape = RoundedCornerShape(4.dp)) ) } } } - @Composable private fun CartItem( item: CartItem, diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductImage.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductImage.kt index c63007c..f634240 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductImage.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductImage.kt @@ -8,7 +8,8 @@ import coil.compose.AsyncImage @Composable fun ProductImage( imageUrl: String, - contentDescription: String,modifier: Modifier = Modifier + contentDescription: String, + modifier: Modifier = Modifier ) { AsyncImage( model = imageUrl, diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt index 1b647d9..d04d646 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/ProductList.kt @@ -28,7 +28,7 @@ fun ProductList( ) { LazyVerticalGrid( columns = GridCells.Fixed(2), - modifier = modifier, + modifier = modifier ) { items(items) { item -> ProductItem( @@ -42,7 +42,7 @@ fun ProductList( @Composable private fun ProductItem( product: Product, - onItemClick: (Long) -> Unit, + onItemClick: (Long) -> Unit ) { Column( modifier = Modifier @@ -55,7 +55,7 @@ private fun ProductItem( contentDescription = product.name, modifier = Modifier .fillMaxWidth() - .aspectRatio(1f), + .aspectRatio(1f) ) Text( text = product.name, diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt index 5b7b760..f4b2f88 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/screens/CartScreen.kt @@ -10,7 +10,6 @@ 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.data.Cart import nextstep.shoppingcart.domain.model.CartItem import nextstep.shoppingcart.domain.model.Price import nextstep.shoppingcart.domain.model.Product @@ -29,7 +28,7 @@ fun CartScreen( onIncrease: (CartItem) -> Unit = {}, onDecrease: (CartItem) -> Unit = {}, onClear: (CartItem) -> Unit = {}, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier ) { AndroidshoppingcartTheme { Scaffold( @@ -37,7 +36,7 @@ fun CartScreen( topBar = { TopBarWithNavigation( name = stringResource(R.string.title_shopping_cart), - navigation = navigation, + navigation = navigation ) }, bottomBar = { @@ -47,19 +46,19 @@ fun CartScreen( Modifier .fillMaxWidth() .background(Blue50) - .padding(16.dp), + .padding(16.dp) ) } ) { innerPadding -> Column( - modifier = Modifier.padding(innerPadding), + modifier = Modifier.padding(innerPadding) ) { CartItemList( items = cartItems, onIncrease = onIncrease, onDecrease = onDecrease, onClear = onClear, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) } } @@ -77,9 +76,9 @@ private fun CartScreenPreview() { id = 1L, name = "[든든] 동원 스위트콘", price = 42200, - imageUrl = "https://thumbnail7", + imageUrl = "https://thumbnail7" ), - count = 1, + count = 1 ), CartItem( id = 1L, @@ -87,10 +86,10 @@ private fun CartScreenPreview() { id = 2L, name = "PET보틀-원형(500ml)", price = 84400, - imageUrl = "", + imageUrl = "" ), - count = 2, - ), + count = 2 + ) ) CartScreen(navigation = {}, cartItems = items, totalPrice = items.sumOf { it.product.price * it.count }) } diff --git a/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithNavigation.kt b/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithNavigation.kt index a23621f..f45f513 100644 --- a/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithNavigation.kt +++ b/app/src/main/java/nextstep/shoppingcart/presentation/components/topbars/TopBarWithNavigation.kt @@ -47,7 +47,6 @@ fun TopBarWithNavigation( ) } - @Preview @Composable private fun BarLayoutPreview() {