Skip to content

Commit

Permalink
4단계 미션 제출!
Browse files Browse the repository at this point in the history
feat: + 아이콘을 누르면 장바구니에 상품이 추가됨과 동시에 수량 조절 버튼이 노출된다.

feat: 상품 목록에서 장바구니에 담을/담긴 상품의 수량을 조절할 수 있다.
  • Loading branch information
murjune committed Nov 2, 2024
1 parent e5b8ad9 commit a3f320a
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 52 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,13 @@
- [x]: 담긴 상품의 수량을 조절할 수 있어야 한다.
- [x]: 수량을 1보다 작게 하면 장바구니에서 상품이 제거된다
- [x]: 담긴 상품 가격의 총합이 주문하기 버튼에 표시된다.
- [x]: 주문 완료 시 장바구니가 비워진다.
- [x]: 주문 완료 시 장바구니가 비워진다.

# 4 단계 목록

- [x]: 상품 목록에서 장바구니에 담을/담긴 상품의 수량을 조절할 수 있다.
- [x]: + 아이콘을 누르면 장바구니에 상품이 추가됨과 동시에 수량 조절 버튼이 노출된다.
- [x]: 상품 목록의 상품 수가 변화하면 장바구니에도 반영되어야 한다. (B마트 UX 참고)
- [x]: 장바구니의 상품 수가 변화하면 상품 목록에도 반영되어야 한다. (B마트 UX 참고)
- [x]: 반복되는 뷰(상품 수량 조절)를 재사용할 수 있는 방법을 고민해 본다.
- [x]: 3단계에서 작성된 장바구니 화면 테스트가 실패하면 안 된다.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package nextstep.shoppingcart.presentation.product
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import nextstep.shoppingcart.domain.model.Product
import nextstep.shoppingcart.presentation.product.ProductScreen
import nextstep.shoppingcart.presentation.product.model.ProductUiModel
import org.junit.Rule
import org.junit.Test

Expand All @@ -20,7 +19,9 @@ class ProductScreenTest {
ProductScreen(
products = emptyList(),
onItemClick = {},
onCartClick = {}
onCartClick = {},
onProductPlus = {},
onProductMinus = {},
)
}

