-
Notifications
You must be signed in to change notification settings - Fork 11
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
[악어] 장바구니 미션 제출합니다. #6
base: main
Are you sure you want to change the base?
Changes from 11 commits
71a229c
e4c0cad
d417681
1038216
29b1d18
efdf548
bb07f93
f81c672
959a34d
0882ffd
f33d230
b03e67d
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, Test |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,41 @@ | ||
# android-shopping-cart | ||
# android-shopping-cart | ||
|
||
## Step1 | ||
### 기능 요구 사항 | ||
- [x] 상품 목록 화면을 구현한다. | ||
|
||
### 프로그래밍 요구 사항 | ||
- ViewModel, Hilt 등은 장바구니 미션에서 활용하지 않는다. 컴포즈 학습에 집중하자. | ||
- 상품 목록 화면을 구현할 때 Lazy 컴포넌트를 활용한다. | ||
- 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다. | ||
- 의미있는 단위의 함수를 모아 별도의 파일로 분리해본다. | ||
|
||
|
||
## Step 2 | ||
### 기능 요구 사항 | ||
- [x] 상품 상세 화면을 구현한다. | ||
- [x] 상품 목록에서 상품을 누르면 상품 상세 화면으로 이동한다. | ||
- [x] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. | ||
- [x] 장바구니 화면의 빈 껍데기를 연결한다. | ||
- [x] 상품 목록에서 장바구니 아이콘을 누르면 장바구니 화면으로 이동한다. | ||
- [x] 상품 상세에서 장바구니 담기 버튼을 누르면 장바구니 화면으로 이동한다. | ||
- [x] 뒤로 가기 버튼이나 아이콘을 누르면 직전 화면으로 돌아간다. | ||
- [x] 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. | ||
|
||
### 프로그래밍 요구 사항 | ||
- 장바구니에 실제로 상품이 담기는 기능은 이 단계에서 고려하지 않는다. | ||
- 컴포저블 함수가 너무 많은 일을 하지 않도록 분리하기 위해 노력해 본다. | ||
- 의미있는 단위의 함수를 모아 별도의 파일로 분리해본다. | ||
|
||
|
||
## Step3 | ||
### 기능 요구 사항 | ||
- [x] 상품을 장바구니에 담는 기능을 구현한다. | ||
- [x] 장바구니 화면을 구현한다. | ||
- [x] 담긴 상품의 수량을 조절할 수 있어야 한다. | ||
- [x] 수량을 1보다 작게 하면 장바구니에서 상품이 제거된다 | ||
- [x] 담긴 상품 가격의 총합이 주문하기 버튼에 표시된다. | ||
|
||
### 프로그래밍 요구 사항 | ||
- 상품을 주문하는 기능에 대해서는 구현하지 않아도 된다. | ||
- ~~장바구니 화면에 대한 테스트 코드를 작성한다.~~ 😅 |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package nextstep.shoppingcart.data | ||
|
||
import nextstep.shoppingcart.domain.model.Product | ||
|
||
class CachedProductDataSource : ProductDataSource { | ||
private val products: MutableList<Product> = | ||
mutableListOf<Product>( | ||
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<Product> = products.toList() | ||
|
||
override fun findProduct(id: Long): Product = products.firstOrNull { it.id == id } ?: Product.NULL_PRODUCT | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +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 { | ||
val items: SnapshotStateList<CartItem> = emptyList<CartItem>().toMutableStateList() | ||
|
||
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 { | ||
items.add(CartItem(product = product, count = 1)) | ||
} | ||
updateState() | ||
} | ||
|
||
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) | ||
} | ||
updateState() | ||
} | ||
} | ||
|
||
fun removeAll(product: Product) { | ||
items.removeAll { it.product == product } | ||
updateState() | ||
} | ||
|
||
private fun updateState() { | ||
totalPrice.value = items.sumOf { it.product.price * it.count } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package nextstep.shoppingcart.data | ||
|
||
import nextstep.shoppingcart.domain.model.Product | ||
|
||
class DefaultProductRepository( | ||
private val productDataSource: ProductDataSource | ||
) : ProductRepository { | ||
override fun fetchProducts(): List<Product> = productDataSource.fetchProducts() | ||
|
||
override fun findProduct(id: Long): Product = productDataSource.findProduct(id) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package nextstep.shoppingcart.data | ||
|
||
import nextstep.shoppingcart.domain.model.Product | ||
|
||
interface ProductDataSource { | ||
fun fetchProducts(): List<Product> | ||
fun findProduct(id: Long): Product | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package nextstep.shoppingcart.data | ||
|
||
import nextstep.shoppingcart.domain.model.Product | ||
|
||
interface ProductRepository { | ||
fun fetchProducts(): List<Product> | ||
fun findProduct(id: Long): Product | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package nextstep.shoppingcart.domain.model | ||
|
||
data class CartItem( | ||
val id: Long = maxCartItemId++, | ||
val product: Product, | ||
val count: Int | ||
) { | ||
companion object { | ||
private var maxCartItemId: Long = 0L | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
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) + "원" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, "Null Product", -1000000, "", false) | ||
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. 널 프로덕트가 뭔가요?? ㅇㅅㅇ 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. 널 객체처럼...ㅋㅋㅋ |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
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.compose.runtime.getValue | ||
import androidx.compose.runtime.remember | ||
import nextstep.shoppingcart.data.Cart | ||
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 { | ||
val cartItems = remember { Cart.items } | ||
val totalPrice by Cart.totalPrice | ||
CartScreen( | ||
navigation = { finish() }, | ||
cartItems = cartItems, | ||
totalPrice = totalPrice, | ||
onIncrease = { addItem(it.product) }, | ||
onDecrease = { subItem(it.product) }, | ||
onClear = { removeItem(it.product) } | ||
) | ||
} | ||
} | ||
|
||
private fun addItem(item: Product) = Cart.addOne(item) | ||
|
||
private fun subItem(item: Product) = Cart.removeOne(item) | ||
|
||
private fun removeItem(item: Product) = Cart.removeAll(item) | ||
|
||
companion object { | ||
fun createIntent(context: Context): Intent = Intent(context, CartActivity::class.java) | ||
} | ||
} |
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.
Cart 에는 레포지토리 패턴을 적용하지 않으신 이유가 있을까요??
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.
product 때까지만 해도 확장성을 열어놓고 생각했었는데요,
만들고 나니 현재로서는 레포지토리 패턴으로 확장하지 않아도 되었겠다는 생각이 들더라구요.
그래서 주어진 Cart 코드로만 작성해봐야겠다 라는 의도로 적용하지 않았습니다...ㅎㅎ