-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[벼리] 장바구니 미션 제출합니다. #3
base: main
Are you sure you want to change the base?
Changes from all commits
e402744
82ed69c
1e97f77
6037c26
bfcb28c
939223b
0ea7b7c
e69f7b3
2748474
324850f
9b38a6f
83c6f49
6ff7cdd
983456d
5a02f7c
3032ca5
c92353b
ce2393f
fde0d16
065e933
cf7a207
9c32c49
c45f464
efd1f7e
2dd73f3
b25b12c
5683619
71801dd
185e9c0
365e8be
d509536
2124e76
0607bc8
d485aec
3c03ff0
7a5e055
ffed077
703309d
9778338
3de7fe7
ee05806
d5232cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
[*.{kt,kts}] | ||
ktlint_function_naming_ignore_when_annotated_with=Composable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,36 @@ | ||
# android-shopping-cart | ||
# android-shopping-cart | ||
|
||
# step1 | ||
|
||
## 기능 요구 사항 | ||
- [x] 상품 목록 화면을 구현한다. | ||
|
||
## 프로그래밍 요구 사항 | ||
- [x] viewModel, Hilt 등은 장바구니 미션에서 활용하지 않는다. | ||
- [x] 상품 목록 화면을 구현할 때 Lazy 컴포넌트를 활용한다. | ||
- [x] 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다. | ||
- [x] 의미있는 단위의 함수를 모아 별도의 파일로 분리해본다. | ||
|
||
# step2 | ||
|
||
## 기능 요구 사항 | ||
- [x] 상품 상세 화면을 구현한다. | ||
- [x] 상품 목록에서 상품을 누르면 상세 화면으로 이동한다. | ||
- [x] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 이동한다. | ||
- [x] 장바구니 화면의 빈 껍데기를 연결한다. | ||
- [x] 상품 목록에서 장바구니 아이콘을 누르면 장바구니 화면으로 이동한다. | ||
- [x] 뒤로가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. | ||
- [x] 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. | ||
|
||
# step3 | ||
|
||
## 기능 요구사항 | ||
- [x] 상품을 장바구니 담는 기능을 구현한다. | ||
- [x] 장바구니 화면을 구현한다. | ||
- [x] 담긴 상품의 수량을 조절할 수 있어야 한다. | ||
- [x] 수량을 1보다 작게 하면 장바구니에서 상품이 제거된다. | ||
- [x] 담긴 상품 가격의 총합이 주문하기 버튼에 표시된다. | ||
|
||
## 프로그래밍 요구사항 | ||
- [x] 상품을 주문하는 기능에 대해서는 구현하지 않아도 된다. | ||
- [x] 장바구니 화면에 대한 테스트코드를 작성한다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package nextstep.shoppingcart | ||
|
||
import androidx.compose.ui.test.isDisplayed | ||
import androidx.compose.ui.test.junit4.createComposeRule | ||
import androidx.compose.ui.test.onAllNodesWithContentDescription | ||
import androidx.compose.ui.test.onFirst | ||
import androidx.compose.ui.test.onNodeWithText | ||
import androidx.compose.ui.test.performClick | ||
import nextstep.shoppingcart.domain.CartProductRepository | ||
import nextstep.shoppingcart.domain.Product | ||
import nextstep.shoppingcart.ui.cart.CartScreen | ||
import org.junit.Before | ||
import org.junit.Rule | ||
import org.junit.Test | ||
|
||
class CartScreenTest { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 장바구니 화면에서 발생할 수 있는 시나리오를 잘 조합하여 테스트 코드로 만들어주셨네요! (혹시 TDD도 해보셨을까요?) 지금의 테스트는 Android View(XML) 환경에서 작성하는 Activity/Fragment단위 테스트와 크게 다르지 않은데요, 이번 기회에 컴포즈의 함수 단위 테스트 작성 이점을 충분히 누려보시면 좋겠어요. |
||
@get:Rule | ||
val composeTestRule = createComposeRule() | ||
|
||
@Before | ||
fun setUp() { | ||
if (CartProductRepository.cartItems.isEmpty()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에디, 채드에게도 남긴 코멘트지만, 다음 두 가지를 고민해보시면 좋겠습니다.
핵심은 이번 미션을 통해 상태를 갖는 컴포넌트, 저장소에 의존하는 컴포넌트는 테스트하기 어렵다라는 점을 얻어가셨으면 좋겠습니다 🙂 |
||
CartProductRepository.addItem( | ||
Product( | ||
id = 0, | ||
name = "레아 사랑해요", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 🙄 |
||
imageUrl = "히히", | ||
price = 1004, | ||
), | ||
) | ||
} | ||
composeTestRule.setContent { | ||
CartScreen() | ||
} | ||
} | ||
|
||
@Test | ||
fun `수량_증가_버튼을_누르면_상품의_수량이_수량_단위만큼_증가한다`() { | ||
// given | ||
val cartItem = CartProductRepository.cartItems.first() | ||
// when | ||
composeTestRule.onAllNodesWithContentDescription( | ||
"수량 증가 버튼", | ||
).onFirst().performClick() | ||
|
||
// then | ||
composeTestRule.onNodeWithText( | ||
(cartItem.quantity.currentValue + cartItem.countInterval).toString(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 개발자들도 이 assert문을 한 눈에 보고 이해할 수 있을까요? |
||
).isDisplayed() | ||
} | ||
|
||
@Test | ||
fun `수량_감소_버튼을_누르면_상품의_수량이_수량_단위만큼_감소한다`() { | ||
// given | ||
val cartItem = CartProductRepository.cartItems.first() | ||
|
||
// when | ||
composeTestRule.onAllNodesWithContentDescription( | ||
"수량 감소 버튼", | ||
).onFirst().performClick() | ||
|
||
// then | ||
composeTestRule.onNodeWithText( | ||
(cartItem.quantity.currentValue - cartItem.countInterval).toString(), | ||
).isDisplayed() | ||
} | ||
|
||
@Test | ||
fun `상품의_수량을_증가시키면_변경된_가격이_버튼에_표시된다`() { | ||
// given | ||
val cartItem = CartProductRepository.cartItems.first() | ||
val previousPrice = CartProductRepository.totalPrice() | ||
composeTestRule.onNodeWithText( | ||
"주문하기(${previousPrice}원)", | ||
).isDisplayed() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. given 절에 assert문이 들어가 있는게 어색하다고 느껴질 것 같아요. 더 궁극적으로는 상태를 가지고 있는 CartScreen을 한 번에 테스트하기보다, 테스트 가능한 작은 단위의 조각으로 나누어 Stateless 컴포넌트를 테스트해야 합니다. 상태 끌어올리기는 컴포즈를 학습하는 과정에서 계속 연습하시게 될거예요! |
||
|
||
// when | ||
composeTestRule.onAllNodesWithContentDescription( | ||
"수량 증가 버튼", | ||
).onFirst().performClick() | ||
|
||
// then | ||
composeTestRule.onNodeWithText( | ||
"주문하기(${previousPrice + cartItem.product.price}원)", | ||
).isDisplayed() | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package nextstep.shoppingcart.domain | ||
|
||
data class CartProduct( | ||
val product: Product, | ||
val quantity: Quantity = Quantity(currentValue = INITIAL_COUNT), | ||
val countInterval: Int = DEFAULT_COUNT_INTERVAL, | ||
) { | ||
val totalPrice: Int = product.price * quantity.currentValue | ||
|
||
fun increaseCount(): CartQuantityUpdateResult = | ||
if ((quantity.maxValue != null && quantity.currentValue < quantity.maxValue) || quantity.maxValue == null) { | ||
CartQuantityUpdateResult.Success( | ||
this.copy( | ||
quantity = quantity.copy(currentValue = quantity.currentValue + countInterval), | ||
), | ||
) | ||
} else { | ||
CartQuantityUpdateResult.MaxFail | ||
} | ||
|
||
fun decreaseCount(): CartQuantityUpdateResult = | ||
if (quantity.currentValue > quantity.minValue) { | ||
CartQuantityUpdateResult.Success( | ||
this.copy( | ||
quantity = quantity.copy(currentValue = quantity.currentValue - countInterval), | ||
), | ||
) | ||
} else { | ||
CartQuantityUpdateResult.MinFail | ||
} | ||
|
||
companion object { | ||
private const val DEFAULT_COUNT_INTERVAL = 1 | ||
private const val INITIAL_COUNT = 1 | ||
val dummy = | ||
CartProduct( | ||
ProductRepository.dummy.first(), | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package nextstep.shoppingcart.domain | ||
|
||
object CartProductRepository { | ||
private val _cartItems: MutableList<CartProduct> = mutableListOf() | ||
val cartItems: List<CartProduct> get() = _cartItems.toList() | ||
|
||
fun addItem(product: Product) { | ||
if (_cartItems.all { it.product.id != product.id }) { | ||
_cartItems.add( | ||
CartProduct( | ||
product = product, | ||
), | ||
) | ||
} else { | ||
plusItemCount(product.id) | ||
} | ||
} | ||
|
||
fun minusItemCount(productId: Long) { | ||
val cartProduct = findItem(productId) | ||
val index = _cartItems.indexOf(cartProduct) | ||
when (val result = cartProduct.decreaseCount()) { | ||
CartQuantityUpdateResult.MaxFail -> throw IllegalArgumentException("수량 감소시에는 수량 증가 실패 이벤트가 발생할 수 없습니다.") | ||
CartQuantityUpdateResult.MinFail -> deleteCartItem(productId) | ||
is CartQuantityUpdateResult.Success -> _cartItems[index] = result.product | ||
} | ||
} | ||
Comment on lines
+19
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 힌트 코드에 있는 기본 코드를 더 구체화해주신 게 보이네요! |
||
|
||
fun plusItemCount(productId: Long): CartQuantityUpdateResult { | ||
val cartProduct = findItem(productId) | ||
val index = _cartItems.indexOf(cartProduct) | ||
val result = cartProduct.increaseCount() | ||
if (result is CartQuantityUpdateResult.Success) { | ||
_cartItems[index] = result.product | ||
} | ||
return result | ||
} | ||
|
||
fun deleteCartItem(productId: Long) { | ||
_cartItems.removeIf { | ||
it.product.id == productId | ||
} | ||
} | ||
|
||
fun findItem(productId: Long): CartProduct = | ||
_cartItems.firstOrNull { | ||
it.product.id == productId | ||
} ?: throw IllegalArgumentException("장바구니에 $productId 에 대한 상품이 없습니다.") | ||
|
||
fun totalPrice(): Int = _cartItems.sumOf { it.totalPrice } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package nextstep.shoppingcart.domain | ||
|
||
sealed interface CartQuantityUpdateResult { | ||
data class Success(val product: CartProduct) : CartQuantityUpdateResult | ||
|
||
data object MinFail : CartQuantityUpdateResult | ||
|
||
data object MaxFail : CartQuantityUpdateResult | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package nextstep.shoppingcart.domain | ||
|
||
data class Product( | ||
val id: Long, | ||
val name: String, | ||
val price: Int, | ||
val imageUrl: String, | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
왜 다들 3단계까지만 하시나요 😭
4단계가 찐인데!