Expand All @@ -35,10 +36,18 @@ class ProductScreenTest {
.setContent {
ProductScreen(
products = listOf(
Product(id = 1L, imageUrl = "testUrl", name = "오둥이", price = 1_000)
ProductUiModel(
id = 1L,
imageUrl = "testUrl",
name = "오둥이",
price = 1_000,
0
)
),
onItemClick = {},
onCartClick = {}
onCartClick = {},
onProductPlus = {},
onProductMinus = {},
)
}
composeTestRule
Expand All @@ -53,7 +62,9 @@ class ProductScreenTest {
ProductScreen(
products = emptyList(),
onItemClick = {},
onCartClick = {}
onCartClick = {},
onProductPlus = {},
onProductMinus = {},
)
}
composeTestRule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import androidx.navigation.toRoute
import kotlinx.coroutines.flow.combine
import kotlinx.serialization.Serializable
import nextstep.shoppingcart.domain.repository.CartRepository
import nextstep.shoppingcart.domain.repository.ProductRepository
import nextstep.shoppingcart.presentation.product.model.ProductUiModel

fun NavController.navigateToProductDetail(
productId: Long,
Expand All @@ -32,12 +34,29 @@ fun NavGraphBuilder.productGraph(
) {
composable<ProductRoute.Home> {
val productRepository = ProductRepository.get()
val products by productRepository.products()
.collectAsStateWithLifecycle(initialValue = emptyList())
val cartRepository = CartRepository.get()
val products: List<ProductUiModel> by combine(
productRepository.products(),
cartRepository.cartProducts()
) { products, cartProducts ->
products.map { product ->
val cartProduct = cartProducts.find { it.product.id == product.id }
ProductUiModel(
id = product.id,
imageUrl = product.imageUrl,
name = product.name,
price = product.price,
count = cartProduct?.count ?: 0
)
}
}.collectAsStateWithLifecycle(emptyList())

ProductScreen(
products = products,
onCartClick = navigateToCart,
onItemClick = navigateToProductDetail
onItemClick = navigateToProductDetail,
onProductPlus = { cartRepository.addProduct(it, 1) },
onProductMinus = { cartRepository.removeProduct(it, 1) },
)
}
composable<ProductRoute.Detail> { backStackEntry ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,19 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import nextstep.shoppingcart.R
import nextstep.shoppingcart.domain.model.Product
import nextstep.shoppingcart.presentation.product.component.ProductItem
import nextstep.shoppingcart.presentation.product.model.ProductUiModel
import nextstep.shoppingcart.presentation.product.preview.ProductPreviewParameterProvider
import nextstep.shoppingcart.presentation.ui.theme.ShoppingCartTheme

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProductScreen(
products: List<Product>,
products: List<ProductUiModel>,
onItemClick: (id: Long) -> Unit,
onCartClick: () -> Unit,
onProductPlus: (id: Long) -> Unit,
onProductMinus: (id: Long) -> Unit,
) {
Scaffold(
topBar = {
Expand Down Expand Up @@ -65,17 +67,20 @@ fun ProductScreen(
modifier = Modifier
.padding(innerPadding)
.padding(horizontal = 18.dp),
onItemClick = onItemClick
onItemClick = onItemClick,
onProductPlus = onProductPlus,
onProductMinus = onProductMinus
)
}
}


@Composable
private fun ProductContent(
products: List<Product>,
products: List<ProductUiModel>,
modifier: Modifier = Modifier,
onItemClick: (id: Long) -> Unit,
onProductPlus: (id: Long) -> Unit,
onProductMinus: (id: Long) -> Unit,
) {
LazyVerticalGrid(
modifier = modifier,
Expand All @@ -86,7 +91,10 @@ private fun ProductContent(
items(products, key = { it.id }) { product ->
ProductItem(
product,
onClick = onItemClick
onClick = onItemClick,
count = product.count,
onProductPlus = { onProductPlus(product.id) },
onProductMinus = { onProductMinus(product.id) },
)
}
}
Expand All @@ -111,9 +119,9 @@ private fun ProductEmptyContent(
@Preview(showBackground = true)
@Composable
private fun PreviewProductScreen(
@PreviewParameter(ProductPreviewParameterProvider::class) products: List<Product>
@PreviewParameter(ProductPreviewParameterProvider::class) products: List<ProductUiModel>
) {
ShoppingCartTheme {
ProductScreen(products, {}, {})
ProductScreen(products, {}, {}, {}, {})
}
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,50 @@
package nextstep.shoppingcart.presentation.product.component

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
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 nextstep.shoppingcart.R
import nextstep.shoppingcart.presentation.product.model.ProductUiModel
import nextstep.shoppingcart.presentation.ui.component.Counter
import nextstep.shoppingcart.presentation.ui.component.ProductImage
import nextstep.shoppingcart.domain.model.Product
import nextstep.shoppingcart.presentation.ui.theme.ShoppingCartTheme

@Composable
internal fun ProductItem(
product: Product,
product: ProductUiModel,
count: Int,
onProductPlus: (id: Long) -> Unit,
onProductMinus: (id: Long) -> Unit,
onClick: (id: Long) -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -37,19 +53,29 @@ internal fun ProductItem(
onClick(product.id)
}
) {
ProductImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(16.dp))
.border(
width = 1.dp,
color = Color.Black,
shape = RoundedCornerShape(16.dp)
),
imageUrl = product.imageUrl,
contentDescription = "상품 이미지",
)
Box {
ProductImage(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(16.dp))
.border(
width = 1.dp,
color = Color.Black,
shape = RoundedCornerShape(16.dp)
),
imageUrl = product.imageUrl,
contentDescription = "상품 이미지",
)
ProductItemCounter(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(4.dp),
count = count,
onProductPlus = { onProductPlus(product.id) },
onProductMinus = { onProductMinus(product.id) }
)
}
Spacer(modifier = Modifier.height(8.dp))
Column(Modifier.padding(horizontal = 4.dp)) {
Text(
Expand All @@ -67,20 +93,88 @@ internal fun ProductItem(
}
}

@Composable
private fun ProductItemCounter(
count: Int,
onProductPlus: () -> Unit,
onProductMinus: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.pointerInput(Unit) {
detectTapGestures {}
}) {
if (count == 0) {
PlusButton(
modifier = Modifier
.padding(12.dp)
.size(42.dp)
.shadow(4.dp, shape = CircleShape)
.clip(CircleShape),
onPlus = onProductPlus
)
} else {
Counter(
modifier = Modifier
.padding(6.dp)
.shadow(4.dp, shape = RoundedCornerShape(6.dp))
.background(Color.White),
count = count,
onPlus = onProductPlus,
onMinus = onProductMinus
)
}
}
}

@Composable
private fun PlusButton(modifier: Modifier = Modifier, onPlus: () -> Unit) {
IconButton(
onClick = onPlus,
modifier = modifier.size(42.dp),
colors = IconButtonDefaults.iconButtonColors(
containerColor = Color.White,
)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "장바구니 추가",
)
}
}

@Preview(showBackground = true)
@Composable
private fun PreviewProductItem() {
ShoppingCartTheme {
ProductItem(
product = Product(
id = 1,
name = "상품 이름",
price = 1000,
imageUrl = "https://www.example.com/image.jpg"

),
onClick = {},
modifier = Modifier.size(100.dp, 150.dp)
)
Column {
ProductItem(
product = ProductUiModel(
id = 1,
name = "상품 이름",
price = 1000,
imageUrl = "https://www.example.com/image.jpg",
count = 1
),
count = 0,
onProductPlus = {},
onProductMinus = {},
onClick = {},
modifier = Modifier.size(200.dp, 250.dp)
)
ProductItem(
product = ProductUiModel(
id = 1,
name = "상품 이름",
price = 1000,
imageUrl = "https://www.example.com/image.jpg",
count = 0
),
count = 1,
onProductPlus = {},
onProductMinus = {},
onClick = {},
modifier = Modifier.size(200.dp, 250.dp)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package nextstep.shoppingcart.presentation.product.model

data class ProductUiModel(
val id: Long,
val imageUrl: String,
val name: String,
val price: Int,
val count: Int
)
Loading

0 comments on commit a3f320a

Please sign in to comment.