diff --git a/README.md b/README.md index fcfdf88ac..22267c7c8 100644 --- a/README.md +++ b/README.md @@ -23,3 +23,13 @@ - 최근 본 상품을 조회할 수 있다. - 상품 목록을 보여준다. - 최근 본 상품이 있는 경우 상품 목록 상단에서 10개까지 확인할 수 있다. +- [ ] 상품 목록에서 장바구니에 담을 상품의 수를 선택할 수 있다. (B마트 UX 참고) +- [ ] 버튼을 누르면 장바구니에 상품이 추가됨과 동시에 수량 선택 버튼이 노출된다. +- [ ] 상품 목록의 상품 수가 변화하면 장바구니에도 반영되어야 한다. +- [ ] 장바구니의 상품 수가 변화하면 상품 목록에도 반영되어야 한다. +- [ ] 장바구니 화면에서 체크박스로 주문할 상품 범위를 조정할 수 있다. +- [ ] 전체 체크박스를 선택하면 해당 페이지 내의 상품들만 선택된다. +- [ ] 페이지가 바뀌어도 선택된 항목은 유지된다. +- [ ] 마지막으로 본 상품 1개를 상품 상세 페이지에서 확인할 수 있다. +- [ ] 마지막으로 본 상품을 선택했을 때는 마지막으로 본 상품이 보이지 않는다. +- [ ] 마지막으로 본 상품 페이지에서 뒤로 가기를 하면 상품 목록으로 이동한다. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b3a6c846d..25fe8a882 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,4 +60,12 @@ dependencies { // concatAdapter implementation("androidx.recyclerview:recyclerview:1.3.0") + + // core-testing + testImplementation("androidx.arch.core:core-testing:2.2.0") + + // OkHttp + implementation("com.squareup.okhttp3:okhttp:4.11.0") + implementation("com.squareup.okhttp3:mockwebserver:4.11.0") + testImplementation("com.squareup.okhttp3:mockwebserver:4.11.0") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 20464efff..164335ce8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,12 +12,13 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.Shopping" + android:usesCleartextTraffic="true" tools:targetApi="31"> - fun add(product: DataProduct) - fun remove(product: DataProduct) -} diff --git a/app/src/main/java/woowacourse/shopping/data/database/dao/basket/BasketDaoImpl.kt b/app/src/main/java/woowacourse/shopping/data/database/dao/basket/BasketDaoImpl.kt deleted file mode 100644 index 84469b529..000000000 --- a/app/src/main/java/woowacourse/shopping/data/database/dao/basket/BasketDaoImpl.kt +++ /dev/null @@ -1,60 +0,0 @@ -package woowacourse.shopping.data.database.dao.basket - -import android.annotation.SuppressLint -import android.content.ContentValues -import android.provider.BaseColumns -import woowacourse.shopping.data.database.ShoppingDatabase -import woowacourse.shopping.data.database.contract.BasketContract -import woowacourse.shopping.data.model.DataPageNumber -import woowacourse.shopping.data.model.DataPrice -import woowacourse.shopping.data.model.DataProduct -import woowacourse.shopping.util.extension.safeSubList - -class BasketDaoImpl(private val database: ShoppingDatabase) : BasketDao { - @SuppressLint("Range") - override fun getPartially(page: DataPageNumber): List { - val products = mutableListOf() - - val db = database.writableDatabase - val cursor = db.rawQuery(GET_ALL_QUERY, null) - - while (cursor.moveToNext()) { - val id: Int = cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)) - val name: String = - cursor.getString(cursor.getColumnIndex(BasketContract.COLUMN_NAME)) - val price: DataPrice = - DataPrice(cursor.getInt(cursor.getColumnIndex(BasketContract.COLUMN_PRICE))) - val imageUrl: String = - cursor.getString(cursor.getColumnIndex(BasketContract.COLUMN_IMAGE_URL)) - products.add(DataProduct(id, name, price, imageUrl)) - } - cursor.close() - - return products.safeSubList(page.start, page.end) - } - - override fun add(product: DataProduct) { - val contentValues = ContentValues().apply { - put(BasketContract.COLUMN_NAME, product.name) - put(BasketContract.COLUMN_PRICE, product.price.value) - put(BasketContract.COLUMN_IMAGE_URL, product.imageUrl) - put(BasketContract.COLUMN_CREATED, System.currentTimeMillis()) - } - - database.writableDatabase.insert(BasketContract.TABLE_NAME, null, contentValues) - } - - override fun remove(product: DataProduct) { - database.writableDatabase.delete( - BasketContract.TABLE_NAME, - "${BaseColumns._ID} = ?", - arrayOf(product.id.toString()) - ) - } - - companion object { - private val GET_ALL_QUERY = """ - SELECT * FROM ${BasketContract.TABLE_NAME} - """.trimIndent() - } -} diff --git a/app/src/main/java/woowacourse/shopping/data/database/dao/cart/CartDao.kt b/app/src/main/java/woowacourse/shopping/data/database/dao/cart/CartDao.kt new file mode 100644 index 000000000..5582eac7b --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/database/dao/cart/CartDao.kt @@ -0,0 +1,24 @@ +package woowacourse.shopping.data.database.dao.cart + +import woowacourse.shopping.data.entity.CartEntity +import woowacourse.shopping.data.model.DataCart +import woowacourse.shopping.data.model.DataCartProduct +import woowacourse.shopping.data.model.DataPage +import woowacourse.shopping.data.model.Product + +interface CartDao { + fun getCartEntitiesByPage(page: DataPage): List + fun insert(product: Product, count: Int) + fun deleteByProductId(id: Int) + fun contains(product: Product): Boolean + fun count(product: Product): Int + fun getProductInCartSize(): Int + fun addProductCount(product: Product, count: Int) + fun minusProductCount(product: Product, count: Int) + fun update(cartProduct: DataCartProduct) + fun updateCount(product: Product, count: Int) + fun getCheckedProductCount(): Int + fun deleteCheckedProducts() + fun getAllCartEntity(): List + fun getCartEntity(productId: Int): CartEntity +} diff --git a/app/src/main/java/woowacourse/shopping/data/database/dao/cart/CartDaoImpl.kt b/app/src/main/java/woowacourse/shopping/data/database/dao/cart/CartDaoImpl.kt new file mode 100644 index 000000000..dc6baf24f --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/database/dao/cart/CartDaoImpl.kt @@ -0,0 +1,250 @@ +package woowacourse.shopping.data.database.dao.cart + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.provider.BaseColumns +import android.util.Log +import woowacourse.shopping.data.database.ShoppingDatabase +import woowacourse.shopping.data.database.contract.CartContract +import woowacourse.shopping.data.database.contract.ProductContract +import woowacourse.shopping.data.entity.CartEntity +import woowacourse.shopping.data.model.CartProduct +import woowacourse.shopping.data.model.DataCart +import woowacourse.shopping.data.model.DataCartProduct +import woowacourse.shopping.data.model.DataPage +import woowacourse.shopping.data.model.DataPrice +import woowacourse.shopping.data.model.Product +import woowacourse.shopping.data.model.ProductCount +import woowacourse.shopping.util.extension.safeSubList + +class CartDaoImpl(private val database: ShoppingDatabase) : CartDao { + @SuppressLint("Range") + override fun getAllCartEntity(): List { + val db = database.readableDatabase + val cartEntities = mutableListOf() + val cursor = db.rawQuery(GET_ALL_CART_ENTITY_QUERY, null) + while (cursor.moveToNext()) { + val cartId: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.CART_ID)) + val productId: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.PRODUCT_ID)) + val count: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.COLUMN_COUNT)) + val isChecked: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.COLUMN_CHECKED)) + cartEntities.add(CartEntity(cartId, productId, count, isChecked)) + } + cursor.close() + return cartEntities + } + + @SuppressLint("Range") + override fun getCartEntity(productId: Int): CartEntity { + val db = database.readableDatabase + val cursor = db.rawQuery(GET_CART_ENTITY_QUERY, arrayOf(productId.toString())) + val cartEntity = if (cursor.moveToNext()) { + val cartId: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.CART_ID)) + val count: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.COLUMN_COUNT)) + val isChecked: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.COLUMN_CHECKED)) + CartEntity(cartId, productId, count, isChecked) + } else { + CartEntity(0, productId, 0, 0) + } + cursor.close() + return cartEntity + } + + @SuppressLint("Range") + override fun getCartEntitiesByPage(page: DataPage): List { + val cartEntities = mutableListOf() + + val db = database.readableDatabase + val cursor = db.rawQuery(GET_ALL_CART_ENTITY_QUERY, null) + + while (cursor.moveToNext()) { + val cartId: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.CART_ID)) + val productId: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.PRODUCT_ID)) + val count: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.COLUMN_COUNT)) + val isChecked: Int = + cursor.getInt(cursor.getColumnIndex(CartContract.COLUMN_CHECKED)) + cartEntities.add(CartEntity(cartId, productId, count, isChecked)) + } + cursor.close() + return cartEntities.safeSubList(page.start, page.end + 1) + } + + + override fun insert(product: Product, count: Int) { + val contentValues = ContentValues().apply { + put(CartContract.PRODUCT_ID, product.id) + put(CartContract.COLUMN_CREATED, System.currentTimeMillis()) + put(CartContract.COLUMN_COUNT, count) + } + + database.writableDatabase.insert(CartContract.TABLE_NAME, null, contentValues) + } + + override fun getProductInCartSize(): Int { + val db = database.writableDatabase + val cursor = db.rawQuery(GET_PRODUCT_IN_CART_SIZE, null) + cursor.moveToNext() + + val productInCartSize = cursor.getInt(0) + cursor.close() + return productInCartSize + } + + override fun deleteByProductId(id: Int) { + database.writableDatabase.delete( + CartContract.TABLE_NAME, + "${CartContract.PRODUCT_ID} = ?", + arrayOf(id.toString()) + ) + } + + override fun update(cartProduct: DataCartProduct) { + val contentValues = ContentValues().apply { + put(CartContract.PRODUCT_ID, cartProduct.product.id) + put(CartContract.COLUMN_COUNT, cartProduct.selectedCount.value) + put(CartContract.COLUMN_CHECKED, cartProduct.isChecked) + } + + database.writableDatabase.update( + CartContract.TABLE_NAME, + contentValues, + "${CartContract.PRODUCT_ID} = ?", + arrayOf(cartProduct.product.id.toString()) + ) + } + + @SuppressLint("Range") + override fun addProductCount(product: Product, count: Int) { + when (val originCount = count(product)) { + 0 -> insert(product, count) + else -> updateCount(product, originCount + count) + } + } + + @SuppressLint("Range") + override fun minusProductCount(product: Product, count: Int) { + when (val originCount = count(product)) { + 0 -> return + else -> updateCount(product, originCount - count) + } + } + + override fun updateCount(product: Product, count: Int) { + val contentValues = ContentValues().apply { + put(CartContract.PRODUCT_ID, product.id) + put(CartContract.COLUMN_COUNT, count) + } + + database.writableDatabase.update( + CartContract.TABLE_NAME, + contentValues, + "${CartContract.PRODUCT_ID} = ?", + arrayOf(product.id.toString()) + ) + } + + override fun getCheckedProductCount(): Int { + val db = database.writableDatabase + val cursor = db.rawQuery(GET_CHECKED_PRODUCT_COUNT, null) + cursor.moveToNext() + + val checkedProductCount = cursor.getInt(0) + cursor.close() + return checkedProductCount + } + + override fun deleteCheckedProducts() { + database.writableDatabase.delete( + CartContract.TABLE_NAME, + "${CartContract.COLUMN_CHECKED} = ?", + arrayOf("1") + ) + } + + override fun contains(product: Product): Boolean { + val db = database.writableDatabase + val cursor = db.rawQuery( + """ + SELECT * FROM ${CartContract.TABLE_NAME} + WHERE ${CartContract.PRODUCT_ID} = ? + """.trimIndent(), arrayOf(product.id.toString()) + ) + + val result = cursor.count > 0 + cursor.close() + return result + } + + @SuppressLint("Range") + override fun count(product: Product): Int { + val db = database.writableDatabase + val cursor = db.rawQuery( + """ + SELECT * FROM ${CartContract.TABLE_NAME} + WHERE ${CartContract.PRODUCT_ID} = ? + """.trimIndent(), arrayOf(product.id.toString()) + ) + + val count = if (cursor.count > 0) { + cursor.moveToNext() + val realCount = cursor.getInt(cursor.getColumnIndex(CartContract.COLUMN_COUNT)) + if (realCount == -1) 0 else realCount + } else { + 0 + } + + cursor.close() + return count + } + + companion object { + private val GET_ALL_CART_ENTITY_QUERY = """ + SELECT * FROM ${CartContract.TABLE_NAME} + """.trimIndent() + + private val GET_CART_ENTITY_QUERY = """ + SELECT * FROM ${CartContract.TABLE_NAME} + WHERE ${CartContract.PRODUCT_ID} = ? + """.trimIndent() + + private val GET_ALL_CART_PRODUCT_QUERY = """ + SELECT * FROM ${ProductContract.TABLE_NAME} as product + LEFT JOIN ${CartContract.TABLE_NAME} as cart + ON cart.${CartContract.PRODUCT_ID} = product.${BaseColumns._ID} + """.trimIndent() + + private val GET_ALL_CART_PRODUCT_IN_CART_QUERY = """ + SELECT * FROM ${ProductContract.TABLE_NAME} as product + LEFT JOIN ${CartContract.TABLE_NAME} as cart + ON cart.${CartContract.PRODUCT_ID} = product.${BaseColumns._ID} + WHERE ${CartContract.COLUMN_COUNT} > 0 + """.trimIndent() + + private val GET_PRODUCT_IN_CART_SIZE = """ + SELECT SUM(${CartContract.COLUMN_COUNT}) FROM ${CartContract.TABLE_NAME} + WHERE ${CartContract.COLUMN_COUNT} > 0 + """.trimIndent() + + private val GET_TOTAL_PRICE = """ + SELECT SUM(${ProductContract.COLUMN_PRICE} * ${CartContract.COLUMN_COUNT}) FROM ${ProductContract.TABLE_NAME} as product + LEFT JOIN ${CartContract.TABLE_NAME} as cart + ON cart.${CartContract.PRODUCT_ID} = product.${BaseColumns._ID} + WHERE ${CartContract.COLUMN_COUNT} > 0 AND ${CartContract.COLUMN_CHECKED} = 1 + """.trimIndent() + + private val GET_CHECKED_PRODUCT_COUNT = """ + SELECT COUNT(*) FROM ${CartContract.TABLE_NAME} + WHERE ${CartContract.COLUMN_COUNT} > 0 AND ${CartContract.COLUMN_CHECKED} = 1 + """.trimIndent() + } +} diff --git a/app/src/main/java/woowacourse/shopping/data/database/dao/product/ProductDao.kt b/app/src/main/java/woowacourse/shopping/data/database/dao/product/ProductDao.kt deleted file mode 100644 index b46c3cb2e..000000000 --- a/app/src/main/java/woowacourse/shopping/data/database/dao/product/ProductDao.kt +++ /dev/null @@ -1,7 +0,0 @@ -package woowacourse.shopping.data.database.dao.product - -import woowacourse.shopping.data.model.DataProduct - -interface ProductDao { - fun getPartially(size: Int, lastId: Int): List -} diff --git a/app/src/main/java/woowacourse/shopping/data/database/dao/product/ProductDaoImpl.kt b/app/src/main/java/woowacourse/shopping/data/database/dao/product/ProductDaoImpl.kt deleted file mode 100644 index 22a7c6904..000000000 --- a/app/src/main/java/woowacourse/shopping/data/database/dao/product/ProductDaoImpl.kt +++ /dev/null @@ -1,49 +0,0 @@ -package woowacourse.shopping.data.database.dao.product - -import android.annotation.SuppressLint -import android.content.ContentValues -import android.database.sqlite.SQLiteOpenHelper -import android.provider.BaseColumns -import woowacourse.shopping.data.database.contract.ProductContract -import woowacourse.shopping.data.model.DataPrice -import woowacourse.shopping.data.model.DataProduct - -class ProductDaoImpl(private val database: SQLiteOpenHelper) : ProductDao { - @SuppressLint("Range") - override fun getPartially(size: Int, lastId: Int): List { - val products = mutableListOf() - val db = database.writableDatabase - val cursor = - db.rawQuery(GET_PARTIALLY_QUERY, arrayOf(lastId.toString(), size.toString())) - while (cursor.moveToNext()) { - val id: Int = cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)) - val name: String = - cursor.getString(cursor.getColumnIndex(ProductContract.COLUMN_NAME)) - val price: DataPrice = - DataPrice(cursor.getInt(cursor.getColumnIndex(ProductContract.COLUMN_PRICE))) - val imageUrl: String = - cursor.getString(cursor.getColumnIndex(ProductContract.COLUMN_IMAGE_URL)) - products.add(DataProduct(id, name, price, imageUrl)) - } - cursor.close() - return products - } - - fun add(product: DataProduct) { - val contentValues = ContentValues().apply { - put(ProductContract.COLUMN_NAME, product.name) - put(ProductContract.COLUMN_PRICE, product.price.value) - put(ProductContract.COLUMN_IMAGE_URL, product.imageUrl) - } - - database.writableDatabase.insert(ProductContract.TABLE_NAME, null, contentValues) - } - - companion object { - private val GET_PARTIALLY_QUERY = """ - SELECT * FROM ${ProductContract.TABLE_NAME} - WHERE ${BaseColumns._ID} > ? - ORDER BY ${BaseColumns._ID} LIMIT ? - """.trimIndent() - } -} diff --git a/app/src/main/java/woowacourse/shopping/data/database/dao/recentproduct/RecentProductDao.kt b/app/src/main/java/woowacourse/shopping/data/database/dao/recentproduct/RecentProductDao.kt index f67797bda..b63be5b22 100644 --- a/app/src/main/java/woowacourse/shopping/data/database/dao/recentproduct/RecentProductDao.kt +++ b/app/src/main/java/woowacourse/shopping/data/database/dao/recentproduct/RecentProductDao.kt @@ -4,7 +4,7 @@ import woowacourse.shopping.data.model.DataRecentProduct interface RecentProductDao { fun getSize(): Int - fun getPartially(size: Int): List - fun add(recentProduct: DataRecentProduct) + fun getRecentProductsPartially(size: Int): List + fun addRecentProduct(item: DataRecentProduct) fun removeLast() } diff --git a/app/src/main/java/woowacourse/shopping/data/database/dao/recentproduct/RecentProductDaoImpl.kt b/app/src/main/java/woowacourse/shopping/data/database/dao/recentproduct/RecentProductDaoImpl.kt index a3fa3866c..83cea561b 100644 --- a/app/src/main/java/woowacourse/shopping/data/database/dao/recentproduct/RecentProductDaoImpl.kt +++ b/app/src/main/java/woowacourse/shopping/data/database/dao/recentproduct/RecentProductDaoImpl.kt @@ -7,8 +7,8 @@ import android.provider.BaseColumns import woowacourse.shopping.data.database.contract.ProductContract import woowacourse.shopping.data.database.contract.RecentProductContract import woowacourse.shopping.data.model.DataPrice -import woowacourse.shopping.data.model.DataProduct import woowacourse.shopping.data.model.DataRecentProduct +import woowacourse.shopping.data.model.Product class RecentProductDaoImpl(private val database: SQLiteOpenHelper) : RecentProductDao { @@ -24,7 +24,7 @@ class RecentProductDaoImpl(private val database: SQLiteOpenHelper) : RecentProdu } @SuppressLint("Range") - override fun getPartially(size: Int): List { + override fun getRecentProductsPartially(size: Int): List { val products = mutableListOf() val db = database.writableDatabase val cursor = db.rawQuery(GET_PARTIALLY_QUERY, arrayOf(size.toString())) @@ -38,17 +38,17 @@ class RecentProductDaoImpl(private val database: SQLiteOpenHelper) : RecentProdu DataPrice(cursor.getInt(cursor.getColumnIndex(RecentProductContract.COLUMN_PRICE))) val imageUrl: String = cursor.getString(cursor.getColumnIndex(RecentProductContract.COLUMN_IMAGE_URL)) - products.add(DataRecentProduct(id, DataProduct(productId, name, price, imageUrl))) + products.add(DataRecentProduct(id, Product(productId, name, price, imageUrl))) } cursor.close() return products } - override fun add(recentProduct: DataRecentProduct) { + override fun addRecentProduct(item: DataRecentProduct) { val contentValues = ContentValues().apply { - put(RecentProductContract.COLUMN_NAME, recentProduct.product.name) - put(RecentProductContract.COLUMN_PRICE, recentProduct.product.price.value) - put(RecentProductContract.COLUMN_IMAGE_URL, recentProduct.product.imageUrl) + put(RecentProductContract.COLUMN_NAME, item.product.name) + put(RecentProductContract.COLUMN_PRICE, item.product.price.value) + put(RecentProductContract.COLUMN_IMAGE_URL, item.product.imageUrl) } database.writableDatabase.insert(RecentProductContract.TABLE_NAME, null, contentValues) diff --git a/app/src/main/java/woowacourse/shopping/data/datasource/basket/BasketDataSource.kt b/app/src/main/java/woowacourse/shopping/data/datasource/basket/BasketDataSource.kt deleted file mode 100644 index f5f185efd..000000000 --- a/app/src/main/java/woowacourse/shopping/data/datasource/basket/BasketDataSource.kt +++ /dev/null @@ -1,14 +0,0 @@ -package woowacourse.shopping.data.datasource.basket - -import woowacourse.shopping.data.model.DataPageNumber -import woowacourse.shopping.data.model.DataProduct - -interface BasketDataSource { - interface Local { - fun getPartially(page: DataPageNumber): List - fun add(product: DataProduct) - fun remove(product: DataProduct) - } - - interface Remote -} diff --git a/app/src/main/java/woowacourse/shopping/data/datasource/basket/LocalBasketDataSource.kt b/app/src/main/java/woowacourse/shopping/data/datasource/basket/LocalBasketDataSource.kt deleted file mode 100644 index 4dae3bdc7..000000000 --- a/app/src/main/java/woowacourse/shopping/data/datasource/basket/LocalBasketDataSource.kt +++ /dev/null @@ -1,18 +0,0 @@ -package woowacourse.shopping.data.datasource.basket - -import woowacourse.shopping.data.database.dao.basket.BasketDao -import woowacourse.shopping.data.model.DataPageNumber -import woowacourse.shopping.data.model.DataProduct - -class LocalBasketDataSource(private val dao: BasketDao) : BasketDataSource.Local { - override fun getPartially(page: DataPageNumber): List = - dao.getPartially(page) - - override fun add(product: DataProduct) { - dao.add(product) - } - - override fun remove(product: DataProduct) { - dao.remove(product) - } -} diff --git a/app/src/main/java/woowacourse/shopping/data/datasource/cart/CartDataSource.kt b/app/src/main/java/woowacourse/shopping/data/datasource/cart/CartDataSource.kt new file mode 100644 index 000000000..d1cb56023 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/datasource/cart/CartDataSource.kt @@ -0,0 +1,23 @@ +package woowacourse.shopping.data.datasource.cart + +import woowacourse.shopping.data.entity.CartEntity +import woowacourse.shopping.data.model.DataCart +import woowacourse.shopping.data.model.DataCartProduct +import woowacourse.shopping.data.model.DataPage +import woowacourse.shopping.data.model.Product + +interface CartDataSource { + interface Local { + fun getAllCartEntity(): List + fun getCartEntity(productId: Int): CartEntity + fun increaseCartCount(product: Product, count: Int) + fun decreaseCartCount(product: Product, count: Int) + fun deleteByProductId(productId: Int) + fun getProductInCartSize(): Int + fun update(cartProducts: List) + fun getCheckedProductCount(): Int + fun removeCheckedProducts() + } + + interface Remote +} diff --git a/app/src/main/java/woowacourse/shopping/data/datasource/cart/LocalCartDataSource.kt b/app/src/main/java/woowacourse/shopping/data/datasource/cart/LocalCartDataSource.kt new file mode 100644 index 000000000..79b1dcfaf --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/datasource/cart/LocalCartDataSource.kt @@ -0,0 +1,43 @@ +package woowacourse.shopping.data.datasource.cart + +import woowacourse.shopping.data.database.dao.cart.CartDao +import woowacourse.shopping.data.entity.CartEntity +import woowacourse.shopping.data.model.DataCart +import woowacourse.shopping.data.model.DataCartProduct +import woowacourse.shopping.data.model.DataPage +import woowacourse.shopping.data.model.Product + +class LocalCartDataSource(private val dao: CartDao) : CartDataSource.Local { + override fun getAllCartEntity(): List = dao.getAllCartEntity() + + override fun getCartEntity(productId: Int): CartEntity = dao.getCartEntity(productId) + + override fun increaseCartCount(product: Product, count: Int) { + dao.addProductCount(product, count) + } + + override fun getProductInCartSize(): Int = dao.getProductInCartSize() + + override fun update(cartProducts: List) { + cartProducts.forEach(dao::update) + } + + override fun getCheckedProductCount(): Int = dao.getCheckedProductCount() + + override fun removeCheckedProducts() { + dao.deleteCheckedProducts() + } + + override fun decreaseCartCount(product: Product, count: Int) { + val productCount = dao.count(product) + when { + !dao.contains(product) -> return + productCount > count -> dao.minusProductCount(product, count) + else -> deleteByProductId(product.id) + } + } + + override fun deleteByProductId(productId: Int) { + dao.deleteByProductId(productId) + } +} diff --git a/app/src/main/java/woowacourse/shopping/data/datasource/product/LocalProductDataSource.kt b/app/src/main/java/woowacourse/shopping/data/datasource/product/LocalProductDataSource.kt deleted file mode 100644 index 909e8c652..000000000 --- a/app/src/main/java/woowacourse/shopping/data/datasource/product/LocalProductDataSource.kt +++ /dev/null @@ -1,9 +0,0 @@ -package woowacourse.shopping.data.datasource.product - -import woowacourse.shopping.data.database.dao.product.ProductDao -import woowacourse.shopping.data.model.DataProduct - -class LocalProductDataSource(private val dao: ProductDao) : ProductDataSource.Local { - override fun getPartially(size: Int, lastId: Int): List = - dao.getPartially(size, lastId) -} diff --git a/app/src/main/java/woowacourse/shopping/data/datasource/product/ProductDataSource.kt b/app/src/main/java/woowacourse/shopping/data/datasource/product/ProductDataSource.kt index a1d650252..c61329319 100644 --- a/app/src/main/java/woowacourse/shopping/data/datasource/product/ProductDataSource.kt +++ b/app/src/main/java/woowacourse/shopping/data/datasource/product/ProductDataSource.kt @@ -1,11 +1,13 @@ package woowacourse.shopping.data.datasource.product -import woowacourse.shopping.data.model.DataProduct +import woowacourse.shopping.data.model.Page +import woowacourse.shopping.data.model.Product interface ProductDataSource { - interface Local { - fun getPartially(size: Int, lastId: Int): List - } + interface Local - interface Remote + interface Remote { + fun getProductByPage(page: Page): List + fun findProductById(id: Int): Product? + } } diff --git a/app/src/main/java/woowacourse/shopping/data/datasource/product/RemoteProductDataSource.kt b/app/src/main/java/woowacourse/shopping/data/datasource/product/RemoteProductDataSource.kt new file mode 100644 index 000000000..aedfd8f25 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/datasource/product/RemoteProductDataSource.kt @@ -0,0 +1,89 @@ +package woowacourse.shopping.data.datasource.product + +import okhttp3.Call +import okhttp3.Callback +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.json.JSONArray +import org.json.JSONObject +import woowacourse.shopping.data.model.Page +import woowacourse.shopping.data.model.Price +import woowacourse.shopping.data.model.Product +import woowacourse.shopping.server.GET +import woowacourse.shopping.server.ShoppingMockWebServer +import java.io.IOException +import java.util.concurrent.CountDownLatch + +class RemoteProductDataSource : ProductDataSource.Remote { + private val shoppingService: ShoppingMockWebServer = ShoppingMockWebServer() + private var BASE_URL: String + + init { + shoppingService.start() + shoppingService.join() + BASE_URL = shoppingService.baseUrl + } + + override fun getProductByPage(page: Page): List { + shoppingService.join() + val url = "${BASE_URL}/products?start=${page.start}&count=${page.sizePerPage}" + val httpClient = OkHttpClient() + val request = Request.Builder().url(url).method(GET, null).build() + val products = mutableListOf() + val countDownLatch = CountDownLatch(1) + + httpClient.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + countDownLatch.countDown() + } + + override fun onResponse(call: Call, response: Response) { + val input = response.body?.string() + val jsonArray = JSONArray(input) + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.getJSONObject(i) + products.add(convertToProduct(jsonObject)) + } + countDownLatch.countDown() + } + }) + + countDownLatch.await() + return products + } + + override fun findProductById(id: Int): Product? { + shoppingService.join() + val url = "${BASE_URL}/products?productId=${id}" + val httpClient = OkHttpClient() + val request = Request.Builder().url(url).method(GET, null).build() + val countDownLatch = CountDownLatch(1) + var product: Product? = null + + httpClient.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + countDownLatch.countDown() + } + + override fun onResponse(call: Call, response: Response) { + val input = response.body?.string() + val jsonObject = JSONObject(input) + if (jsonObject.getInt("id") == id) { + product = convertToProduct(jsonObject) + } + countDownLatch.countDown() + } + }) + + countDownLatch.await() + return product + } + + private fun convertToProduct(response: JSONObject): Product = Product( + id = response.getInt("id"), + imageUrl = response.getString("imageUrl"), + name = response.getString("name"), + price = Price(response.getInt("price")) + ) +} diff --git a/app/src/main/java/woowacourse/shopping/data/datasource/recentproduct/LocalRecentProductDataSource.kt b/app/src/main/java/woowacourse/shopping/data/datasource/recentproduct/LocalRecentProductDataSource.kt index 5276e2904..ed5d640cf 100644 --- a/app/src/main/java/woowacourse/shopping/data/datasource/recentproduct/LocalRecentProductDataSource.kt +++ b/app/src/main/java/woowacourse/shopping/data/datasource/recentproduct/LocalRecentProductDataSource.kt @@ -6,13 +6,14 @@ import woowacourse.shopping.data.model.DataRecentProduct class LocalRecentProductDataSource(private val dao: RecentProductDao) : RecentProductDataSource.Local { - override fun getPartially(size: Int): List = dao.getPartially(size) + override fun getPartially(size: Int): List = + dao.getRecentProductsPartially(size) override fun add(product: DataRecentProduct) { while (dao.getSize() >= STORED_DATA_SIZE) { dao.removeLast() } - dao.add(product) + dao.addRecentProduct(product) } companion object { diff --git a/app/src/main/java/woowacourse/shopping/data/entity/CartEntity.kt b/app/src/main/java/woowacourse/shopping/data/entity/CartEntity.kt new file mode 100644 index 000000000..44f25b5ea --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/entity/CartEntity.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.data.entity + +typealias DataCartEntity = CartEntity + +class CartEntity( + val id: Int, + val productId: Int, + val count: Int, + val checked: Int, +) diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/CartEntityMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/CartEntityMapper.kt new file mode 100644 index 000000000..a89e87c5e --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/mapper/CartEntityMapper.kt @@ -0,0 +1,12 @@ +package woowacourse.shopping.data.mapper + +import woowacourse.shopping.data.entity.DataCartEntity +import woowacourse.shopping.domain.model.DomainCartEntity + +fun DataCartEntity.toDomain(): DomainCartEntity = DomainCartEntity( + id, productId, count, checked == 1 +) + +fun DomainCartEntity.toData(): DataCartEntity = DataCartEntity( + id, productId, count, if (checked) 1 else 0 +) diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/CartMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/CartMapper.kt new file mode 100644 index 000000000..1eb147fe3 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/mapper/CartMapper.kt @@ -0,0 +1,12 @@ +package woowacourse.shopping.data.mapper + +import woowacourse.shopping.data.model.DataCart +import woowacourse.shopping.domain.model.DomainCart + +fun DataCart.toDomain(): DomainCart = DomainCart( + items = cartProducts.map { it.toDomain() }, +) + +fun DomainCart.toData(): DataCart = DataCart( + cartProducts = items.map { it.toData() }, +) diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/CartProductMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/CartProductMapper.kt new file mode 100644 index 000000000..90e6b3de1 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/mapper/CartProductMapper.kt @@ -0,0 +1,21 @@ +package woowacourse.shopping.data.mapper + +import woowacourse.shopping.data.model.DataCartProduct +import woowacourse.shopping.domain.model.CartProduct +import woowacourse.shopping.domain.model.DomainCartProduct + +fun DataCartProduct.toDomain(): CartProduct = CartProduct( + id = id, + product = product.toDomain(), + selectedCount = selectedCount.toDomain(), + isChecked = isChecked == 1, +) + +fun CartProduct.toData(): DataCartProduct = DataCartProduct( + id = id, + product = product.toData(), + selectedCount = selectedCount.toData(), + isChecked = if (isChecked) 1 else 0, +) + +fun List.toData(): List = map { it.toData() } diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/PageMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/PageMapper.kt new file mode 100644 index 000000000..3293a870a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/mapper/PageMapper.kt @@ -0,0 +1,7 @@ +package woowacourse.shopping.data.mapper + +import woowacourse.shopping.data.model.DataPage +import woowacourse.shopping.domain.model.page.DomainPage + +fun DomainPage.toData(extraSize: Int = 0): DataPage = + DataPage(value = value, sizePerPage = sizePerPage + extraSize) diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/PageNumberMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/PageNumberMapper.kt deleted file mode 100644 index 14e270dde..000000000 --- a/app/src/main/java/woowacourse/shopping/data/mapper/PageNumberMapper.kt +++ /dev/null @@ -1,9 +0,0 @@ -package woowacourse.shopping.data.mapper - -import woowacourse.shopping.data.model.DataPageNumber -import woowacourse.shopping.domain.DomainPageNumber - -fun DataPageNumber.toDomain(): DomainPageNumber = DomainPageNumber(value = value) - -fun DomainPageNumber.toData(): DataPageNumber = - DataPageNumber(value = value, sizePerPage = sizePerPage) diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/PriceMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/PriceMapper.kt index 9f09d5c2e..e039adca1 100644 --- a/app/src/main/java/woowacourse/shopping/data/mapper/PriceMapper.kt +++ b/app/src/main/java/woowacourse/shopping/data/mapper/PriceMapper.kt @@ -1,7 +1,7 @@ package woowacourse.shopping.data.mapper import woowacourse.shopping.data.model.DataPrice -import woowacourse.shopping.domain.Price +import woowacourse.shopping.domain.model.Price fun DataPrice.toDomain(): Price = Price(value) diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/ProductCountMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/ProductCountMapper.kt new file mode 100644 index 000000000..3271a2554 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/mapper/ProductCountMapper.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.data.mapper + +import woowacourse.shopping.data.model.DataProductCount +import woowacourse.shopping.domain.model.ProductCount + +fun DataProductCount.toDomain(): ProductCount = + ProductCount(value) + +fun ProductCount.toData(): DataProductCount = + DataProductCount(value) diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/ProductMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/ProductMapper.kt index 699cc27cf..4d1b2dd7f 100644 --- a/app/src/main/java/woowacourse/shopping/data/mapper/ProductMapper.kt +++ b/app/src/main/java/woowacourse/shopping/data/mapper/ProductMapper.kt @@ -1,10 +1,21 @@ package woowacourse.shopping.data.mapper import woowacourse.shopping.data.model.DataProduct -import woowacourse.shopping.domain.Product +import woowacourse.shopping.domain.model.Product fun DataProduct.toDomain(): Product = - Product(id = id, name = name, price = price.toDomain(), imageUrl = imageUrl) + Product( + id = id, + name = name, + price = price.toDomain(), + imageUrl = imageUrl, + ) fun Product.toData(): DataProduct = - DataProduct(id = id, name = name, price = price.toData(), imageUrl = imageUrl) + DataProduct( + id = id, + name = name, + price = price.toData(), + imageUrl = imageUrl, + ) + diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/RecentProductMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/RecentProductMapper.kt index ed50923c2..79bd048e2 100644 --- a/app/src/main/java/woowacourse/shopping/data/mapper/RecentProductMapper.kt +++ b/app/src/main/java/woowacourse/shopping/data/mapper/RecentProductMapper.kt @@ -1,10 +1,12 @@ package woowacourse.shopping.data.mapper import woowacourse.shopping.data.model.DataRecentProduct -import woowacourse.shopping.domain.RecentProduct +import woowacourse.shopping.domain.model.RecentProduct fun DataRecentProduct.toDomain(): RecentProduct = RecentProduct(id = id, product = product.toDomain()) fun RecentProduct.toData(): DataRecentProduct = DataRecentProduct(id = id, product = product.toData()) + +fun List.toDomain(): List = map { it.toDomain() } diff --git a/app/src/main/java/woowacourse/shopping/data/model/Cart.kt b/app/src/main/java/woowacourse/shopping/data/model/Cart.kt new file mode 100644 index 000000000..7d0a97cff --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/model/Cart.kt @@ -0,0 +1,7 @@ +package woowacourse.shopping.data.model + +typealias DataCart = Cart + +data class Cart( + val cartProducts: List = emptyList(), +) diff --git a/app/src/main/java/woowacourse/shopping/data/model/CartProduct.kt b/app/src/main/java/woowacourse/shopping/data/model/CartProduct.kt new file mode 100644 index 000000000..efd82625c --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/model/CartProduct.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.data.model + +typealias DataCartProduct = CartProduct + +data class CartProduct( + val id: Int, + val product: DataProduct, + val selectedCount: DataProductCount = DataProductCount(0), + val isChecked: Int, +) diff --git a/app/src/main/java/woowacourse/shopping/data/model/PageNumber.kt b/app/src/main/java/woowacourse/shopping/data/model/Page.kt similarity index 56% rename from app/src/main/java/woowacourse/shopping/data/model/PageNumber.kt rename to app/src/main/java/woowacourse/shopping/data/model/Page.kt index 52fdff57d..a1e44012d 100644 --- a/app/src/main/java/woowacourse/shopping/data/model/PageNumber.kt +++ b/app/src/main/java/woowacourse/shopping/data/model/Page.kt @@ -1,8 +1,8 @@ package woowacourse.shopping.data.model -typealias DataPageNumber = PageNumber +typealias DataPage = Page -data class PageNumber(val value: Int, val sizePerPage: Int) { +class Page(val value: Int, val sizePerPage: Int) { val start = value * sizePerPage - sizePerPage val end = value * sizePerPage + 1 } diff --git a/app/src/main/java/woowacourse/shopping/data/model/ProductCount.kt b/app/src/main/java/woowacourse/shopping/data/model/ProductCount.kt new file mode 100644 index 000000000..82e627b3a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/model/ProductCount.kt @@ -0,0 +1,5 @@ +package woowacourse.shopping.data.model + +typealias DataProductCount = ProductCount + +class ProductCount(val value: Int) diff --git a/app/src/main/java/woowacourse/shopping/data/model/RecentProduct.kt b/app/src/main/java/woowacourse/shopping/data/model/RecentProduct.kt index 1c96e00b8..1468c7688 100644 --- a/app/src/main/java/woowacourse/shopping/data/model/RecentProduct.kt +++ b/app/src/main/java/woowacourse/shopping/data/model/RecentProduct.kt @@ -4,5 +4,5 @@ typealias DataRecentProduct = RecentProduct data class RecentProduct( val id: Int, - val product: DataProduct, + val product: Product, ) diff --git a/app/src/main/java/woowacourse/shopping/data/repository/BasketRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/BasketRepository.kt deleted file mode 100644 index 70797aca1..000000000 --- a/app/src/main/java/woowacourse/shopping/data/repository/BasketRepository.kt +++ /dev/null @@ -1,22 +0,0 @@ -package woowacourse.shopping.data.repository - -import woowacourse.shopping.data.datasource.basket.BasketDataSource -import woowacourse.shopping.data.mapper.toData -import woowacourse.shopping.data.mapper.toDomain -import woowacourse.shopping.domain.PageNumber -import woowacourse.shopping.domain.Product -import woowacourse.shopping.domain.repository.DomainBasketRepository - -class BasketRepository(private val localBasketDataSource: BasketDataSource.Local) : - DomainBasketRepository { - override fun getPartially(page: PageNumber): List = - localBasketDataSource.getPartially(page.toData()).map { it.toDomain() } - - override fun add(product: Product) { - localBasketDataSource.add(product.toData()) - } - - override fun remove(product: Product) { - localBasketDataSource.remove(product.toData()) - } -} diff --git a/app/src/main/java/woowacourse/shopping/data/repository/CartRepositoryImpl.kt b/app/src/main/java/woowacourse/shopping/data/repository/CartRepositoryImpl.kt new file mode 100644 index 000000000..11c9edec4 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/repository/CartRepositoryImpl.kt @@ -0,0 +1,46 @@ +package woowacourse.shopping.data.repository + +import woowacourse.shopping.data.datasource.cart.CartDataSource +import woowacourse.shopping.data.mapper.toData +import woowacourse.shopping.data.mapper.toDomain +import woowacourse.shopping.domain.model.Cart +import woowacourse.shopping.domain.model.CartEntity +import woowacourse.shopping.domain.model.CartProduct +import woowacourse.shopping.domain.model.Product +import woowacourse.shopping.domain.model.page.Page +import woowacourse.shopping.domain.repository.CartRepository + +class CartRepositoryImpl(private val localCartDataSource: CartDataSource.Local) : + CartRepository { + override fun getAllCartEntities(): List = + localCartDataSource.getAllCartEntity().map { it.toDomain() } + + override fun getCartEntity(productId: Int): CartEntity = + localCartDataSource.getCartEntity(productId).toDomain() + + override fun increaseCartCount(product: Product, count: Int) { + localCartDataSource.increaseCartCount(product.toData(), count) + } + + override fun update(cartProducts: List) { + localCartDataSource.update(cartProducts.toData()) + } + + override fun getCheckedProductCount(): Int = + localCartDataSource.getCheckedProductCount() + + override fun removeCheckedProducts() { + localCartDataSource.removeCheckedProducts() + } + + override fun decreaseCartCount(product: Product, count: Int) { + localCartDataSource.decreaseCartCount(product.toData(), count) + } + + override fun deleteByProductId(productId: Int) { + localCartDataSource.deleteByProductId(productId) + } + + override fun getProductInCartSize(): Int = + localCartDataSource.getProductInCartSize() +} diff --git a/app/src/main/java/woowacourse/shopping/data/repository/ProductRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/ProductRepository.kt deleted file mode 100644 index f3962a3f4..000000000 --- a/app/src/main/java/woowacourse/shopping/data/repository/ProductRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package woowacourse.shopping.data.repository - -import woowacourse.shopping.data.datasource.product.ProductDataSource -import woowacourse.shopping.data.mapper.toDomain -import woowacourse.shopping.domain.Product -import woowacourse.shopping.domain.repository.DomainProductRepository - -class ProductRepository( - private val localProductDataSource: ProductDataSource.Local, -) : DomainProductRepository { - override fun getPartially(size: Int, startId: Int): List = - localProductDataSource.getPartially(size, startId).map { it.toDomain() } -} diff --git a/app/src/main/java/woowacourse/shopping/data/repository/ProductRepositoryImpl.kt b/app/src/main/java/woowacourse/shopping/data/repository/ProductRepositoryImpl.kt new file mode 100644 index 000000000..dbc351d0f --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/repository/ProductRepositoryImpl.kt @@ -0,0 +1,18 @@ +package woowacourse.shopping.data.repository + +import woowacourse.shopping.data.datasource.product.ProductDataSource +import woowacourse.shopping.data.mapper.toData +import woowacourse.shopping.data.mapper.toDomain +import woowacourse.shopping.domain.model.Product +import woowacourse.shopping.domain.model.page.Page +import woowacourse.shopping.domain.repository.ProductRepository + +class ProductRepositoryImpl( + private val remoteProductDataSource: ProductDataSource.Remote, +) : ProductRepository { + override fun getProductByPage(page: Page): List = + remoteProductDataSource.getProductByPage(page.toData()).map { it.toDomain() } + + override fun findProductById(id: Int): Product? = + remoteProductDataSource.findProductById(id)?.toDomain() +} diff --git a/app/src/main/java/woowacourse/shopping/data/repository/RecentProductRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/RecentProductRepository.kt deleted file mode 100644 index 74f316701..000000000 --- a/app/src/main/java/woowacourse/shopping/data/repository/RecentProductRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package woowacourse.shopping.data.repository - -import woowacourse.shopping.data.datasource.recentproduct.RecentProductDataSource -import woowacourse.shopping.data.mapper.toData -import woowacourse.shopping.data.mapper.toDomain -import woowacourse.shopping.domain.RecentProduct -import woowacourse.shopping.domain.repository.DomainRecentProductRepository - -class RecentProductRepository(private val localRecentProductDataSource: RecentProductDataSource.Local) : - DomainRecentProductRepository { - override fun add(recentProduct: RecentProduct) { - localRecentProductDataSource.add(recentProduct.toData()) - } - - override fun getPartially(size: Int): List = - localRecentProductDataSource.getPartially(size).map { it.toDomain() } -} diff --git a/app/src/main/java/woowacourse/shopping/data/repository/RecentProductRepositoryImpl.kt b/app/src/main/java/woowacourse/shopping/data/repository/RecentProductRepositoryImpl.kt new file mode 100644 index 000000000..4cb88e55e --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/repository/RecentProductRepositoryImpl.kt @@ -0,0 +1,20 @@ +package woowacourse.shopping.data.repository + +import woowacourse.shopping.data.datasource.recentproduct.RecentProductDataSource +import woowacourse.shopping.data.mapper.toData +import woowacourse.shopping.data.mapper.toDomain +import woowacourse.shopping.domain.model.RecentProduct +import woowacourse.shopping.domain.model.RecentProducts +import woowacourse.shopping.domain.repository.RecentProductRepository + +class RecentProductRepositoryImpl( + private val localRecentProductDataSource: RecentProductDataSource.Local, +) : RecentProductRepository { + + override fun add(recentProduct: RecentProduct) { + localRecentProductDataSource.add(recentProduct.toData()) + } + + override fun getPartially(size: Int): RecentProducts = + RecentProducts(localRecentProductDataSource.getPartially(size).toDomain()) +} diff --git a/app/src/main/java/woowacourse/shopping/mapper/BasketMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/BasketMapper.kt new file mode 100644 index 000000000..7301048e6 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/mapper/BasketMapper.kt @@ -0,0 +1,12 @@ +package woowacourse.shopping.mapper + +import woowacourse.shopping.domain.model.DomainCart +import woowacourse.shopping.model.UiCart + +fun UiCart.toDomain(): DomainCart = DomainCart( + items = cartProducts.map { it.toDomain() }, +) + +fun DomainCart.toUi(): UiCart = UiCart( + cartProducts = items.map { it.toUi() }, +) diff --git a/app/src/main/java/woowacourse/shopping/mapper/BasketProductMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/BasketProductMapper.kt new file mode 100644 index 000000000..a67a1ef0b --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/mapper/BasketProductMapper.kt @@ -0,0 +1,21 @@ +package woowacourse.shopping.mapper + +import woowacourse.shopping.domain.model.DomainCartProduct +import woowacourse.shopping.model.UiCartProduct + +fun UiCartProduct.toDomain(): DomainCartProduct = DomainCartProduct( + id = id, + product = product.toDomain(), + selectedCount = selectedCount.toDomain(), + isChecked = isChecked, +) + +fun DomainCartProduct.toUi(): UiCartProduct = UiCartProduct( + id = id, + product = product.toUi(), + selectedCount = selectedCount.toUi(), + isChecked = isChecked, +) + +fun List.toUi(): List = + map { cartProduct -> cartProduct.toUi() } diff --git a/app/src/main/java/woowacourse/shopping/mapper/PageMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/PageMapper.kt new file mode 100644 index 000000000..42a868b2a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/mapper/PageMapper.kt @@ -0,0 +1,6 @@ +package woowacourse.shopping.mapper + +import woowacourse.shopping.domain.model.page.DomainPage +import woowacourse.shopping.model.UiPage + +fun DomainPage.toUi(): UiPage = UiPage(value = value) diff --git a/app/src/main/java/woowacourse/shopping/mapper/PageNumberMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/PageNumberMapper.kt deleted file mode 100644 index 0f6fbde5f..000000000 --- a/app/src/main/java/woowacourse/shopping/mapper/PageNumberMapper.kt +++ /dev/null @@ -1,10 +0,0 @@ -package woowacourse.shopping.mapper - -import woowacourse.shopping.domain.DomainPageNumber -import woowacourse.shopping.model.UiPageNumber - -fun UiPageNumber.toDomain(sizePerPage: Int): DomainPageNumber = - DomainPageNumber(value = value, sizePerPage = sizePerPage) - -fun DomainPageNumber.toUi(): UiPageNumber = - UiPageNumber(value = value) diff --git a/app/src/main/java/woowacourse/shopping/mapper/PriceMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/PriceMapper.kt index 6a9417c3f..dcb3a9691 100644 --- a/app/src/main/java/woowacourse/shopping/mapper/PriceMapper.kt +++ b/app/src/main/java/woowacourse/shopping/mapper/PriceMapper.kt @@ -1,6 +1,6 @@ package woowacourse.shopping.mapper -import woowacourse.shopping.domain.Price +import woowacourse.shopping.domain.model.Price import woowacourse.shopping.model.UiPrice fun UiPrice.toDomain(): Price = diff --git a/app/src/main/java/woowacourse/shopping/mapper/ProductCountMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/ProductCountMapper.kt new file mode 100644 index 000000000..70dd8a8a3 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/mapper/ProductCountMapper.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.mapper + +import woowacourse.shopping.domain.model.ProductCount +import woowacourse.shopping.model.UiProductCount + +fun UiProductCount.toDomain(): ProductCount = + ProductCount(value) + +fun ProductCount.toUi(): UiProductCount = + UiProductCount(value) diff --git a/app/src/main/java/woowacourse/shopping/mapper/ProductMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/ProductMapper.kt index 76b5c7d6f..c2cc996c4 100644 --- a/app/src/main/java/woowacourse/shopping/mapper/ProductMapper.kt +++ b/app/src/main/java/woowacourse/shopping/mapper/ProductMapper.kt @@ -1,10 +1,20 @@ package woowacourse.shopping.mapper -import woowacourse.shopping.domain.Product +import woowacourse.shopping.domain.model.Product import woowacourse.shopping.model.UiProduct fun UiProduct.toDomain(): Product = - Product(id = id, name = name, price = price.toDomain(), imageUrl = imageUrl) + Product( + id = id, + name = name, + price = price.toDomain(), + imageUrl = imageUrl, + ) fun Product.toUi(): UiProduct = - UiProduct(id = id, name = name, price = price.toUi(), imageUrl = imageUrl) + UiProduct( + id = id, + name = name, + price = price.toUi(), + imageUrl = imageUrl, + ) diff --git a/app/src/main/java/woowacourse/shopping/mapper/ProductsMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/ProductsMapper.kt deleted file mode 100644 index bd7455f4c..000000000 --- a/app/src/main/java/woowacourse/shopping/mapper/ProductsMapper.kt +++ /dev/null @@ -1,13 +0,0 @@ -package woowacourse.shopping.mapper - -import woowacourse.shopping.domain.DomainProducts -import woowacourse.shopping.model.UiProducts - -fun UiProducts.toDomain(loadUnit: Int): DomainProducts = DomainProducts( - items = getItems().map { it.toDomain() }, - loadUnit = loadUnit, -) - -fun DomainProducts.toUi(): UiProducts = UiProducts( - items = getItems().map { it.toUi() }, -) diff --git a/app/src/main/java/woowacourse/shopping/mapper/RecentProductMapper.kt b/app/src/main/java/woowacourse/shopping/mapper/RecentProductMapper.kt index 554b9d5ca..9d7e0df70 100644 --- a/app/src/main/java/woowacourse/shopping/mapper/RecentProductMapper.kt +++ b/app/src/main/java/woowacourse/shopping/mapper/RecentProductMapper.kt @@ -1,6 +1,6 @@ package woowacourse.shopping.mapper -import woowacourse.shopping.domain.RecentProduct +import woowacourse.shopping.domain.model.RecentProduct import woowacourse.shopping.model.UiRecentProduct fun UiRecentProduct.toDomain(): RecentProduct = @@ -8,3 +8,6 @@ fun UiRecentProduct.toDomain(): RecentProduct = fun RecentProduct.toUi(): UiRecentProduct = UiRecentProduct(id = id, product = product.toUi()) + +fun List.toUi(): List = + map { recentProduct -> recentProduct.toUi() } diff --git a/app/src/main/java/woowacourse/shopping/model/Cart.kt b/app/src/main/java/woowacourse/shopping/model/Cart.kt new file mode 100644 index 000000000..6cc9f9228 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/Cart.kt @@ -0,0 +1,7 @@ +package woowacourse.shopping.model + +typealias UiCart = Cart + +class Cart( + val cartProducts: List = emptyList(), +) diff --git a/app/src/main/java/woowacourse/shopping/model/CartProduct.kt b/app/src/main/java/woowacourse/shopping/model/CartProduct.kt new file mode 100644 index 000000000..22cc3f43d --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/CartProduct.kt @@ -0,0 +1,13 @@ +package woowacourse.shopping.model + +typealias UiCartProduct = CartProduct + +data class CartProduct( + val id: Int, + val product: UiProduct, + val selectedCount: UiProductCount = UiProductCount(0), + val isChecked: Boolean, +) { + val shouldShowCounter: Boolean + get() = selectedCount.value > 0 +} diff --git a/app/src/main/java/woowacourse/shopping/model/PageNumber.kt b/app/src/main/java/woowacourse/shopping/model/Page.kt similarity index 52% rename from app/src/main/java/woowacourse/shopping/model/PageNumber.kt rename to app/src/main/java/woowacourse/shopping/model/Page.kt index 193783a7f..05236561c 100644 --- a/app/src/main/java/woowacourse/shopping/model/PageNumber.kt +++ b/app/src/main/java/woowacourse/shopping/model/Page.kt @@ -1,7 +1,7 @@ package woowacourse.shopping.model -typealias UiPageNumber = PageNumber +typealias UiPage = Page -data class PageNumber(val value: Int) { +data class Page(val value: Int) { fun toText(): String = value.toString() } diff --git a/app/src/main/java/woowacourse/shopping/model/PageMapper.kt b/app/src/main/java/woowacourse/shopping/model/PageMapper.kt deleted file mode 100644 index 955d1af5a..000000000 --- a/app/src/main/java/woowacourse/shopping/model/PageMapper.kt +++ /dev/null @@ -1,7 +0,0 @@ -package woowacourse.shopping.model - -import woowacourse.shopping.domain.DomainPageNumber - -fun UiPageNumber.toDomain(): DomainPageNumber = DomainPageNumber(value = value) - -fun DomainPageNumber.toUi(): UiPageNumber = UiPageNumber(value = value) diff --git a/app/src/main/java/woowacourse/shopping/model/Price.kt b/app/src/main/java/woowacourse/shopping/model/Price.kt index 95bcd906d..8f8b07027 100644 --- a/app/src/main/java/woowacourse/shopping/model/Price.kt +++ b/app/src/main/java/woowacourse/shopping/model/Price.kt @@ -1,7 +1,7 @@ package woowacourse.shopping.model import android.os.Parcelable -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize typealias UiPrice = Price diff --git a/app/src/main/java/woowacourse/shopping/model/ProductCount.kt b/app/src/main/java/woowacourse/shopping/model/ProductCount.kt new file mode 100644 index 000000000..2baf24d64 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/ProductCount.kt @@ -0,0 +1,17 @@ +package woowacourse.shopping.model + +import android.os.Parcelable +import android.view.View.GONE +import android.view.View.VISIBLE +import kotlinx.parcelize.Parcelize + +typealias UiProductCount = ProductCount + +@Parcelize +data class ProductCount(val value: Int) : Parcelable { + fun toText(): String = + if (value > 99) "99" else value.toString() + + fun getVisibility(): Int = + if (value == 0) GONE else VISIBLE +} diff --git a/app/src/main/java/woowacourse/shopping/model/Products.kt b/app/src/main/java/woowacourse/shopping/model/Products.kt deleted file mode 100644 index 9fbd6ef83..000000000 --- a/app/src/main/java/woowacourse/shopping/model/Products.kt +++ /dev/null @@ -1,13 +0,0 @@ -package woowacourse.shopping.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -typealias UiProducts = Products - -@Parcelize -class Products( - private val items: List = emptyList(), -) : Parcelable { - fun getItems(): List = items -} diff --git a/app/src/main/java/woowacourse/shopping/server/Mock.kt b/app/src/main/java/woowacourse/shopping/server/Mock.kt new file mode 100644 index 000000000..b4cba17dc --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/server/Mock.kt @@ -0,0 +1,22 @@ +package woowacourse.shopping.server + +fun getProducts(startId: Int, offset: Int): String = List(offset) { id -> + """ + { + "id": ${startId + id}, + "name": "상품${startId + id}", + "imageUrl": "https://mediahub.seoul.go.kr/uploads/2016/09/952e8925ec41cc06e6164d695d776e51.jpg", + "price": 1000 + } + """ +}.joinToString(",", prefix = "[", postfix = "]").trimIndent() + + +fun getProductById(productId: Int): String = """ + { + "id": ${productId}, + "name": "상품${productId}", + "imageUrl": "https://mediahub.seoul.go.kr/uploads/2016/09/952e8925ec41cc06e6164d695d776e51.jpg", + "price": 1000 + } +""".trimIndent() diff --git a/app/src/main/java/woowacourse/shopping/server/ServerContract.kt b/app/src/main/java/woowacourse/shopping/server/ServerContract.kt new file mode 100644 index 000000000..b607396dd --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/server/ServerContract.kt @@ -0,0 +1,6 @@ +package woowacourse.shopping.server + +internal const val PORT = "8080" +internal const val BASE_URL = "http://localhost:$PORT" + +internal const val GET = "GET" diff --git a/app/src/main/java/woowacourse/shopping/server/ShoppingMockWebServer.kt b/app/src/main/java/woowacourse/shopping/server/ShoppingMockWebServer.kt new file mode 100644 index 000000000..6a7cccf9c --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/server/ShoppingMockWebServer.kt @@ -0,0 +1,55 @@ +package woowacourse.shopping.server + +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import woowacourse.shopping.util.extension.parseQueryString + +class ShoppingMockWebServer : Thread() { + private val mockWebServer: MockWebServer = MockWebServer() + private lateinit var _baseUrl: String + val baseUrl: String get() = _baseUrl + + override fun run() { + super.run() + mockWebServer.url("/") + mockWebServer.dispatcher = getDispatcher() + _baseUrl = "http://localhost:${mockWebServer.port}" + } + + private fun getDispatcher(): Dispatcher = object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + return when (request.method) { + GET -> { + val path = request.path ?: return MockResponse().setResponseCode(404) + return processGet(path) + } + + else -> MockResponse().setResponseCode(404) + } + } + } + + private fun processGet(path: String): MockResponse = when { + path.startsWith("/products") && path.contains("productId") -> { + val productId = path.parseQueryString()["productId"]?.toInt() ?: 1 + MockResponse() + .setHeader("Content-Type", "application/json") + .setResponseCode(200) + .setBody(getProductById(productId)) + } + + path.startsWith("/products") -> { + val queryStrings = path.parseQueryString() + val startId = queryStrings["start"]?.toInt() ?: 1 + val offset = queryStrings["count"]?.toInt() ?: 20 + MockResponse() + .setHeader("Content-Type", "application/json") + .setResponseCode(200) + .setBody(getProducts(startId, offset)) + } + + else -> MockResponse().setResponseCode(404) + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/basket/BasketActivity.kt b/app/src/main/java/woowacourse/shopping/ui/basket/BasketActivity.kt deleted file mode 100644 index b0edd44b9..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/basket/BasketActivity.kt +++ /dev/null @@ -1,56 +0,0 @@ -package woowacourse.shopping.ui.basket - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.databinding.DataBindingUtil -import woowacourse.shopping.R -import woowacourse.shopping.data.database.ShoppingDatabase -import woowacourse.shopping.databinding.ActivityBasketBinding -import woowacourse.shopping.model.UiPageNumber -import woowacourse.shopping.model.UiProduct -import woowacourse.shopping.ui.basket.BasketContract.Presenter -import woowacourse.shopping.ui.basket.BasketContract.View -import woowacourse.shopping.ui.basket.recyclerview.adapter.BasketAdapter -import woowacourse.shopping.util.factory.createBasketPresenter - -class BasketActivity : AppCompatActivity(), View { - private val shoppingDatabase by lazy { ShoppingDatabase(this) } - override val presenter: Presenter by lazy { createBasketPresenter(this, shoppingDatabase) } - private lateinit var binding: ActivityBasketBinding - - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_basket) - binding.presenter = presenter - binding.adapter = BasketAdapter(presenter::removeBasketProduct) - } - - override fun updateBasket(products: List) { - binding.adapter?.submitList(products) - } - - override fun updateNavigatorEnabled(previous: Boolean, next: Boolean) { - binding.previousButton.isEnabled = previous - binding.nextButton.isEnabled = next - } - - override fun updatePageNumber(page: UiPageNumber) { - binding.pageNumberTextView.text = page.toText() - } - - override fun closeScreen() { - finish() - } - - override fun onDestroy() { - super.onDestroy() - shoppingDatabase.close() - } - - companion object { - fun getIntent(context: Context) = Intent(context, BasketActivity::class.java) - } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/basket/BasketContract.kt b/app/src/main/java/woowacourse/shopping/ui/basket/BasketContract.kt deleted file mode 100644 index a1053abcb..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/basket/BasketContract.kt +++ /dev/null @@ -1,23 +0,0 @@ -package woowacourse.shopping.ui.basket - -import woowacourse.shopping.model.PageNumber -import woowacourse.shopping.model.UiProduct - -interface BasketContract { - interface View { - val presenter: Presenter - - fun updateBasket(products: List) - fun updateNavigatorEnabled(previous: Boolean, next: Boolean) - fun closeScreen() - fun updatePageNumber(page: PageNumber) - } - - abstract class Presenter(protected val view: View) { - abstract fun fetchBasket() - abstract fun fetchPrevious() - abstract fun fetchNext() - abstract fun removeBasketProduct(product: UiProduct) - abstract fun closeScreen() - } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/basket/BasketPresenter.kt b/app/src/main/java/woowacourse/shopping/ui/basket/BasketPresenter.kt deleted file mode 100644 index 3f70514b8..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/basket/BasketPresenter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package woowacourse.shopping.ui.basket - -import woowacourse.shopping.domain.PageNumber -import woowacourse.shopping.domain.Products -import woowacourse.shopping.domain.repository.BasketRepository -import woowacourse.shopping.mapper.toDomain -import woowacourse.shopping.mapper.toUi -import woowacourse.shopping.model.UiProduct -import woowacourse.shopping.ui.basket.BasketContract.Presenter -import woowacourse.shopping.ui.basket.BasketContract.View - -class BasketPresenter( - view: View, - private val basketRepository: BasketRepository, - private var products: Products = Products(loadUnit = BASKET_PAGING_SIZE), - private var currentPage: PageNumber = PageNumber(), -) : Presenter(view) { - - override fun fetchBasket() { - val currentProducts = basketRepository.getPartially(currentPage) - products = products.copy(currentProducts) - - view.updateBasket(products.getItemsByUnit().map { it.toUi() }) - view.updateNavigatorEnabled(currentPage.hasPrevious(), products.canLoadMore()) - view.updatePageNumber(currentPage.toUi()) - } - - override fun fetchNext() { - currentPage++ - fetchBasket() - } - - override fun fetchPrevious() { - currentPage-- - fetchBasket() - } - - override fun removeBasketProduct(product: UiProduct) { - basketRepository.remove(product.toDomain()) - fetchBasket() - } - - override fun closeScreen() { - view.closeScreen() - } - - companion object { - private const val BASKET_PAGING_SIZE = 5 - } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/basket/recyclerview/adapter/BasketAdapter.kt b/app/src/main/java/woowacourse/shopping/ui/basket/recyclerview/adapter/BasketAdapter.kt deleted file mode 100644 index c0abd40aa..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/basket/recyclerview/adapter/BasketAdapter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package woowacourse.shopping.ui.basket.recyclerview.adapter - -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import woowacourse.shopping.model.UiProduct - -class BasketAdapter(private val onDeleteClick: (UiProduct) -> Unit) : - ListAdapter(basketDiffUtil) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasketViewHolder = - BasketViewHolder(parent) { pos -> onDeleteClick(currentList[pos]) } - - - override fun onBindViewHolder(holder: BasketViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - companion object { - private val basketDiffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: UiProduct, newItem: UiProduct): Boolean = - oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: UiProduct, newItem: UiProduct): Boolean = - oldItem == newItem - } - } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/basket/recyclerview/adapter/BasketViewHolder.kt b/app/src/main/java/woowacourse/shopping/ui/basket/recyclerview/adapter/BasketViewHolder.kt deleted file mode 100644 index 3af1a28a6..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/basket/recyclerview/adapter/BasketViewHolder.kt +++ /dev/null @@ -1,24 +0,0 @@ -package woowacourse.shopping.ui.basket.recyclerview.adapter - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import woowacourse.shopping.R -import woowacourse.shopping.databinding.ItemBasketBinding -import woowacourse.shopping.model.UiProduct - -class BasketViewHolder(parent: ViewGroup, onItemClick: (Int) -> Unit) : ViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.item_basket, parent, false) -) { - private val binding = ItemBasketBinding.bind(itemView) - - init { - binding.closeButton.setOnClickListener { - onItemClick(bindingAdapterPosition) - } - } - - fun bind(item: UiProduct) { - binding.product = item - } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt new file mode 100644 index 000000000..97f7eafe6 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt @@ -0,0 +1,78 @@ +package woowacourse.shopping.ui.cart + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import woowacourse.shopping.R +import woowacourse.shopping.databinding.ActivityCartBinding +import woowacourse.shopping.model.UiCartProduct +import woowacourse.shopping.model.UiPage +import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.ui.cart.CartContract.View +import woowacourse.shopping.ui.cart.listener.CartClickListener +import woowacourse.shopping.ui.cart.recyclerview.adapter.CartAdapter +import woowacourse.shopping.util.extension.setContentView +import woowacourse.shopping.util.extension.showToast +import woowacourse.shopping.util.inject.inject + +class CartActivity : AppCompatActivity(), View, CartClickListener { + private val presenter: CartPresenter by lazy { inject(this, this) } + private lateinit var binding: ActivityCartBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityCartBinding.inflate(layoutInflater).setContentView(this) + binding.lifecycleOwner = this + binding.presenter = presenter + binding.adapter = CartAdapter(this) + presenter.fetchCart(1) + } + + override fun updateCart(cartProducts: List) { + binding.adapter?.submitList(cartProducts) + } + + override fun updateNavigatorEnabled(previousEnabled: Boolean, nextEnabled: Boolean) { + binding.previousButton.isEnabled = previousEnabled + binding.nextButton.isEnabled = nextEnabled + } + + override fun updatePageNumber(page: UiPage) { + binding.pageNumberTextView.text = page.toText() + } + + override fun updateTotalPrice(totalPrice: Int) { + binding.totalPriceTextView.text = getString(R.string.price_format, totalPrice) + } + + override fun onCountChanged(product: UiProduct, count: Int, isIncreased: Boolean) { + presenter.changeProductCount(product, count, isIncreased) + } + + override fun onCheckStateChanged(product: UiProduct, isChecked: Boolean) { + presenter.changeProductSelectState(product, isChecked) + } + + override fun onDeleteClick(product: UiProduct) { + presenter.removeProduct(product) + } + + override fun showOrderComplete(productCount: Int) { + showToast(getString(R.string.order_success_message, productCount)) + navigateToHome() + } + + override fun showOrderFailed() { + showToast(getString(R.string.order_failed_message)) + } + + override fun navigateToHome() { + setResult(RESULT_OK) + finish() + } + + companion object { + fun getIntent(context: Context) = Intent(context, CartActivity::class.java) + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartContract.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartContract.kt new file mode 100644 index 000000000..0952e4b5e --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartContract.kt @@ -0,0 +1,27 @@ +package woowacourse.shopping.ui.cart + +import woowacourse.shopping.model.Page +import woowacourse.shopping.model.UiCartProduct +import woowacourse.shopping.model.UiProduct + +interface CartContract { + interface View { + fun updateCart(cartProducts: List) + fun updateNavigatorEnabled(previousEnabled: Boolean, nextEnabled: Boolean) + fun updatePageNumber(page: Page) + fun updateTotalPrice(totalPrice: Int) + fun showOrderComplete(productCount: Int) + fun showOrderFailed() + fun navigateToHome() + } + + abstract class Presenter(protected val view: View) { + abstract fun fetchCart(page: Int) + abstract fun changeProductCount(product: UiProduct, count: Int, increase: Boolean) + abstract fun changeProductSelectState(product: UiProduct, isSelect: Boolean) + abstract fun toggleAllCheckState() + abstract fun removeProduct(product: UiProduct) + abstract fun order() + abstract fun navigateToHome() + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartPresenter.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartPresenter.kt new file mode 100644 index 000000000..49b6f1ca0 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartPresenter.kt @@ -0,0 +1,106 @@ +package woowacourse.shopping.ui.cart + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations +import woowacourse.shopping.domain.model.Cart +import woowacourse.shopping.domain.model.CartProduct +import woowacourse.shopping.domain.model.ProductCount +import woowacourse.shopping.domain.model.page.Page +import woowacourse.shopping.domain.model.page.Pagination +import woowacourse.shopping.domain.repository.CartRepository +import woowacourse.shopping.domain.repository.ProductRepository +import woowacourse.shopping.mapper.toDomain +import woowacourse.shopping.mapper.toUi +import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.ui.cart.CartContract.Presenter +import woowacourse.shopping.ui.cart.CartContract.View + +class CartPresenter( + view: View, + private val productRepository: ProductRepository, + private val cartRepository: CartRepository, + cartSize: Int = 5, +) : Presenter(view) { + private var cart: Cart = Cart(minProductSize = 1) + private var currentPage: Page = Pagination(sizePerPage = cartSize) + + private val _totalCheckSize = MutableLiveData(cartRepository.getCheckedProductCount()) + val totalCheckSize: LiveData get() = _totalCheckSize + + private val _pageCheckSize = MutableLiveData(currentPage.getCheckedProductSize(cart)) + val isAllChecked: LiveData = Transformations.map(_pageCheckSize) { pageCheckSize -> + pageCheckSize == currentPage.takeItems(cart).size + } + + override fun fetchCart(page: Int) { + currentPage = currentPage.update(page) + cart = cart.update(loadCartProducts()) + + view.updateNavigatorEnabled(currentPage.hasPrevious(), currentPage.hasNext(cart)) + view.updatePageNumber(currentPage.toUi()) + fetchView() + } + + private fun loadCartProducts(): List = + cartRepository.getAllCartEntities().mapNotNull { + val product = productRepository.findProductById(it.productId) + product?.run { CartProduct(it.id, this, ProductCount(it.count), it.checked) } + } + + override fun changeProductCount(product: UiProduct, count: Int, increase: Boolean) { + updateCart(changeCount(product, count, increase)) + } + + private fun changeCount(product: UiProduct, count: Int, isInc: Boolean): Cart = when (isInc) { + true -> cart.increaseProductCount(product.toDomain(), count) + false -> cart.decreaseProductCount(product.toDomain(), count) + } + + override fun changeProductSelectState(product: UiProduct, isSelect: Boolean) { + updateCart(changeSelectState(product, isSelect)) + } + + private fun changeSelectState(product: UiProduct, isSelect: Boolean): Cart = + if (isSelect) cart.select(product.toDomain()) else cart.unselect(product.toDomain()) + + override fun toggleAllCheckState() { + updateCart( + if (isAllChecked.value == true) { + cart.unselectAll(currentPage) + } else cart.selectAll( + currentPage + ) + ) + } + + override fun removeProduct(product: UiProduct) { + cartRepository.deleteByProductId(product.id) + fetchCart(currentPage.value) + } + + override fun order() { + if (_totalCheckSize.value == 0) { + view.showOrderFailed(); return + } + cartRepository.removeCheckedProducts() + view.showOrderComplete(_totalCheckSize.value ?: 0) + } + + override fun navigateToHome() { + view.navigateToHome() + } + + private fun updateCart(newCart: Cart) { + cart = cart.update(newCart) + cartRepository.update(currentPage.takeItems(cart)) + fetchView() + } + + private fun fetchView() { + _totalCheckSize.value = cartRepository.getCheckedProductCount() + _pageCheckSize.value = currentPage.getCheckedProductSize(cart) + view.updateTotalPrice(cart.getCheckedProductTotalPrice()) + view.updateCart(currentPage.takeItems(cart).toUi()) + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/listener/CartClickListener.kt b/app/src/main/java/woowacourse/shopping/ui/cart/listener/CartClickListener.kt new file mode 100644 index 000000000..7847bc1af --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/cart/listener/CartClickListener.kt @@ -0,0 +1,9 @@ +package woowacourse.shopping.ui.cart.listener + +import woowacourse.shopping.model.UiProduct + +interface CartClickListener { + fun onCountChanged(product: UiProduct, count: Int, isIncreased: Boolean) + fun onCheckStateChanged(product: UiProduct, isChecked: Boolean) + fun onDeleteClick(product: UiProduct) +} diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/recyclerview/adapter/CartAdapter.kt b/app/src/main/java/woowacourse/shopping/ui/cart/recyclerview/adapter/CartAdapter.kt new file mode 100644 index 000000000..46c241b88 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/cart/recyclerview/adapter/CartAdapter.kt @@ -0,0 +1,18 @@ +package woowacourse.shopping.ui.cart.recyclerview.adapter + +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import woowacourse.shopping.model.UiCartProduct +import woowacourse.shopping.ui.cart.listener.CartClickListener +import woowacourse.shopping.util.diffutil.CartDiffUtil + +class CartAdapter( + private val cartClickListener: CartClickListener, +) : ListAdapter(CartDiffUtil) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartViewHolder = + CartViewHolder(parent, cartClickListener) + + override fun onBindViewHolder(holder: CartViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/recyclerview/adapter/CartViewHolder.kt b/app/src/main/java/woowacourse/shopping/ui/cart/recyclerview/adapter/CartViewHolder.kt new file mode 100644 index 000000000..c82361fe1 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/cart/recyclerview/adapter/CartViewHolder.kt @@ -0,0 +1,26 @@ +package woowacourse.shopping.ui.cart.recyclerview.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import woowacourse.shopping.R +import woowacourse.shopping.databinding.ItemCartBinding +import woowacourse.shopping.model.UiCartProduct +import woowacourse.shopping.ui.cart.listener.CartClickListener + +class CartViewHolder( + parent: ViewGroup, + cartClickListener: CartClickListener, +) : ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_cart, parent, false) +) { + private val binding = ItemCartBinding.bind(itemView) + + init { + binding.cartClickListener = cartClickListener + } + + fun bind(item: UiCartProduct) { + binding.cartProduct = item + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailActivity.kt b/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailActivity.kt new file mode 100644 index 000000000..529b02088 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailActivity.kt @@ -0,0 +1,79 @@ +package woowacourse.shopping.ui.detail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener +import woowacourse.shopping.R +import woowacourse.shopping.databinding.ActivityProductDetailBinding +import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.model.UiRecentProduct +import woowacourse.shopping.ui.detail.dialog.ProductCounterDialog +import woowacourse.shopping.ui.detail.ProductDetailContract.Presenter +import woowacourse.shopping.ui.detail.ProductDetailContract.View +import woowacourse.shopping.ui.shopping.ShoppingActivity +import woowacourse.shopping.util.extension.getParcelableExtraCompat +import woowacourse.shopping.util.extension.setContentView +import woowacourse.shopping.util.inject.inject + +class ProductDetailActivity : AppCompatActivity(), View, OnMenuItemClickListener { + private lateinit var binding: ActivityProductDetailBinding + private val presenter: Presenter by lazy { + inject( + view = this, + detailProduct = intent.getParcelableExtraCompat(DETAIL_PRODUCT_KEY)!!, + recentProduct = intent.getParcelableExtraCompat(LAST_VIEWED_PRODUCT_KEY), + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityProductDetailBinding.inflate(layoutInflater).setContentView(this) + initView() + } + + private fun initView() { + binding.presenter = presenter + binding.productDetailToolBar.setOnMenuItemClickListener(this) + } + + override fun showProductDetail(product: UiProduct) { + binding.detailProduct = product + } + + override fun showLastViewedProductDetail(lastViewedProduct: UiProduct?) { + binding.lastViewedProduct = lastViewedProduct + } + + override fun showProductCounter(product: UiProduct) { + ProductCounterDialog(this, product, presenter::navigateToHome).show() + } + + override fun navigateToHome(product: UiProduct, count: Int) { + startActivity(ShoppingActivity.getIntent(this, product, count)) + } + + override fun navigateToProductDetail(recentProduct: UiRecentProduct) { + startActivity(getIntent(this, recentProduct.product, null)) + finish() + } + + override fun onMenuItemClick(item: MenuItem): Boolean { + when (item.itemId) { + R.id.close -> finish() + } + return true + } + + companion object { + private const val DETAIL_PRODUCT_KEY = "detail_product_key" + private const val LAST_VIEWED_PRODUCT_KEY = "last_viewed_product_key" + + fun getIntent(context: Context, detail: UiProduct, recent: UiRecentProduct?): Intent = + Intent(context, ProductDetailActivity::class.java) + .putExtra(DETAIL_PRODUCT_KEY, detail) + .putExtra(LAST_VIEWED_PRODUCT_KEY, recent) + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailContract.kt b/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailContract.kt new file mode 100644 index 000000000..5618b14b2 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailContract.kt @@ -0,0 +1,20 @@ +package woowacourse.shopping.ui.detail + +import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.model.UiRecentProduct + +interface ProductDetailContract { + interface View { + fun showProductDetail(product: UiProduct) + fun showLastViewedProductDetail(lastViewedProduct: UiProduct?) + fun showProductCounter(product: UiProduct) + fun navigateToProductDetail(recentProduct: UiRecentProduct) + fun navigateToHome(product: UiProduct, count: Int) + } + + abstract class Presenter(protected val view: View) { + abstract fun inquiryProductCounter() + abstract fun inquiryLastViewedProduct() + abstract fun navigateToHome(count: Int) + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailPresenter.kt b/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailPresenter.kt new file mode 100644 index 000000000..9cac331c0 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/detail/ProductDetailPresenter.kt @@ -0,0 +1,30 @@ +package woowacourse.shopping.ui.detail + +import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.model.UiRecentProduct +import woowacourse.shopping.ui.detail.ProductDetailContract.Presenter +import woowacourse.shopping.ui.detail.ProductDetailContract.View + +class ProductDetailPresenter( + view: View, + private val product: UiProduct, + private val recentProduct: UiRecentProduct?, +) : Presenter(view) { + + init { + view.showProductDetail(product) + view.showLastViewedProductDetail(recentProduct?.product) + } + + override fun inquiryProductCounter() { + view.showProductCounter(product) + } + + override fun inquiryLastViewedProduct() { + recentProduct?.let { view.navigateToProductDetail(it) } + } + + override fun navigateToHome(count: Int) { + view.navigateToHome(product, count) + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/detail/dialog/ProductCounterDialog.kt b/app/src/main/java/woowacourse/shopping/ui/detail/dialog/ProductCounterDialog.kt new file mode 100644 index 000000000..98600dab0 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/detail/dialog/ProductCounterDialog.kt @@ -0,0 +1,33 @@ +package woowacourse.shopping.ui.detail.dialog + +import android.app.Dialog +import android.content.Context +import woowacourse.shopping.R +import woowacourse.shopping.databinding.CounterBinding +import woowacourse.shopping.model.UiProduct + +class ProductCounterDialog( + context: Context, + product: UiProduct, + putInCart: (count: Int) -> Unit, +) : Dialog(context) { + + init { + val binding: CounterBinding = CounterBinding.inflate(layoutInflater) + setContentView(binding.root) + initDialogSize(context) + binding.product = product + binding.onPutInCart = { count -> + putInCart(count) + dismiss() + } + } + + private fun initDialogSize(context: Context) { + val metrics = context.resources.displayMetrics + val width = (metrics.widthPixels * 0.9).toInt() + val height = (width * 0.4).toInt() + window?.setLayout(width, height) + window?.setBackgroundDrawableResource(R.drawable.shape_woowa_round_4_white_rect) + } +} diff --git a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailActivity.kt b/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailActivity.kt deleted file mode 100644 index c4a660895..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailActivity.kt +++ /dev/null @@ -1,67 +0,0 @@ -package woowacourse.shopping.ui.productdetail - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.MenuItem -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener -import androidx.databinding.DataBindingUtil -import woowacourse.shopping.R -import woowacourse.shopping.databinding.ActivityProductDetailBinding -import woowacourse.shopping.model.UiProduct -import woowacourse.shopping.ui.basket.BasketActivity -import woowacourse.shopping.ui.productdetail.ProductDetailContract.Presenter -import woowacourse.shopping.ui.productdetail.ProductDetailContract.View -import woowacourse.shopping.util.extension.getParcelableExtraCompat -import woowacourse.shopping.util.extension.showImage -import woowacourse.shopping.util.factory.createProductDetailPresenter - -class ProductDetailActivity : AppCompatActivity(), View, OnMenuItemClickListener { - private lateinit var binding: ActivityProductDetailBinding - override val presenter: Presenter by lazy { - createProductDetailPresenter(this, this, intent.getParcelableExtraCompat(PRODUCT_KEY)!!) - } - - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_product_detail) - initView() - } - - private fun initView() { - binding.productDetailPresenter = presenter - binding.productDetailToolBar.setOnMenuItemClickListener(this) - } - - override fun showProductImage(imageUrl: String) { - binding.productImageView.showImage(imageUrl) - } - - override fun showProductName(name: String) { - binding.productNameTextView.text = name - } - - override fun showProductPrice(amount: Int) { - binding.productPriceTextView.text = getString(R.string.price_format, amount) - } - - override fun navigateToBasketScreen() { - startActivity(BasketActivity.getIntent(this)) - finish() - } - - override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.close -> finish() - } - return true - } - - companion object { - private const val PRODUCT_KEY = "product_key" - fun getIntent(context: Context, product: UiProduct): Intent = - Intent(context, ProductDetailActivity::class.java).putExtra(PRODUCT_KEY, product) - } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailContract.kt b/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailContract.kt deleted file mode 100644 index 65add3086..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailContract.kt +++ /dev/null @@ -1,16 +0,0 @@ -package woowacourse.shopping.ui.productdetail - -interface ProductDetailContract { - interface View { - val presenter: Presenter - - fun showProductImage(imageUrl: String) - fun navigateToBasketScreen() - fun showProductName(name: String) - fun showProductPrice(amount: Int) - } - - abstract class Presenter(protected val view: View) { - abstract fun addBasketProduct() - } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailPresenter.kt b/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailPresenter.kt deleted file mode 100644 index 0942cb4b8..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailPresenter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package woowacourse.shopping.ui.productdetail - -import woowacourse.shopping.domain.repository.BasketRepository -import woowacourse.shopping.mapper.toDomain -import woowacourse.shopping.model.UiProduct - -class ProductDetailPresenter( - view: ProductDetailContract.View, - private val basketRepository: BasketRepository, - private val product: UiProduct, -) : ProductDetailContract.Presenter(view) { - - init { - view.showProductImage(product.imageUrl) - view.showProductName(product.name) - view.showProductPrice(product.price.value) - } - - override fun addBasketProduct() { - basketRepository.add(product.toDomain()) - view.navigateToBasketScreen() - } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingActivity.kt b/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingActivity.kt index 542899ffc..8933443fe 100644 --- a/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingActivity.kt +++ b/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingActivity.kt @@ -1,58 +1,73 @@ package woowacourse.shopping.ui.shopping +import android.content.Context +import android.content.Intent import android.os.Bundle -import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.ConcatAdapter import woowacourse.shopping.R -import woowacourse.shopping.data.database.ShoppingDatabase import woowacourse.shopping.databinding.ActivityShoppingBinding +import woowacourse.shopping.model.ProductCount +import woowacourse.shopping.model.UiCartProduct import woowacourse.shopping.model.UiProduct import woowacourse.shopping.model.UiRecentProduct -import woowacourse.shopping.ui.basket.BasketActivity -import woowacourse.shopping.ui.productdetail.ProductDetailActivity +import woowacourse.shopping.ui.cart.CartActivity +import woowacourse.shopping.ui.detail.ProductDetailActivity import woowacourse.shopping.ui.shopping.ShoppingContract.Presenter import woowacourse.shopping.ui.shopping.ShoppingContract.View import woowacourse.shopping.ui.shopping.recyclerview.adapter.loadmore.LoadMoreAdapter import woowacourse.shopping.ui.shopping.recyclerview.adapter.product.ProductAdapter import woowacourse.shopping.ui.shopping.recyclerview.adapter.recentproduct.RecentProductAdapter import woowacourse.shopping.ui.shopping.recyclerview.adapter.recentproduct.RecentProductWrapperAdapter -import woowacourse.shopping.util.factory.createShoppingPresenter -import woowacourse.shopping.util.isolatedViewTypeConfig +import woowacourse.shopping.util.builder.add +import woowacourse.shopping.util.builder.isolatedViewTypeConcatAdapter +import woowacourse.shopping.util.extension.findItemActionView +import woowacourse.shopping.util.extension.findTextView +import woowacourse.shopping.util.extension.getParcelableExtraCompat +import woowacourse.shopping.util.extension.setContentView +import woowacourse.shopping.util.inject.inject +import woowacourse.shopping.util.listener.ProductClickListener +import woowacourse.shopping.widget.ProductCounterView.OnClickListener -class ShoppingActivity : AppCompatActivity(), View, OnMenuItemClickListener { +class ShoppingActivity : AppCompatActivity(), View, OnClickListener, ProductClickListener { private lateinit var binding: ActivityShoppingBinding - - private val shoppingDatabase by lazy { ShoppingDatabase(this) } - override val presenter: Presenter by lazy { createShoppingPresenter(this, shoppingDatabase) } + private val presenter: Presenter by lazy { inject(this, this) } private val recentProductAdapter = RecentProductAdapter(presenter::inquiryRecentProductDetail) private val recentProductWrapperAdapter = RecentProductWrapperAdapter(recentProductAdapter) - private val productAdapter = ProductAdapter(presenter::inquiryProductDetail) - private val loadMoreAdapter = LoadMoreAdapter(presenter::fetchProducts) + private val productAdapter = ProductAdapter(this, this) + private val loadMoreAdapter = LoadMoreAdapter(presenter::loadMoreProducts) + + private val cartActivityLauncher = registerForActivityResult(StartActivityForResult()) { + presenter.fetchAll() + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = DataBindingUtil.setContentView(this, R.layout.activity_shopping) + binding = ActivityShoppingBinding.inflate(layoutInflater).setContentView(this) initView() } private fun initView() { binding.presenter = presenter - binding.shoppingToolBar.setOnMenuItemClickListener(this) + initMenuClickListener() initRecyclerView() } + private fun initMenuClickListener() { + val cartItemView = binding.shoppingToolBar.findItemActionView(R.id.cart) + cartItemView?.setOnClickListener { presenter.navigateToCart() } + } + private fun initRecyclerView() { - binding.adapter = ConcatAdapter( - isolatedViewTypeConfig, recentProductWrapperAdapter, productAdapter, loadMoreAdapter - ) - presenter.fetchAll() + binding.adapter = isolatedViewTypeConcatAdapter { + add(recentProductWrapperAdapter) + add(productAdapter) + add(loadMoreAdapter) + } } - override fun updateProducts(products: List) { + override fun updateProducts(products: List) { productAdapter.submitList(products) } @@ -60,12 +75,12 @@ class ShoppingActivity : AppCompatActivity(), View, OnMenuItemClickListener { recentProductWrapperAdapter.submitList(recentProducts) } - override fun showProductDetail(product: UiProduct) { - startActivity(ProductDetailActivity.getIntent(this, product)) + override fun navigateToProductDetail(product: UiProduct, recentProduct: UiRecentProduct?) { + startActivity(ProductDetailActivity.getIntent(this, product, recentProduct)) } - override fun navigateToBasketScreen() { - startActivity(BasketActivity.getIntent(this)) + override fun navigateToCart() { + cartActivityLauncher.launch(CartActivity.getIntent(this)) } override fun showLoadMoreButton() { @@ -76,15 +91,45 @@ class ShoppingActivity : AppCompatActivity(), View, OnMenuItemClickListener { loadMoreAdapter.hideButton() } - override fun onMenuItemClick(item: MenuItem): Boolean { - when (item.itemId) { - R.id.basket -> presenter.openBasket() - } - return true + override fun updateCartBadge(count: ProductCount) { + val cartBadgeView = binding.shoppingToolBar.findItemActionView(R.id.cart) ?: return + val productCountTextView = cartBadgeView.findTextView(R.id.cart_count_badge) ?: return + + productCountTextView.visibility = count.getVisibility() + productCountTextView.text = count.toText() } - override fun onDestroy() { - super.onDestroy() - shoppingDatabase.close() + override fun onClickProduct(product: UiProduct) { + presenter.inquiryProductDetail(product) + } + + override fun onClickProductPlus(product: UiProduct) { + presenter.increaseCartCount(product) + } + + override fun onClickCounterPlus(product: UiProduct) { + presenter.increaseCartCount(product) + } + + override fun onClickCounterMinus(product: UiProduct) { + presenter.decreaseCartCount(product) + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + val product = intent?.getParcelableExtraCompat(PRODUCT_KEY) ?: return + val count = intent.getIntExtra(COUNT_KEY, 0) + presenter.increaseCartCount(product, count) + } + + companion object { + private const val PRODUCT_KEY = "product_key" + private const val COUNT_KEY = "count_key" + + fun getIntent(context: Context, product: UiProduct, count: Int): Intent = + Intent(context, ShoppingActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + .putExtra(PRODUCT_KEY, product) + .putExtra(COUNT_KEY, count) } } diff --git a/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingContract.kt b/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingContract.kt index 30ad7fa97..36bc658bc 100644 --- a/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingContract.kt +++ b/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingContract.kt @@ -1,26 +1,29 @@ package woowacourse.shopping.ui.shopping +import woowacourse.shopping.model.CartProduct +import woowacourse.shopping.model.ProductCount import woowacourse.shopping.model.UiProduct import woowacourse.shopping.model.UiRecentProduct interface ShoppingContract { interface View { - val presenter: Presenter - - fun updateProducts(products: List) + fun updateProducts(products: List) fun updateRecentProducts(recentProducts: List) - fun showProductDetail(product: UiProduct) - fun navigateToBasketScreen() + fun navigateToProductDetail(product: UiProduct, recentProduct: UiRecentProduct?) + fun navigateToCart() fun showLoadMoreButton() fun hideLoadMoreButton() + fun updateCartBadge(count: ProductCount) } abstract class Presenter(protected val view: View) { abstract fun fetchAll() - abstract fun fetchProducts() abstract fun fetchRecentProducts() + abstract fun loadMoreProducts() abstract fun inquiryProductDetail(product: UiProduct) abstract fun inquiryRecentProductDetail(recentProduct: UiRecentProduct) - abstract fun openBasket() + abstract fun navigateToCart() + abstract fun increaseCartCount(product: UiProduct, count: Int = 1) + abstract fun decreaseCartCount(product: UiProduct, count: Int = 1) } } diff --git a/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingPresenter.kt b/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingPresenter.kt index 9d36a0af9..753cb7ea1 100644 --- a/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingPresenter.kt +++ b/app/src/main/java/woowacourse/shopping/ui/shopping/ShoppingPresenter.kt @@ -1,77 +1,111 @@ package woowacourse.shopping.ui.shopping -import woowacourse.shopping.domain.Products -import woowacourse.shopping.domain.RecentProduct -import woowacourse.shopping.domain.RecentProducts -import woowacourse.shopping.domain.repository.DomainProductRepository -import woowacourse.shopping.domain.repository.DomainRecentProductRepository +import woowacourse.shopping.domain.model.Cart +import woowacourse.shopping.domain.model.DomainCartProduct +import woowacourse.shopping.domain.model.Product +import woowacourse.shopping.domain.model.ProductCount +import woowacourse.shopping.domain.model.RecentProduct +import woowacourse.shopping.domain.model.RecentProducts +import woowacourse.shopping.domain.model.page.LoadMore +import woowacourse.shopping.domain.model.page.Page +import woowacourse.shopping.domain.repository.CartRepository +import woowacourse.shopping.domain.repository.ProductRepository +import woowacourse.shopping.domain.repository.RecentProductRepository import woowacourse.shopping.mapper.toDomain import woowacourse.shopping.mapper.toUi import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.model.UiProductCount import woowacourse.shopping.model.UiRecentProduct import woowacourse.shopping.ui.shopping.ShoppingContract.Presenter import woowacourse.shopping.ui.shopping.ShoppingContract.View class ShoppingPresenter( view: View, - private val productRepository: DomainProductRepository, - private val recentProductRepository: DomainRecentProductRepository, + private val productRepository: ProductRepository, + private val recentProductRepository: RecentProductRepository, + private val cartRepository: CartRepository, + private val recentProductSize: Int = 10, + private var recentProducts: RecentProducts = RecentProducts(), + cartSize: Int = 20, ) : Presenter(view) { - private var products = Products() - private var recentProducts = RecentProducts() - + private var cart = Cart() + private var currentPage: Page = LoadMore(sizePerPage = cartSize) + private val cartProductCount: UiProductCount + get() = UiProductCount(cartRepository.getProductInCartSize()) override fun fetchAll() { - fetchProducts() + loadMoreProducts() fetchRecentProducts() } - override fun fetchProducts() { - val newProducts = productRepository - .getPartially(TOTAL_LOAD_PRODUCT_SIZE_AT_ONCE, products.lastId) - products = products.addAll(newProducts) + private fun convertToCartProduct(product: Product): DomainCartProduct { + val cartEntity = cartRepository.getCartEntity(product.id) + return DomainCartProduct( + cartEntity.id, + product, + ProductCount(cartEntity.count), + cartEntity.checked + ) + } - view.updateProducts(products.getItemsByUnit().map { it.toUi() }) - view.updateLoadMoreVisible() + override fun fetchRecentProducts() { + updateRecentProducts(recentProductRepository.getPartially(recentProductSize)) } - private fun View.updateLoadMoreVisible() { - if (products.canLoadMore()) { - showLoadMoreButton() - } else { - hideLoadMoreButton() - } + override fun loadMoreProducts() { + updateCart(cart + loadCartProducts(currentPage)) + view.updateLoadMoreVisible() + currentPage = currentPage.next() } override fun inquiryProductDetail(product: UiProduct) { val recentProduct = RecentProduct(product = product.toDomain()) - recentProducts += recentProduct + view.navigateToProductDetail(product, recentProducts.getLatest()?.toUi()) + recentProductRepository.add(recentProduct) + updateRecentProducts(recentProducts + recentProduct) + } - view.updateRecentProducts(recentProducts.getItems().map { it.toUi() }) - view.showProductDetail(product) + override fun inquiryRecentProductDetail(recentProduct: UiRecentProduct) { + view.navigateToProductDetail(recentProduct.product, recentProducts.getLatest()?.toUi()) + recentProductRepository.add(recentProduct.toDomain()) + } - recentProductRepository.add(recentProduct) + override fun navigateToCart() { + view.navigateToCart() } - override fun fetchRecentProducts() { - recentProducts = RecentProducts(recentProductRepository.getPartially(RECENT_PRODUCT_SIZE)) - view.updateRecentProducts(recentProducts.getItems().map { it.toUi() }) + override fun increaseCartCount(product: UiProduct, count: Int) { + val newProduct = product.toDomain() + cartRepository.increaseCartCount(newProduct, count) + updateCart(cart.increaseProductCount(newProduct, count)) } - override fun inquiryRecentProductDetail(recentProduct: UiRecentProduct) { - view.showProductDetail(recentProduct.product) - recentProductRepository.add(recentProduct.toDomain()) + override fun decreaseCartCount(product: UiProduct, count: Int) { + val removingProduct = product.toDomain() + cartRepository.decreaseCartCount(removingProduct, count) + updateCart(cart.decreaseProductCount(removingProduct, count)) + } + + private fun View.updateLoadMoreVisible() { + if (currentPage.hasNext(cart)) showLoadMoreButton() else hideLoadMoreButton() } - override fun openBasket() { - view.navigateToBasketScreen() + private fun updateCart(newCart: Cart) { + cart = cart.update(newCart) + updateCartView() } - companion object { - private const val RECENT_PRODUCT_SIZE = 10 - private const val LOAD_PRODUCT_SIZE_AT_ONCE = 20 - private const val PRODUCT_SIZE_FOR_HAS_NEXT = 1 - private const val TOTAL_LOAD_PRODUCT_SIZE_AT_ONCE = - LOAD_PRODUCT_SIZE_AT_ONCE + PRODUCT_SIZE_FOR_HAS_NEXT + private fun updateCartView() { + view.updateCartBadge(cartProductCount) + view.updateProducts(currentPage.takeItems(cart).toUi()) } + + private fun updateRecentProducts(newRecentProducts: RecentProducts) { + recentProducts = recentProducts.update(newRecentProducts) + view.updateRecentProducts(recentProducts.getItems().toUi()) + } + + private fun loadCartProducts(page: Page): List = productRepository + .getProductByPage(page) + .map { convertToCartProduct(it) } } diff --git a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/product/ProductAdapter.kt b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/product/ProductAdapter.kt index 3cdaeeec4..eb516475c 100644 --- a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/product/ProductAdapter.kt +++ b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/product/ProductAdapter.kt @@ -1,30 +1,28 @@ package woowacourse.shopping.ui.shopping.recyclerview.adapter.product import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.model.UiCartProduct import woowacourse.shopping.ui.shopping.ShoppingViewType +import woowacourse.shopping.util.diffutil.ProductDiffUtil +import woowacourse.shopping.util.listener.ProductClickListener +import woowacourse.shopping.widget.ProductCounterView.OnClickListener -class ProductAdapter(private val onItemClick: (UiProduct) -> Unit) : - ListAdapter(productDiffUtil) { +class ProductAdapter( + private val productClickListener: ProductClickListener, + private val counterClickListener: OnClickListener, +) : ListAdapter(ProductDiffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder = - ProductViewHolder(parent) { pos -> onItemClick(currentList[pos]) } + ProductViewHolder( + parent = parent, + productClickListener = productClickListener, + counterClickListener = counterClickListener, + ) override fun onBindViewHolder(holder: ProductViewHolder, position: Int) { holder.bind(getItem(position)) } override fun getItemViewType(position: Int): Int = ShoppingViewType.PRODUCT.value - - companion object { - private val productDiffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: UiProduct, newItem: UiProduct): Boolean = - oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: UiProduct, newItem: UiProduct): Boolean = - oldItem == newItem - } - } } diff --git a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/product/ProductViewHolder.kt b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/product/ProductViewHolder.kt index adb5fb809..f1e972b2c 100644 --- a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/product/ProductViewHolder.kt +++ b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/product/ProductViewHolder.kt @@ -5,20 +5,25 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import woowacourse.shopping.R import woowacourse.shopping.databinding.ItemProductBinding -import woowacourse.shopping.model.UiProduct -import woowacourse.shopping.util.extension.setOnSingleClickListener +import woowacourse.shopping.model.UiCartProduct +import woowacourse.shopping.util.listener.ProductClickListener +import woowacourse.shopping.widget.ProductCounterView.OnClickListener -class ProductViewHolder(parent: ViewGroup, onItemClick: (Int) -> Unit) : - RecyclerView.ViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.item_product, parent, false) - ) { +class ProductViewHolder( + parent: ViewGroup, + productClickListener: ProductClickListener, + counterClickListener: OnClickListener, +) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_product, parent, false) +) { private val binding = ItemProductBinding.bind(itemView) init { - binding.root.setOnSingleClickListener { onItemClick(bindingAdapterPosition) } + binding.productClickListener = productClickListener + binding.counterClickListener = counterClickListener } - fun bind(product: UiProduct) { - binding.product = product + fun bind(cartProduct: UiCartProduct) { + binding.cartProduct = cartProduct } } diff --git a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/recentproduct/RecentProductAdapter.kt b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/recentproduct/RecentProductAdapter.kt index 478ab69fb..bf82dddb5 100644 --- a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/recentproduct/RecentProductAdapter.kt +++ b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/adapter/recentproduct/RecentProductAdapter.kt @@ -1,12 +1,12 @@ package woowacourse.shopping.ui.shopping.recyclerview.adapter.recentproduct import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import woowacourse.shopping.model.UiRecentProduct +import woowacourse.shopping.util.diffutil.RecentProductDiffUtil class RecentProductAdapter(private val onItemClick: (UiRecentProduct) -> Unit) : - ListAdapter(recentProductDiffUtil) { + ListAdapter(RecentProductDiffUtil) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentProductViewHolder = RecentProductViewHolder(parent) { pos -> onItemClick(currentList[pos]) } @@ -14,14 +14,4 @@ class RecentProductAdapter(private val onItemClick: (UiRecentProduct) -> Unit) : override fun onBindViewHolder(holder: RecentProductViewHolder, position: Int) { holder.bind(getItem(position)) } - - companion object { - private val recentProductDiffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: UiRecentProduct, newItem: UiRecentProduct): - Boolean = oldItem.id == newItem.id - - override fun areContentsTheSame(oldItem: UiRecentProduct, newItem: UiRecentProduct): - Boolean = oldItem == newItem - } - } } diff --git a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/layoutmanager/ShoppingGridLayoutManager.kt b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/layoutmanager/ShoppingGridLayoutManager.kt index f6eb7c95f..fc03e95f4 100644 --- a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/layoutmanager/ShoppingGridLayoutManager.kt +++ b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/layoutmanager/ShoppingGridLayoutManager.kt @@ -10,8 +10,8 @@ import woowacourse.shopping.ui.shopping.ShoppingViewType class ShoppingGridLayoutManager( context: Context, adapter: Adapter, - spanSize: Int = DEFAULT_MAXIMUM_SPAN_SIZE, -) : GridLayoutManager(context, spanSize) { + maxSpanSize: Int = DEFAULT_MAXIMUM_SPAN_SIZE, +) : GridLayoutManager(context, maxSpanSize) { init { spanSizeLookup = object : SpanSizeLookup() { diff --git a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/listener/EndScrollListener.kt b/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/listener/EndScrollListener.kt deleted file mode 100644 index f1b8ff9b4..000000000 --- a/app/src/main/java/woowacourse/shopping/ui/shopping/recyclerview/listener/EndScrollListener.kt +++ /dev/null @@ -1,20 +0,0 @@ -package woowacourse.shopping.ui.shopping.recyclerview.listener - -import androidx.recyclerview.widget.RecyclerView - -class EndScrollListener( - private val onEndScroll: () -> Unit, -) : RecyclerView.OnScrollListener() { - - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - if (!recyclerView.canScrollVertically(DIRECTION_SCROLL_DOWN)) { - onEndScroll() - } - } - - companion object { - private const val DIRECTION_SCROLL_DOWN = 1 - } - -} diff --git a/app/src/main/java/woowacourse/shopping/util/ConcatConfig.kt b/app/src/main/java/woowacourse/shopping/util/ConcatConfig.kt deleted file mode 100644 index 51fb51fd8..000000000 --- a/app/src/main/java/woowacourse/shopping/util/ConcatConfig.kt +++ /dev/null @@ -1,8 +0,0 @@ -package woowacourse.shopping.util - -import androidx.recyclerview.widget.ConcatAdapter.Config - -val isolatedViewTypeConfig: Config - get() = Config.Builder() - .setIsolateViewTypes(false) - .build() diff --git a/app/src/main/java/woowacourse/shopping/util/bindingadapter/ProductContaierViewBindingAdapter.kt b/app/src/main/java/woowacourse/shopping/util/bindingadapter/ProductContaierViewBindingAdapter.kt new file mode 100644 index 000000000..fcde3ddd8 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/bindingadapter/ProductContaierViewBindingAdapter.kt @@ -0,0 +1,19 @@ +package woowacourse.shopping.util.bindingadapter + +import androidx.databinding.BindingAdapter +import woowacourse.shopping.widget.ProductCounterView + +@BindingAdapter("bind:count") +fun ProductCounterView.setCount(count: Int) { + this.count = count +} + +@BindingAdapter("bind:onPlusClick") +fun ProductCounterView.setOnPlusClick(onClick: Runnable) { + setOnPlusClickListener { _, _ -> onClick.run() } +} + +@BindingAdapter("bind:onMinusClick") +fun ProductCounterView.setOnMinusClick(onClick: Runnable) { + setOnMinusClickListener { _, _ -> onClick.run() } +} diff --git a/app/src/main/java/woowacourse/shopping/util/bindingadapter/RecyclerViewBindingAdapter.kt b/app/src/main/java/woowacourse/shopping/util/bindingadapter/RecyclerViewBindingAdapter.kt index 9004c4b4e..a312288d4 100644 --- a/app/src/main/java/woowacourse/shopping/util/bindingadapter/RecyclerViewBindingAdapter.kt +++ b/app/src/main/java/woowacourse/shopping/util/bindingadapter/RecyclerViewBindingAdapter.kt @@ -4,16 +4,11 @@ import androidx.databinding.BindingAdapter import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.LayoutManager -import woowacourse.shopping.ui.shopping.recyclerview.listener.EndScrollListener -@BindingAdapter("bind:adapter") -fun RecyclerView.setAdapter(adapter: ConcatAdapter) { +@BindingAdapter("bind:adapter", "bind:onAdapted", requireAll = false) +fun RecyclerView.setAdapter(adapter: ConcatAdapter, onAdapted: () -> Unit) { this.adapter = adapter -} - -@BindingAdapter("bind:onAdapted") -fun RecyclerView.setOnAdapted(onAdapted: () -> Unit) { - onAdapted() + onAdapted.invoke() } @BindingAdapter("bind:fixedSize") @@ -21,16 +16,12 @@ fun RecyclerView.setFixedSize(fixedSize: Boolean) { setHasFixedSize(fixedSize) } -@BindingAdapter("bind:onEndScroll") -fun RecyclerView.setOnEndScroll(onEndScroll: () -> Unit) { - addOnScrollListener(EndScrollListener(onEndScroll)) -} - @BindingAdapter("bind:layoutManager") fun RecyclerView.setLayoutManager(layoutManager: LayoutManager) { this.layoutManager = layoutManager } -interface OnAdaptedListener { - fun onAdapted() +@BindingAdapter("bind:animator") +fun RecyclerView.setAnimator(itemAnimator: RecyclerView.ItemAnimator?) { + this.itemAnimator = itemAnimator } diff --git a/app/src/main/java/woowacourse/shopping/util/bindingadapter/TextViewBindingAdapter.kt b/app/src/main/java/woowacourse/shopping/util/bindingadapter/TextViewBindingAdapter.kt new file mode 100644 index 000000000..d1cc2e7ef --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/bindingadapter/TextViewBindingAdapter.kt @@ -0,0 +1,11 @@ +package woowacourse.shopping.util.bindingadapter + +import android.widget.TextView +import androidx.databinding.BindingAdapter +import woowacourse.shopping.R +import woowacourse.shopping.model.UiPrice + +@BindingAdapter("bind:price") +fun TextView.setPrice(price: UiPrice?) { + text = context.getString(R.string.price_format, price?.value) +} diff --git a/app/src/main/java/woowacourse/shopping/util/builder/ConcatAdapterBuilder.kt b/app/src/main/java/woowacourse/shopping/util/builder/ConcatAdapterBuilder.kt new file mode 100644 index 000000000..9ee2d73ab --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/builder/ConcatAdapterBuilder.kt @@ -0,0 +1,17 @@ +package woowacourse.shopping.util.builder + +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.ViewHolder + +fun isolatedViewTypeConcatAdapter(block: ConcatAdapter.() -> Unit): ConcatAdapter = + ConcatAdapter(isolatedViewTypeConfig).apply(block) + +fun ConcatAdapter.add(adapter: Adapter) { + addAdapter(adapter) +} + +val isolatedViewTypeConfig: ConcatAdapter.Config + get() = ConcatAdapter.Config.Builder() + .setIsolateViewTypes(false) + .build() diff --git a/app/src/main/java/woowacourse/shopping/util/diffutil/CartDiffUtil.kt b/app/src/main/java/woowacourse/shopping/util/diffutil/CartDiffUtil.kt new file mode 100644 index 000000000..a7e284057 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/diffutil/CartDiffUtil.kt @@ -0,0 +1,16 @@ +package woowacourse.shopping.util.diffutil + +import androidx.recyclerview.widget.DiffUtil +import woowacourse.shopping.model.UiCartProduct + +object CartDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: UiCartProduct, + newItem: UiCartProduct, + ): Boolean = oldItem.id == newItem.id + + override fun areContentsTheSame( + oldItem: UiCartProduct, + newItem: UiCartProduct, + ): Boolean = oldItem == newItem +} diff --git a/app/src/main/java/woowacourse/shopping/util/diffutil/ProductDiffUtil.kt b/app/src/main/java/woowacourse/shopping/util/diffutil/ProductDiffUtil.kt new file mode 100644 index 000000000..280e3a905 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/diffutil/ProductDiffUtil.kt @@ -0,0 +1,16 @@ +package woowacourse.shopping.util.diffutil + +import androidx.recyclerview.widget.DiffUtil +import woowacourse.shopping.model.CartProduct + +object ProductDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: CartProduct, + newItem: CartProduct, + ): Boolean = oldItem.product.id == newItem.product.id + + override fun areContentsTheSame( + oldItem: CartProduct, + newItem: CartProduct, + ): Boolean = oldItem == newItem +} diff --git a/app/src/main/java/woowacourse/shopping/util/diffutil/RecentProductDiffUtil.kt b/app/src/main/java/woowacourse/shopping/util/diffutil/RecentProductDiffUtil.kt new file mode 100644 index 000000000..27d83c366 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/diffutil/RecentProductDiffUtil.kt @@ -0,0 +1,16 @@ +package woowacourse.shopping.util.diffutil + +import androidx.recyclerview.widget.DiffUtil +import woowacourse.shopping.model.UiRecentProduct + +object RecentProductDiffUtil : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: UiRecentProduct, + newItem: UiRecentProduct, + ): Boolean = oldItem.id == newItem.id + + override fun areContentsTheSame( + oldItem: UiRecentProduct, + newItem: UiRecentProduct, + ): Boolean = oldItem == newItem +} diff --git a/app/src/main/java/woowacourse/shopping/util/extension/AndroidExtension.kt b/app/src/main/java/woowacourse/shopping/util/extension/AndroidExtension.kt deleted file mode 100644 index 877dc72f0..000000000 --- a/app/src/main/java/woowacourse/shopping/util/extension/AndroidExtension.kt +++ /dev/null @@ -1,42 +0,0 @@ -package woowacourse.shopping.util.extension - -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.os.Parcelable - -@Suppress("DEPRECATION") -inline fun Intent.getParcelableExtraCompat(key: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelableExtra(key, T::class.java) - } else { - getParcelableExtra(key) as? T - } -} - -@Suppress("DEPRECATION") -inline fun Intent.getParcelableArrayListExtraCompat(key: String): ArrayList? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelableArrayListExtra(key, T::class.java) - } else { - getParcelableArrayListExtra(key) - } -} - -@Suppress("DEPRECATION") -inline fun Bundle.getParcelableCompat(key: String): T? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelable(key, T::class.java) - } else { - getParcelable(key) as? T - } -} - -@Suppress("DEPRECATION") -inline fun Bundle.getParcelableArrayListCompat(key: String): ArrayList? { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getParcelableArrayList(key, T::class.java) - } else { - getParcelableArrayList(key) - } -} diff --git a/app/src/main/java/woowacourse/shopping/util/extension/ContextExtension.kt b/app/src/main/java/woowacourse/shopping/util/extension/ContextExtension.kt new file mode 100644 index 000000000..72ba5db9a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/extension/ContextExtension.kt @@ -0,0 +1,8 @@ +package woowacourse.shopping.util.extension + +import android.content.Context +import android.widget.Toast + +fun Context.showToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} diff --git a/app/src/main/java/woowacourse/shopping/util/extension/ImageViewExt.kt b/app/src/main/java/woowacourse/shopping/util/extension/ImageViewExtension.kt similarity index 100% rename from app/src/main/java/woowacourse/shopping/util/extension/ImageViewExt.kt rename to app/src/main/java/woowacourse/shopping/util/extension/ImageViewExtension.kt diff --git a/app/src/main/java/woowacourse/shopping/util/extension/IntentExtension.kt b/app/src/main/java/woowacourse/shopping/util/extension/IntentExtension.kt new file mode 100644 index 000000000..580437b98 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/extension/IntentExtension.kt @@ -0,0 +1,13 @@ +package woowacourse.shopping.util.extension + +import android.content.Intent +import android.os.Build +import android.os.Parcelable + +inline fun Intent.getParcelableExtraCompat(key: String): T? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getParcelableExtra(key, T::class.java) + } else { + getParcelableExtra(key) as? T + } +} diff --git a/app/src/main/java/woowacourse/shopping/util/extension/StringExtension.kt b/app/src/main/java/woowacourse/shopping/util/extension/StringExtension.kt new file mode 100644 index 000000000..d0257611b --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/extension/StringExtension.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.util.extension + +fun String.parseQueryString(): Map { + val queryStrings = mutableMapOf() + substringAfter("?").split("&").forEach { + val (key, value) = it.trim().split("=") + queryStrings[key] = value + } + return queryStrings +} diff --git a/app/src/main/java/woowacourse/shopping/util/extension/ToolbarExtension.kt b/app/src/main/java/woowacourse/shopping/util/extension/ToolbarExtension.kt new file mode 100644 index 000000000..f296ac0b3 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/extension/ToolbarExtension.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.util.extension + +import android.view.MenuItem +import android.view.View +import androidx.annotation.IdRes +import androidx.appcompat.widget.Toolbar + +fun Toolbar.findItem(@IdRes id: Int): MenuItem = menu.findItem(id) + +fun Toolbar.findItemActionView(@IdRes id: Int): View? = findItem(id).actionView diff --git a/app/src/main/java/woowacourse/shopping/util/extension/ViewDataBindingExtension.kt b/app/src/main/java/woowacourse/shopping/util/extension/ViewDataBindingExtension.kt new file mode 100644 index 000000000..bf2c992df --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/extension/ViewDataBindingExtension.kt @@ -0,0 +1,9 @@ +package woowacourse.shopping.util.extension + +import android.app.Activity +import androidx.databinding.ViewDataBinding + +fun T.setContentView(activity: Activity): T = run { + activity.setContentView(root) + this +} diff --git a/app/src/main/java/woowacourse/shopping/util/extension/ViewExtension.kt b/app/src/main/java/woowacourse/shopping/util/extension/ViewExtension.kt index 6b91b0bd7..205cecbee 100644 --- a/app/src/main/java/woowacourse/shopping/util/extension/ViewExtension.kt +++ b/app/src/main/java/woowacourse/shopping/util/extension/ViewExtension.kt @@ -1,6 +1,8 @@ package woowacourse.shopping.util.extension import android.view.View +import android.widget.TextView +import androidx.annotation.IdRes inline fun View.setOnSingleClickListener( delay: Long = 500L, @@ -15,3 +17,6 @@ inline fun View.setOnSingleClickListener( } } } + +fun View.findTextView(@IdRes id: Int): TextView? = findViewById(id) ?: null + diff --git a/app/src/main/java/woowacourse/shopping/util/factory/PresenterFactory.kt b/app/src/main/java/woowacourse/shopping/util/factory/PresenterFactory.kt deleted file mode 100644 index 2bdd56a29..000000000 --- a/app/src/main/java/woowacourse/shopping/util/factory/PresenterFactory.kt +++ /dev/null @@ -1,50 +0,0 @@ -package woowacourse.shopping.util.factory - -import android.content.Context -import woowacourse.shopping.data.database.ShoppingDatabase -import woowacourse.shopping.data.database.dao.basket.BasketDaoImpl -import woowacourse.shopping.data.database.dao.product.ProductDaoImpl -import woowacourse.shopping.data.database.dao.recentproduct.RecentProductDaoImpl -import woowacourse.shopping.data.datasource.basket.LocalBasketDataSource -import woowacourse.shopping.data.datasource.product.LocalProductDataSource -import woowacourse.shopping.data.datasource.recentproduct.LocalRecentProductDataSource -import woowacourse.shopping.data.repository.BasketRepository -import woowacourse.shopping.data.repository.ProductRepository -import woowacourse.shopping.data.repository.RecentProductRepository -import woowacourse.shopping.model.UiProduct -import woowacourse.shopping.ui.basket.BasketContract -import woowacourse.shopping.ui.basket.BasketPresenter -import woowacourse.shopping.ui.productdetail.ProductDetailContract -import woowacourse.shopping.ui.productdetail.ProductDetailPresenter -import woowacourse.shopping.ui.shopping.ShoppingContract -import woowacourse.shopping.ui.shopping.ShoppingPresenter - -fun createShoppingPresenter( - view: ShoppingContract.View, - database: ShoppingDatabase, -): ShoppingContract.Presenter = ShoppingPresenter( - view, - ProductRepository(LocalProductDataSource(ProductDaoImpl(database))), - RecentProductRepository( - LocalRecentProductDataSource(RecentProductDaoImpl(database)) - ) -) - -fun createProductDetailPresenter( - view: ProductDetailContract.View, - context: Context, - product: UiProduct, -): ProductDetailContract.Presenter = ProductDetailPresenter( - view = view, - basketRepository = - BasketRepository(LocalBasketDataSource(BasketDaoImpl(ShoppingDatabase(context)))), - product = product -) - -fun createBasketPresenter( - view: BasketContract.View, - database: ShoppingDatabase, -): BasketContract.Presenter = BasketPresenter( - view, - BasketRepository(LocalBasketDataSource(BasketDaoImpl(database))) -) diff --git a/app/src/main/java/woowacourse/shopping/util/inject/DaoInject.kt b/app/src/main/java/woowacourse/shopping/util/inject/DaoInject.kt new file mode 100644 index 000000000..56dad881a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/inject/DaoInject.kt @@ -0,0 +1,13 @@ +package woowacourse.shopping.util.inject + +import woowacourse.shopping.data.database.ShoppingDatabase +import woowacourse.shopping.data.database.dao.cart.CartDao +import woowacourse.shopping.data.database.dao.cart.CartDaoImpl +import woowacourse.shopping.data.database.dao.recentproduct.RecentProductDao +import woowacourse.shopping.data.database.dao.recentproduct.RecentProductDaoImpl + +fun injectRecentProductDao(database: ShoppingDatabase): RecentProductDao = + RecentProductDaoImpl(database) + +fun injectCartDao(database: ShoppingDatabase): CartDao = + CartDaoImpl(database) diff --git a/app/src/main/java/woowacourse/shopping/util/inject/DataSourceInject.kt b/app/src/main/java/woowacourse/shopping/util/inject/DataSourceInject.kt new file mode 100644 index 000000000..1ff8efb6a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/inject/DataSourceInject.kt @@ -0,0 +1,19 @@ +package woowacourse.shopping.util.inject + +import woowacourse.shopping.data.database.dao.cart.CartDao +import woowacourse.shopping.data.database.dao.recentproduct.RecentProductDao +import woowacourse.shopping.data.datasource.cart.CartDataSource +import woowacourse.shopping.data.datasource.cart.LocalCartDataSource +import woowacourse.shopping.data.datasource.product.ProductDataSource +import woowacourse.shopping.data.datasource.product.RemoteProductDataSource +import woowacourse.shopping.data.datasource.recentproduct.LocalRecentProductDataSource +import woowacourse.shopping.data.datasource.recentproduct.RecentProductDataSource + +fun inject(): ProductDataSource.Remote = + RemoteProductDataSource() + +fun inject(dao: RecentProductDao): RecentProductDataSource.Local = + LocalRecentProductDataSource(dao) + +fun inject(dao: CartDao): CartDataSource.Local = + LocalCartDataSource(dao) diff --git a/app/src/main/java/woowacourse/shopping/util/inject/DatabaseInject.kt b/app/src/main/java/woowacourse/shopping/util/inject/DatabaseInject.kt new file mode 100644 index 000000000..7ac350e92 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/inject/DatabaseInject.kt @@ -0,0 +1,7 @@ +package woowacourse.shopping.util.inject + +import android.content.Context +import woowacourse.shopping.data.database.ShoppingDatabase + +fun createShoppingDatabase(context: Context): ShoppingDatabase = + ShoppingDatabase(context) diff --git a/app/src/main/java/woowacourse/shopping/util/inject/PresenterInject.kt b/app/src/main/java/woowacourse/shopping/util/inject/PresenterInject.kt new file mode 100644 index 000000000..042af7261 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/inject/PresenterInject.kt @@ -0,0 +1,46 @@ +package woowacourse.shopping.util.inject + +import android.content.Context +import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.model.UiRecentProduct +import woowacourse.shopping.ui.cart.CartContract +import woowacourse.shopping.ui.cart.CartPresenter +import woowacourse.shopping.ui.detail.ProductDetailContract +import woowacourse.shopping.ui.detail.ProductDetailPresenter +import woowacourse.shopping.ui.shopping.ShoppingContract +import woowacourse.shopping.ui.shopping.ShoppingPresenter + +fun inject( + view: ShoppingContract.View, + context: Context, +): ShoppingContract.Presenter { + val database = createShoppingDatabase(context) + return ShoppingPresenter( + view, + inject(inject()), + inject(inject(injectRecentProductDao(database))), + inject(inject(injectCartDao(database))), + ) +} + +fun inject( + view: ProductDetailContract.View, + detailProduct: UiProduct, + recentProduct: UiRecentProduct?, +): ProductDetailContract.Presenter = ProductDetailPresenter( + view = view, + product = detailProduct, + recentProduct = recentProduct, +) + +fun inject( + view: CartContract.View, + context: Context, +): CartPresenter { + val database = createShoppingDatabase(context) + return CartPresenter( + view, + inject(inject()), + inject(inject(injectCartDao(database))) + ) +} diff --git a/app/src/main/java/woowacourse/shopping/util/inject/RepositoryInject.kt b/app/src/main/java/woowacourse/shopping/util/inject/RepositoryInject.kt new file mode 100644 index 000000000..cee9d42ec --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/inject/RepositoryInject.kt @@ -0,0 +1,20 @@ +package woowacourse.shopping.util.inject + +import woowacourse.shopping.data.datasource.cart.CartDataSource +import woowacourse.shopping.data.datasource.product.ProductDataSource +import woowacourse.shopping.data.datasource.recentproduct.RecentProductDataSource +import woowacourse.shopping.data.repository.CartRepositoryImpl +import woowacourse.shopping.data.repository.ProductRepositoryImpl +import woowacourse.shopping.data.repository.RecentProductRepositoryImpl +import woowacourse.shopping.domain.repository.CartRepository +import woowacourse.shopping.domain.repository.ProductRepository +import woowacourse.shopping.domain.repository.RecentProductRepository + +fun inject(localDataSource: ProductDataSource.Remote): ProductRepository = + ProductRepositoryImpl(localDataSource) + +fun inject(localDataSource: RecentProductDataSource.Local): RecentProductRepository = + RecentProductRepositoryImpl(localDataSource) + +fun inject(localDataSource: CartDataSource.Local): CartRepository = + CartRepositoryImpl(localDataSource) diff --git a/app/src/main/java/woowacourse/shopping/util/listener/ProductClickListener.kt b/app/src/main/java/woowacourse/shopping/util/listener/ProductClickListener.kt new file mode 100644 index 000000000..c21b9ce5e --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/listener/ProductClickListener.kt @@ -0,0 +1,8 @@ +package woowacourse.shopping.util.listener + +import woowacourse.shopping.model.Product + +interface ProductClickListener { + fun onClickProduct(product: Product) + fun onClickProductPlus(product: Product) +} diff --git a/app/src/main/java/woowacourse/shopping/widget/ProductCounterView.kt b/app/src/main/java/woowacourse/shopping/widget/ProductCounterView.kt new file mode 100644 index 000000000..ecae142a3 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/widget/ProductCounterView.kt @@ -0,0 +1,82 @@ +package woowacourse.shopping.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import woowacourse.shopping.R +import woowacourse.shopping.databinding.ViewCounterBinding +import woowacourse.shopping.model.UiProduct +import kotlin.properties.Delegates + +class ProductCounterView : ConstraintLayout { + private val binding by lazy { + ViewCounterBinding.inflate(LayoutInflater.from(context), this, true) + } + var count: Int by Delegates.observable(INITIAL_COUNT) { _, _, newCount -> + binding.countTextView.text = newCount.toString() + } + private var minCount: Int = DEFAULT_MIN_COUNT + private var maxCount: Int = DEFAULT_MAX_COUNT + + init { + binding.count = count + binding.counterPlusButton.setOnClickListener { plusCount() } + binding.counterMinusButton.setOnClickListener { minusCount() } + } + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + initTypedArrayValue(attrs) + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + initTypedArrayValue(attrs) + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { + initTypedArrayValue(attrs) + } + + private fun initTypedArrayValue(attrs: AttributeSet) { + context.obtainStyledAttributes(attrs, R.styleable.ProductCounterView).use { + count = it.getInt(R.styleable.ProductCounterView_count, INITIAL_COUNT) + minCount = it.getInt(R.styleable.ProductCounterView_min_count, DEFAULT_MIN_COUNT) + maxCount = it.getInt(R.styleable.ProductCounterView_max_count, DEFAULT_MAX_COUNT) + } + } + + fun setOnPlusClickListener(onPlusClick: (view: ProductCounterView, newCount: Int) -> Unit) { + binding.counterPlusButton.setOnClickListener { + plusCount() + onPlusClick(this, count) + } + } + + fun setOnMinusClickListener(onMinusClick: (view: ProductCounterView, newCount: Int) -> Unit) { + binding.counterMinusButton.setOnClickListener { + minusCount() + onMinusClick(this, count) + } + } + + private fun plusCount() { + if (count < maxCount) ++count + } + + private fun minusCount() { + if (count > minCount) --count + } + + companion object { + private const val INITIAL_COUNT: Int = 1 + private const val DEFAULT_MIN_COUNT = 1 + private const val DEFAULT_MAX_COUNT = 99 + } + + interface OnClickListener { + fun onClickCounterPlus(product: UiProduct) + fun onClickCounterMinus(product: UiProduct) + } +} diff --git a/app/src/main/res/color/color_order_button.xml b/app/src/main/res/color/color_order_button.xml new file mode 100644 index 000000000..811f8a010 --- /dev/null +++ b/app/src/main/res/color/color_order_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_plus.xml b/app/src/main/res/drawable/ic_plus.xml new file mode 100644 index 000000000..a9503fd36 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/shape_cart_count_badge.xml b/app/src/main/res/drawable/shape_cart_count_badge.xml new file mode 100644 index 000000000..c5aaeaa96 --- /dev/null +++ b/app/src/main/res/drawable/shape_cart_count_badge.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/shape_counter_minus.xml b/app/src/main/res/drawable/shape_counter_minus.xml new file mode 100644 index 000000000..937236cee --- /dev/null +++ b/app/src/main/res/drawable/shape_counter_minus.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_counter_plus.xml b/app/src/main/res/drawable/shape_counter_plus.xml new file mode 100644 index 000000000..db3b31321 --- /dev/null +++ b/app/src/main/res/drawable/shape_counter_plus.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_woowa_round_4_white_rect.xml b/app/src/main/res/drawable/shape_woowa_round_4_white_rect.xml new file mode 100644 index 000000000..35f4685a6 --- /dev/null +++ b/app/src/main/res/drawable/shape_woowa_round_4_white_rect.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/layout/activity_basket.xml b/app/src/main/res/layout/activity_basket.xml deleted file mode 100644 index 248e8116e..000000000 --- a/app/src/main/res/layout/activity_basket.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_cart.xml b/app/src/main/res/layout/activity_cart.xml new file mode 100644 index 000000000..0784bcfd5 --- /dev/null +++ b/app/src/main/res/layout/activity_cart.xml @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_product_detail.xml b/app/src/main/res/layout/activity_product_detail.xml index 532c874ef..28540aace 100644 --- a/app/src/main/res/layout/activity_product_detail.xml +++ b/app/src/main/res/layout/activity_product_detail.xml @@ -1,103 +1,179 @@ + + + + + name="detailProduct" + type="woowacourse.shopping.model.Product" /> + + - + android:fillViewport="true"> - - - - - - - + tools:context=".ui.detail.ProductDetailActivity"> - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_shopping.xml b/app/src/main/res/layout/activity_shopping.xml index c9768f9ed..294aaf7ed 100644 --- a/app/src/main/res/layout/activity_shopping.xml +++ b/app/src/main/res/layout/activity_shopping.xml @@ -26,7 +26,8 @@ android:id="@+id/shopping_tool_bar" android:layout_width="match_parent" android:layout_height="?actionBarSize" - android:background="@color/woowa_toolbar_gray" + android:background="@color/woowa_dark_gray" + android:paddingEnd="20dp" app:layout_constraintTop_toTopOf="parent" app:menu="@menu/menu_shopping" app:title="@string/tb_shopping" @@ -37,8 +38,10 @@ android:layout_width="match_parent" android:layout_height="0dp" bind:adapter="@{adapter}" + bind:animator="@{null}" bind:fixedSize="@{true}" bind:layoutManager="@{ShoppingGridLayoutManager.create(context, adapter)}" + bind:onAdapted="@{() -> presenter.fetchAll()}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@+id/shopping_tool_bar" app:spanCount="2" diff --git a/app/src/main/res/layout/item_basket.xml b/app/src/main/res/layout/item_cart.xml similarity index 56% rename from app/src/main/res/layout/item_basket.xml rename to app/src/main/res/layout/item_cart.xml index a8636c3e3..11cb095b3 100644 --- a/app/src/main/res/layout/item_basket.xml +++ b/app/src/main/res/layout/item_cart.xml @@ -6,9 +6,17 @@ + + + + + name="cartClickListener" + type="woowacourse.shopping.ui.cart.listener.CartClickListener" /> + + @@ -68,12 +90,27 @@ android:id="@+id/product_price_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@{@string/price_format(product.price.value)}" + android:includeFontPadding="false" + bind:price="@{cartProduct.product.price}" android:textColor="@color/woowa_text_black" android:textSize="16sp" - app:layout_constraintBottom_toBottomOf="@+id/product_image_view" + app:layout_constraintBottom_toTopOf="@+id/counter_view" app:layout_constraintEnd_toEndOf="@id/close_button" tools:text="99,800원" /> + + diff --git a/app/src/main/res/layout/item_product.xml b/app/src/main/res/layout/item_product.xml index da8b1050c..ed9b3f401 100644 --- a/app/src/main/res/layout/item_product.xml +++ b/app/src/main/res/layout/item_product.xml @@ -6,14 +6,25 @@ + + + + + name="productClickListener" + type="woowacourse.shopping.util.listener.ProductClickListener" /> + + @@ -21,7 +32,7 @@ android:id="@+id/product_image_view" android:layout_width="match_parent" android:layout_height="0dp" - bind:imageUrl="@{product.imageUrl}" + bind:imageUrl="@{cartProduct.product.imageUrl}" android:scaleType="centerCrop" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" @@ -29,6 +40,40 @@ app:layout_constraintTop_toTopOf="parent" tools:srcCompat="@tools:sample/avatars" /> + + + + + diff --git a/app/src/main/res/layout/layout_cart_badge.xml b/app/src/main/res/layout/layout_cart_badge.xml new file mode 100644 index 000000000..0609f53b9 --- /dev/null +++ b/app/src/main/res/layout/layout_cart_badge.xml @@ -0,0 +1,39 @@ + + + + + + + + diff --git a/app/src/main/res/layout/layout_product_counter_dialog.xml b/app/src/main/res/layout/layout_product_counter_dialog.xml new file mode 100644 index 000000000..1cd54aa46 --- /dev/null +++ b/app/src/main/res/layout/layout_product_counter_dialog.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_counter.xml b/app/src/main/res/layout/view_counter.xml new file mode 100644 index 000000000..b764b1775 --- /dev/null +++ b/app/src/main/res/layout/view_counter.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_shopping.xml b/app/src/main/res/menu/menu_shopping.xml index 6bbb9bc75..14b0ad366 100644 --- a/app/src/main/res/menu/menu_shopping.xml +++ b/app/src/main/res/menu/menu_shopping.xml @@ -2,8 +2,8 @@ diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 000000000..5303f77ee --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index bb26a63ab..9547c8bc3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,12 +8,12 @@ #FF000000 #FFFFFFFF - #555555 + #555555 #333333 #AAAAAA - #04C09E + #04C09E #EBEBEB #E6E6E6 #04C09E - + #D5D5D5 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2df4c177..d41b34f33 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,7 +10,7 @@ 금액 - 장바구니 담기 + 장바구니 담기 Cart @@ -19,6 +19,14 @@ ]]> 1 화면 닫기 - 장바구니 - + 장바구니 + - + + + 마지막으로 본 상품 + 담기 + 주문하기(%d) + 주문하기 + 전체 + 제품 %d개를 성공적으로 주문하였습니다! + 주문에 실패하였습니다! diff --git a/app/src/test/java/woowacourse/shopping/basket/BasketPresenterTest.kt b/app/src/test/java/woowacourse/shopping/basket/BasketPresenterTest.kt deleted file mode 100644 index a0ed4a7cf..000000000 --- a/app/src/test/java/woowacourse/shopping/basket/BasketPresenterTest.kt +++ /dev/null @@ -1,108 +0,0 @@ -package woowacourse.shopping.basket - -import io.mockk.every -import io.mockk.mockk -import io.mockk.slot -import io.mockk.verify -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import woowacourse.shopping.domain.PageNumber -import woowacourse.shopping.domain.repository.BasketRepository -import woowacourse.shopping.ui.basket.BasketContract -import woowacourse.shopping.ui.basket.BasketPresenter - -internal class BasketPresenterTest { - - private lateinit var presenter: BasketContract.Presenter - private lateinit var view: BasketContract.View - private lateinit var basketRepository: BasketRepository - - @Before - fun setUp() { - basketRepository = mockk(relaxed = true) - view = mockk(relaxed = true) - presenter = BasketPresenter(view, basketRepository) - } - - @Test - internal fun 장바구니를_목록을_갱신하면_현재_페이지에_해당하는_아이템을_보여주고_네비게이터를_갱신한다() { - // given - /* ... */ - - // when - presenter.fetchBasket() - - // then - verify(exactly = 1) { basketRepository.getPartially(any()) } - verify(exactly = 1) { view.updateBasket(any()) } - verify(exactly = 1) { view.updateNavigatorEnabled(any(), any()) } - verify(exactly = 1) { view.updatePageNumber(any()) } - } - - @Test - internal fun 이전_장바구니를_불러오면_페이지를_변경하고_장바구니를_갱신한다() { - // given - val page = 2 - presenter = BasketPresenter(view, basketRepository, currentPage = PageNumber(page)) - - val currentPage = slot() - every { basketRepository.getPartially(capture(currentPage)) } returns mockk(relaxed = true) - - // when - presenter.fetchPrevious() - - // then - assertEquals(currentPage.captured, PageNumber(page - 1)) - verify(exactly = 1) { view.updateBasket(any()) } - verify(exactly = 1) { view.updateNavigatorEnabled(any(), any()) } - verify(exactly = 1) { view.updatePageNumber(any()) } - } - - @Test - internal fun 다음_장바구니를_불러오면_페이지를_변경하고_장바구니를_갱신한다() { - // given - val page = 1 - presenter = BasketPresenter(view, basketRepository, currentPage = PageNumber(page)) - - val currentPage = slot() - every { basketRepository.getPartially(capture(currentPage)) } returns mockk(relaxed = true) - - // when - presenter.fetchNext() - - // then - assertEquals(currentPage.captured, PageNumber(page + 1)) - verify(exactly = 1) { view.updateBasket(any()) } - verify(exactly = 1) { view.updateNavigatorEnabled(any(), any()) } - verify(exactly = 1) { view.updatePageNumber(any()) } - } - - @Test - internal fun 장바구니_목록에_있는_제품을_제거하면_뷰를_갱신한다() { - // given - /* ... */ - - // when - presenter.removeBasketProduct(mockk(relaxed = true)) - - // then - verify(exactly = 1) { basketRepository.remove(any()) } - verify(exactly = 1) { basketRepository.getPartially(any()) } - verify(exactly = 1) { view.updateBasket(any()) } - verify(exactly = 1) { view.updateNavigatorEnabled(any(), any()) } - verify(exactly = 1) { view.updatePageNumber(any()) } - } - - @Test - internal fun 종료하면_화면을_닫는다() { - // given - /* ... */ - - // when - presenter.closeScreen() - - // then - verify(exactly = 1) { view.closeScreen() } - } -} diff --git a/app/src/test/java/woowacourse/shopping/ui/cart/CartPresenterTest.kt b/app/src/test/java/woowacourse/shopping/ui/cart/CartPresenterTest.kt new file mode 100644 index 000000000..5247d494f --- /dev/null +++ b/app/src/test/java/woowacourse/shopping/ui/cart/CartPresenterTest.kt @@ -0,0 +1,114 @@ +package woowacourse.shopping.ui.cart + +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import woowacourse.shopping.domain.model.page.Page +import woowacourse.shopping.domain.model.page.Pagination +import woowacourse.shopping.domain.repository.CartRepository +import woowacourse.shopping.domain.repository.ProductRepository +import woowacourse.shopping.mapper.toDomain +import woowacourse.shopping.model.Product +import woowacourse.shopping.model.UiPrice + +internal class CartPresenterTest { + private lateinit var presenter: CartContract.Presenter + private lateinit var view: CartContract.View + private lateinit var productRepository: ProductRepository + private lateinit var cartRepository: CartRepository + + @Before + fun setUp() { + cartRepository = mockk(relaxed = true) + productRepository = mockk(relaxed = true) + view = mockk(relaxed = true) + presenter = CartPresenter(view, productRepository, cartRepository) + } + + @Test + internal fun 장바구니를_목록을_갱신하면_현재_페이지에_해당하는_아이템을_보여주고_네비게이터를_갱신한다() { + // given + val page = 1 + + // when + presenter.fetchCart(page) + + // then + verify(exactly = 1) { view.updateCart(any()) } + verify(exactly = 1) { view.updateNavigatorEnabled(any(), any()) } + verify(exactly = 1) { view.updatePageNumber(any()) } + } + + @Test + internal fun 이전_장바구니를_불러오면_페이지를_변경하고_장바구니를_갱신한다() { + // given + val page = 2 + presenter = CartPresenter(view, productRepository, cartRepository) + + val currentPage = slot() + + // when + presenter.fetchCart(page - 1) + + // then + assertEquals(currentPage.captured, Pagination(page - 1)) + verify(exactly = 1) { view.updateCart(any()) } + verify(exactly = 1) { view.updateNavigatorEnabled(any(), any()) } + verify(exactly = 1) { view.updatePageNumber(any()) } + } + + @Test + internal fun 다음_장바구니를_불러오면_페이지를_변경하고_장바구니를_갱신한다() { + // given + val page = 1 + presenter = CartPresenter(view, productRepository, cartRepository) + + val currentPage = slot() + + // when + presenter.fetchCart(page + 1) + + // then + assertEquals(currentPage.captured, Pagination(page + 1)) + verify(exactly = 1) { view.updateCart(any()) } + verify(exactly = 1) { view.updateNavigatorEnabled(any(), any()) } + verify(exactly = 1) { view.updatePageNumber(any()) } + } + + @Test + internal fun 장바구니_목록에_있는_제품을_제거하면_뷰를_갱신한다() { + // given + val products = MutableList(8) { id -> + Product(id, "상품 $id", UiPrice(1000), "") + } + val product = Product(0, "상품 0", UiPrice(1000), "") + every { cartRepository.decreaseCartCount(product.toDomain(), 1) } answers { + products.remove(product) + } + + // when + presenter.removeProduct(product) + + // then + verify(exactly = 1) { cartRepository.decreaseCartCount(product.toDomain(), 1) } + verify(exactly = 1) { view.updateCart(any()) } + verify(exactly = 1) { view.updateNavigatorEnabled(any(), any()) } + verify(exactly = 1) { view.updatePageNumber(any()) } + } + + @Test + internal fun 종료하면_화면을_닫는다() { + // given + /* ... */ + + // when + presenter.navigateToHome() + + // then + verify(exactly = 1) { view.navigateToHome() } + } +} diff --git a/app/src/test/java/woowacourse/shopping/ui/detail/ProductDetailPresenterTest.kt b/app/src/test/java/woowacourse/shopping/ui/detail/ProductDetailPresenterTest.kt new file mode 100644 index 000000000..817871502 --- /dev/null +++ b/app/src/test/java/woowacourse/shopping/ui/detail/ProductDetailPresenterTest.kt @@ -0,0 +1,48 @@ +package woowacourse.shopping.ui.detail + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Before +import org.junit.Test +import woowacourse.shopping.model.UiProduct +import woowacourse.shopping.model.UiRecentProduct + +internal class ProductDetailPresenterTest { + private lateinit var presenter: ProductDetailContract.Presenter + private lateinit var view: ProductDetailContract.View + private lateinit var detailProduct: UiProduct + private lateinit var recentProduct: UiRecentProduct + + @Before + fun setUp() { + view = mockk(relaxed = true) + detailProduct = mockk(relaxed = true) + recentProduct = mockk(relaxed = true) + presenter = ProductDetailPresenter(view, detailProduct, recentProduct) + } + + @Test + internal fun 프레젠터가_초기화될_때_상품_정보를_보여준다() { + // given + /* ... */ + + // when + /* init */ + + // then + verify(exactly = 1) { view.showProductDetail(detailProduct) } + verify(exactly = 1) { view.showLastViewedProductDetail(recentProduct.product) } + } + + @Test + internal fun 장바구니에_상품을_추가한다() { + // given + /* ... */ + + // when + presenter.inquiryProductCounter() + + // then + verify(exactly = 1) { view.showProductCounter(detailProduct) } + } +} diff --git a/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailPresenterTest.kt b/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailPresenterTest.kt deleted file mode 100644 index 12714b4ad..000000000 --- a/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailPresenterTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -package woowacourse.shopping.ui.productdetail - -import io.mockk.mockk -import io.mockk.verify -import org.junit.Before -import org.junit.Test -import woowacourse.shopping.domain.repository.BasketRepository - -internal class ProductDetailPresenterTest { - private lateinit var presenter: ProductDetailContract.Presenter - private lateinit var view: ProductDetailContract.View - private lateinit var basketRepository: BasketRepository - - @Before - fun setUp() { - view = mockk(relaxed = true) - basketRepository = mockk(relaxed = true) - presenter = ProductDetailPresenter(view, basketRepository, mockk(relaxed = true)) - } - - @Test - internal fun 프레젠터가_초기화될_때_상품_정보를_보여준다() { - // given - /* ... */ - - // when - /* init */ - - // then - verify(exactly = 1) { view.showProductImage(any()) } - verify(exactly = 1) { view.showProductName(any()) } - verify(exactly = 1) { view.showProductPrice(any()) } - } - - @Test - internal fun 장바구니에_상품을_추가한다() { - // given - /* ... */ - - // when - presenter.addBasketProduct() - - // then - verify(exactly = 1) { basketRepository.add(any()) } - verify(exactly = 1) { view.navigateToBasketScreen() } - } -} diff --git a/app/src/test/java/woowacourse/shopping/ui/shopping/ShoppingPresenterTest.kt b/app/src/test/java/woowacourse/shopping/ui/shopping/ShoppingPresenterTest.kt index d1dbbb9f1..94d8e9d27 100644 --- a/app/src/test/java/woowacourse/shopping/ui/shopping/ShoppingPresenterTest.kt +++ b/app/src/test/java/woowacourse/shopping/ui/shopping/ShoppingPresenterTest.kt @@ -5,37 +5,51 @@ import io.mockk.mockk import io.mockk.verify import org.junit.Before import org.junit.Test -import woowacourse.shopping.domain.repository.DomainProductRepository -import woowacourse.shopping.domain.repository.DomainRecentProductRepository +import woowacourse.shopping.domain.model.Price +import woowacourse.shopping.domain.model.Product +import woowacourse.shopping.domain.model.RecentProduct +import woowacourse.shopping.domain.model.RecentProducts +import woowacourse.shopping.domain.repository.CartRepository +import woowacourse.shopping.domain.repository.ProductRepository +import woowacourse.shopping.domain.repository.RecentProductRepository +import woowacourse.shopping.mapper.toDomain +import woowacourse.shopping.mapper.toUi +import woowacourse.shopping.model.UiPrice import woowacourse.shopping.model.UiProduct import woowacourse.shopping.model.UiRecentProduct internal class ShoppingPresenterTest { - private lateinit var presenter: ShoppingContract.Presenter private lateinit var view: ShoppingContract.View - private lateinit var productRepository: DomainProductRepository - private lateinit var recentProductRepository: DomainRecentProductRepository + private lateinit var productRepository: ProductRepository + private lateinit var recentProductRepository: RecentProductRepository + private lateinit var cartRepository: CartRepository @Before fun setUp() { view = mockk(relaxed = true) productRepository = mockk(relaxed = true) recentProductRepository = mockk(relaxed = true) - presenter = ShoppingPresenter(view, productRepository, recentProductRepository) + cartRepository = mockk(relaxed = true) + presenter = + ShoppingPresenter(view, productRepository, recentProductRepository, cartRepository) } @Test - internal fun 패치_올을_호출하면_제품과_최근_본_목록을_갱신한다() { + internal fun fetchAll_메서드를_호출하면_제품과_최근_본_목록을_갱신한다() { // given - every { productRepository.getPartially(any(), any()) } answers { listOf() } + every { recentProductRepository.getPartially(any()) } returns RecentProducts( + items = listOf( + RecentProduct(1, Product(1, "상품", Price(1000), "상품 이미지")) + ) + ) // when presenter.fetchAll() // then - verify(exactly = 1) { view.updateProducts(any()) } verify(exactly = 1) { view.hideLoadMoreButton() } + verify(exactly = 1) { view.updateRecentProducts(any()) } } @Test @@ -48,20 +62,29 @@ internal class ShoppingPresenterTest { // then verify(exactly = 1) { view.updateRecentProducts(any()) } - verify(exactly = 1) { view.showProductDetail(any()) } + verify(exactly = 1) { view.navigateToProductDetail(product, any()) } verify(exactly = 1) { recentProductRepository.add(any()) } } @Test internal fun 최근_제품_목록을_갱신한다() { // given - /* ... */ + val recentProducts = mockk(relaxed = true) + presenter = ShoppingPresenter( + view, + productRepository, + recentProductRepository, + cartRepository, + 10, + recentProducts, + ) // when presenter.fetchRecentProducts() // then - verify(exactly = 1) { view.updateRecentProducts(any()) } + val expected = recentProducts.getItems().toUi() + verify(exactly = 1) { view.updateRecentProducts(expected) } } @Test @@ -73,7 +96,7 @@ internal class ShoppingPresenterTest { presenter.inquiryRecentProductDetail(recentProduct) // then - verify(exactly = 1) { view.showProductDetail(any()) } + verify(exactly = 1) { view.navigateToProductDetail(any(), any()) } verify(exactly = 1) { recentProductRepository.add(any()) } } @@ -83,9 +106,36 @@ internal class ShoppingPresenterTest { /* ... */ // when - presenter.openBasket() + presenter.navigateToCart() // then - verify(exactly = 1) { view.navigateToBasketScreen() } + verify(exactly = 1) { view.navigateToCart() } + } + + @Test + internal fun 장바구니_화면으로_이동한다() { + // given + /* ... */ + + // when + presenter.navigateToCart() + + // then + verify(exactly = 1) { view.navigateToCart() } + } + + @Test + internal fun `제품_개수를_증가시킨다`() { + // given + val product = UiProduct(0, "제품", UiPrice(1000), "") + val count = 3 + + // when + presenter.increaseCartCount(product, count) + + // then + verify(exactly = 1) { cartRepository.increaseCartCount(product.toDomain(), count) } + verify(exactly = 1) { view.updateCartBadge(any()) } + verify(exactly = 1) { view.updateProducts(any()) } } } diff --git a/domain/src/main/java/woowacourse/shopping/domain/Basket.kt b/domain/src/main/java/woowacourse/shopping/domain/Basket.kt deleted file mode 100644 index 0d9a8f679..000000000 --- a/domain/src/main/java/woowacourse/shopping/domain/Basket.kt +++ /dev/null @@ -1,9 +0,0 @@ -package woowacourse.shopping.domain - -data class Basket(private val products: List) { - fun add(product: Product): Basket = - Basket(products + product) - - fun delete(product: Product): Basket = - Basket(products - product) -} diff --git a/domain/src/main/java/woowacourse/shopping/domain/BasketProduct.kt b/domain/src/main/java/woowacourse/shopping/domain/BasketProduct.kt deleted file mode 100644 index 7c4f97f5d..000000000 --- a/domain/src/main/java/woowacourse/shopping/domain/BasketProduct.kt +++ /dev/null @@ -1,6 +0,0 @@ -package woowacourse.shopping.domain - -data class BasketProduct( - val id: Int, - val product: Product, -) diff --git a/domain/src/main/java/woowacourse/shopping/domain/PageNumber.kt b/domain/src/main/java/woowacourse/shopping/domain/PageNumber.kt deleted file mode 100644 index 8abb57d53..000000000 --- a/domain/src/main/java/woowacourse/shopping/domain/PageNumber.kt +++ /dev/null @@ -1,29 +0,0 @@ -package woowacourse.shopping.domain - -typealias DomainPageNumber = PageNumber - -data class PageNumber( - val value: Int = DEFAULT_PAGE, - val sizePerPage: Int = DEFAULT_SIZE_PER_PAGE, -) { - init { - require(value >= DEFAULT_PAGE) { INVALID_PAGE_NUMBER_ERROR_MESSAGE } - } - - fun hasPrevious(): Boolean = value > MIN_PAGE - - operator fun inc(): PageNumber = - copy(value = value + 1) - - operator fun dec(): PageNumber = - copy(value = (value - 1).coerceAtLeast(MIN_PAGE)) - - companion object { - private const val DEFAULT_PAGE = 1 - private const val DEFAULT_SIZE_PER_PAGE = 5 - private const val MIN_PAGE = 1 - - private const val INVALID_PAGE_NUMBER_ERROR_MESSAGE = - "페이지 번호는 1 이상의 정수만 가능합니다." - } -} diff --git a/domain/src/main/java/woowacourse/shopping/domain/Products.kt b/domain/src/main/java/woowacourse/shopping/domain/Products.kt deleted file mode 100644 index 780e9d437..000000000 --- a/domain/src/main/java/woowacourse/shopping/domain/Products.kt +++ /dev/null @@ -1,30 +0,0 @@ -package woowacourse.shopping.domain - -typealias DomainProducts = Products - -data class Products( - private val items: List = emptyList(), - private val loadUnit: Int = DEFAULT_LOAD_AT_ONCE, -) { - val lastId: Int - get() = items.maxOfOrNull { it.id } ?: -1 - - fun addAll(newItems: List): Products = copy(items = items + newItems) - - fun add(newItem: Product): Products = copy(items = items + newItem) - - fun canLoadMore(): Boolean = - items.size >= loadUnit && (items.size % loadUnit >= 1 || loadUnit == 1 && items.size > loadUnit) - - fun getItems(): List = items.toList() - - fun getItemsByUnit(): List = items.take( - (items.size / loadUnit).coerceAtLeast(1) * loadUnit - ) - - operator fun plus(item: Product): Products = add(item) - - companion object { - private const val DEFAULT_LOAD_AT_ONCE = 20 - } -} diff --git a/domain/src/main/java/woowacourse/shopping/domain/model/Cart.kt b/domain/src/main/java/woowacourse/shopping/domain/model/Cart.kt new file mode 100644 index 000000000..55cc2423c --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/model/Cart.kt @@ -0,0 +1,62 @@ +package woowacourse.shopping.domain.model + +import woowacourse.shopping.domain.model.page.Page + +typealias DomainCart = Cart + +data class Cart( + val items: List = emptyList(), + val minProductSize: Int = 0, +) { + fun increaseProductCount(product: Product, count: Int = 1): Cart = + copy(items = items + .map { item -> if (item.product.id == product.id) item.plusCount(count) else item } + .distinctBy { it.product.id }) + + fun decreaseProductCount(product: Product, count: Int = 1): Cart = + copy(items = items + .map { item -> if (item.canDecreaseCount(product)) item.minusCount(count) else item } + .filter { it.selectedCount.value >= minProductSize } + .distinctBy { it.product.id }) + + private fun CartProduct.canDecreaseCount(product: Product): Boolean = + this.product.id == product.id && selectedCount.value > minProductSize + + fun select(product: Product): Cart = + copy(items = items.map { item -> + if (item.product.id == product.id) item.select() else item + }) + + fun unselect(product: Product): Cart = + copy(items = items.map { item -> + if (item.product.id == product.id) item.unselect() else item + }) + + fun selectAll(page: Page): Cart { + val cartProductsOfPage = page.takeItems(this) + return copy(items = items.map { item -> + cartProductsOfPage.find { it.id == item.id }?.select() ?: item + }) + } + + fun unselectAll(page: Page): Cart { + val cartProductsOfPage = page.takeItems(this) + return copy(items = items.map { item -> + cartProductsOfPage.find { it.id == item.id }?.unselect() ?: item + }) + } + + fun update(cart: Cart): Cart = + copy(items = cart.items.distinctBy { it.product.id }) + + fun update(cartProducts: List): Cart = + copy(items = cartProducts.distinctBy { it.product.id }) + + fun getCheckedProductTotalPrice(): Int = items.sumOf { it.getTotalPrice(true) } + + operator fun plus(items: Cart): Cart = + copy(items = (this.items + items.items).distinctBy { it.product.id }) + + operator fun plus(items: List): Cart = + copy(items = (this.items + items).distinctBy { it.product.id }) +} diff --git a/domain/src/main/java/woowacourse/shopping/domain/model/CartEntity.kt b/domain/src/main/java/woowacourse/shopping/domain/model/CartEntity.kt new file mode 100644 index 000000000..79bbe6cce --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/model/CartEntity.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.domain.model + +typealias DomainCartEntity = CartEntity + +class CartEntity( + val id: Int, + val productId: Int, + val count: Int, + val checked: Boolean, +) diff --git a/domain/src/main/java/woowacourse/shopping/domain/model/CartProduct.kt b/domain/src/main/java/woowacourse/shopping/domain/model/CartProduct.kt new file mode 100644 index 000000000..cf3d79fdb --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/model/CartProduct.kt @@ -0,0 +1,31 @@ +package woowacourse.shopping.domain.model + +typealias DomainCartProduct = CartProduct + +data class CartProduct( + val id: Int = 0, + val product: Product, + val selectedCount: ProductCount = ProductCount(0), + val isChecked: Boolean, +) { + val productId: Int = product.id + + fun plusCount(count: Int = 1): CartProduct = + copy(selectedCount = selectedCount + count) + + fun minusCount(count: Int = 1): CartProduct = + copy(selectedCount = selectedCount - count) + + fun select(): CartProduct = + copy(isChecked = true) + + fun unselect(): CartProduct = + copy(isChecked = false) + + fun getTotalPrice(onlyChecked: Boolean): Int { + if (onlyChecked && isChecked) { + return product.price.value * selectedCount.value + } + return 0 + } +} diff --git a/domain/src/main/java/woowacourse/shopping/domain/Price.kt b/domain/src/main/java/woowacourse/shopping/domain/model/Price.kt similarity index 87% rename from domain/src/main/java/woowacourse/shopping/domain/Price.kt rename to domain/src/main/java/woowacourse/shopping/domain/model/Price.kt index 63f2d2e66..59293a1db 100644 --- a/domain/src/main/java/woowacourse/shopping/domain/Price.kt +++ b/domain/src/main/java/woowacourse/shopping/domain/model/Price.kt @@ -1,4 +1,4 @@ -package woowacourse.shopping.domain +package woowacourse.shopping.domain.model data class Price(val value: Int) { init { diff --git a/domain/src/main/java/woowacourse/shopping/domain/Product.kt b/domain/src/main/java/woowacourse/shopping/domain/model/Product.kt similarity index 72% rename from domain/src/main/java/woowacourse/shopping/domain/Product.kt rename to domain/src/main/java/woowacourse/shopping/domain/model/Product.kt index 12b9ab783..f06d41f5f 100644 --- a/domain/src/main/java/woowacourse/shopping/domain/Product.kt +++ b/domain/src/main/java/woowacourse/shopping/domain/model/Product.kt @@ -1,4 +1,4 @@ -package woowacourse.shopping.domain +package woowacourse.shopping.domain.model data class Product( val id: Int, diff --git a/domain/src/main/java/woowacourse/shopping/domain/model/ProductCount.kt b/domain/src/main/java/woowacourse/shopping/domain/model/ProductCount.kt new file mode 100644 index 000000000..8e6fb2882 --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/model/ProductCount.kt @@ -0,0 +1,21 @@ +package woowacourse.shopping.domain.model + +data class ProductCount( + val value: Int, + private val minCount: Int = EMPTY_COUNT, +) { + operator fun plus(count: Int): ProductCount = copy(value = value + count) + + operator fun minus(count: Int): ProductCount = + copy(value = (value - count).coerceAtLeast(minCount)) + + operator fun plus(count: ProductCount): ProductCount = + copy(value = value + count.value) + + operator fun minus(count: ProductCount): ProductCount = + copy(value = (value - count.value).coerceAtLeast(minCount)) + + companion object { + private const val EMPTY_COUNT = 0 + } +} diff --git a/domain/src/main/java/woowacourse/shopping/domain/RecentProduct.kt b/domain/src/main/java/woowacourse/shopping/domain/model/RecentProduct.kt similarity index 64% rename from domain/src/main/java/woowacourse/shopping/domain/RecentProduct.kt rename to domain/src/main/java/woowacourse/shopping/domain/model/RecentProduct.kt index 772df4e09..cb5e544b6 100644 --- a/domain/src/main/java/woowacourse/shopping/domain/RecentProduct.kt +++ b/domain/src/main/java/woowacourse/shopping/domain/model/RecentProduct.kt @@ -1,4 +1,4 @@ -package woowacourse.shopping.domain +package woowacourse.shopping.domain.model data class RecentProduct( val id: Int = 0, diff --git a/domain/src/main/java/woowacourse/shopping/domain/RecentProducts.kt b/domain/src/main/java/woowacourse/shopping/domain/model/RecentProducts.kt similarity index 54% rename from domain/src/main/java/woowacourse/shopping/domain/RecentProducts.kt rename to domain/src/main/java/woowacourse/shopping/domain/model/RecentProducts.kt index 133ccc7c8..9bb6d3dea 100644 --- a/domain/src/main/java/woowacourse/shopping/domain/RecentProducts.kt +++ b/domain/src/main/java/woowacourse/shopping/domain/model/RecentProducts.kt @@ -1,10 +1,9 @@ -package woowacourse.shopping.domain +package woowacourse.shopping.domain.model -class RecentProducts( - _items: List = emptyList(), +data class RecentProducts( + private val items: List = emptyList(), private val maxCount: Int = 10, ) { - private val items: List = _items.take(maxCount) fun add(newItem: RecentProduct): RecentProducts { val newItems = items.toMutableList() @@ -14,7 +13,11 @@ class RecentProducts( return RecentProducts(newItems.take(maxCount), maxCount) } + fun update(newItems: RecentProducts): RecentProducts = copy(items = newItems.items) + + fun getLatest(): RecentProduct? = items.firstOrNull() + operator fun plus(newItem: RecentProduct): RecentProducts = add(newItem) - fun getItems(): List = items.map { it }.toList() + fun getItems(): List = items.take(maxCount).map { it.copy() } } diff --git a/domain/src/main/java/woowacourse/shopping/domain/model/page/LoadMore.kt b/domain/src/main/java/woowacourse/shopping/domain/model/page/LoadMore.kt new file mode 100644 index 000000000..bf83b918f --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/model/page/LoadMore.kt @@ -0,0 +1,29 @@ +package woowacourse.shopping.domain.model.page + +import woowacourse.shopping.domain.model.Cart +import woowacourse.shopping.domain.model.CartProduct +import woowacourse.shopping.domain.util.safeSubList + +typealias DomainLoadMore = LoadMore + +class LoadMore( + value: Int = 1, + sizePerPage: Int = 20, +) : Page(value, sizePerPage) { + override fun getStartPage(): Page = LoadMore(1, sizePerPage) + + override fun hasPrevious(): Boolean = value > 1 + + override fun hasNext(cart: Cart): Boolean = cart.items.size >= value * sizePerPage + + override fun next(): Page = LoadMore(value + 1, sizePerPage) + + override fun update(value: Int): Page = LoadMore(value, sizePerPage) + + override fun takeItems(cart: Cart): List = + cart.items.take(value * sizePerPage) + + override fun getCheckedProductSize(cart: Cart): Int = cart.items + .safeSubList(0, value * sizePerPage) + .count { item -> item.isChecked } +} diff --git a/domain/src/main/java/woowacourse/shopping/domain/model/page/Page.kt b/domain/src/main/java/woowacourse/shopping/domain/model/page/Page.kt new file mode 100644 index 000000000..66933b546 --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/model/page/Page.kt @@ -0,0 +1,38 @@ +package woowacourse.shopping.domain.model.page + +import woowacourse.shopping.domain.model.Cart +import woowacourse.shopping.domain.model.CartProduct + +typealias DomainPage = Page + +abstract class Page( + val value: Int = DEFAULT_PAGE, + val sizePerPage: Int = DEFAULT_SIZE_PER_PAGE, +) { + init { + require(value >= MIN_PAGE) { INVALID_PAGE_NUMBER_ERROR_MESSAGE } + } + + abstract fun getStartPage(): Page + + abstract fun hasPrevious(): Boolean + + abstract fun hasNext(cart: Cart): Boolean + + abstract fun next(): Page + + abstract fun update(value: Int): Page + + abstract fun takeItems(cart: Cart): List + + abstract fun getCheckedProductSize(cart: Cart): Int + + companion object { + private const val DEFAULT_PAGE = 1 + private const val DEFAULT_SIZE_PER_PAGE = 5 + + private const val MIN_PAGE = 1 + private const val INVALID_PAGE_NUMBER_ERROR_MESSAGE = + "페이지 번호는 1 이상의 정수만 가능합니다." + } +} diff --git a/domain/src/main/java/woowacourse/shopping/domain/model/page/Pagination.kt b/domain/src/main/java/woowacourse/shopping/domain/model/page/Pagination.kt new file mode 100644 index 000000000..5472479c4 --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/model/page/Pagination.kt @@ -0,0 +1,33 @@ +package woowacourse.shopping.domain.model.page + +import woowacourse.shopping.domain.model.Cart +import woowacourse.shopping.domain.model.CartProduct +import woowacourse.shopping.domain.util.safeSubList + +typealias DomainPagination = Pagination + +class Pagination( + value: Int = 1, + sizePerPage: Int = 5, +) : Page(value, sizePerPage) { + override fun getStartPage(): Page = Pagination(FIRST_PAGE, sizePerPage) + + override fun hasPrevious(): Boolean = value > FIRST_PAGE + + override fun hasNext(cart: Cart): Boolean = cart.items.size > sizePerPage * value + + override fun next(): Page = Pagination(value + 1, sizePerPage) + + override fun update(value: Int): Page = Pagination(value, sizePerPage) + + override fun takeItems(cart: Cart): List = + cart.items.safeSubList((value - 1) * sizePerPage, value * sizePerPage) + + override fun getCheckedProductSize(cart: Cart): Int = + takeItems(cart).count { item -> item.isChecked } + + + companion object { + private const val FIRST_PAGE = 1 + } +} diff --git a/domain/src/main/java/woowacourse/shopping/domain/repository/BasketRepository.kt b/domain/src/main/java/woowacourse/shopping/domain/repository/BasketRepository.kt deleted file mode 100644 index bb12793fe..000000000 --- a/domain/src/main/java/woowacourse/shopping/domain/repository/BasketRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package woowacourse.shopping.domain.repository - -import woowacourse.shopping.domain.PageNumber -import woowacourse.shopping.domain.Product - -typealias DomainBasketRepository = BasketRepository - -interface BasketRepository { - fun getPartially(page: PageNumber): List - fun add(product: Product) - fun remove(product: Product) -} diff --git a/domain/src/main/java/woowacourse/shopping/domain/repository/CartRepository.kt b/domain/src/main/java/woowacourse/shopping/domain/repository/CartRepository.kt new file mode 100644 index 000000000..823447cd7 --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/repository/CartRepository.kt @@ -0,0 +1,17 @@ +package woowacourse.shopping.domain.repository + +import woowacourse.shopping.domain.model.CartEntity +import woowacourse.shopping.domain.model.CartProduct +import woowacourse.shopping.domain.model.Product + +interface CartRepository { + fun getAllCartEntities(): List + fun getCartEntity(productId: Int): CartEntity + fun increaseCartCount(product: Product, count: Int) + fun decreaseCartCount(product: Product, count: Int) + fun deleteByProductId(productId: Int) + fun getProductInCartSize(): Int + fun update(cartProducts: List) + fun getCheckedProductCount(): Int + fun removeCheckedProducts() +} diff --git a/domain/src/main/java/woowacourse/shopping/domain/repository/ProductRepository.kt b/domain/src/main/java/woowacourse/shopping/domain/repository/ProductRepository.kt index 58b73b7a0..fa276857f 100644 --- a/domain/src/main/java/woowacourse/shopping/domain/repository/ProductRepository.kt +++ b/domain/src/main/java/woowacourse/shopping/domain/repository/ProductRepository.kt @@ -1,9 +1,9 @@ package woowacourse.shopping.domain.repository -import woowacourse.shopping.domain.Product - -typealias DomainProductRepository = ProductRepository +import woowacourse.shopping.domain.model.Product +import woowacourse.shopping.domain.model.page.Page interface ProductRepository { - fun getPartially(size: Int, startId: Int): List + fun getProductByPage(page: Page): List + fun findProductById(id: Int): Product? } diff --git a/domain/src/main/java/woowacourse/shopping/domain/repository/RecentProductRepository.kt b/domain/src/main/java/woowacourse/shopping/domain/repository/RecentProductRepository.kt index 1f82fd0ed..6796f02bc 100644 --- a/domain/src/main/java/woowacourse/shopping/domain/repository/RecentProductRepository.kt +++ b/domain/src/main/java/woowacourse/shopping/domain/repository/RecentProductRepository.kt @@ -1,10 +1,9 @@ package woowacourse.shopping.domain.repository -import woowacourse.shopping.domain.RecentProduct - -typealias DomainRecentProductRepository = RecentProductRepository +import woowacourse.shopping.domain.model.RecentProduct +import woowacourse.shopping.domain.model.RecentProducts interface RecentProductRepository { fun add(recentProduct: RecentProduct) - fun getPartially(size: Int): List + fun getPartially(size: Int): RecentProducts } diff --git a/domain/src/main/java/woowacourse/shopping/domain/util/ListExtension.kt b/domain/src/main/java/woowacourse/shopping/domain/util/ListExtension.kt new file mode 100644 index 000000000..a08e3f66d --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/domain/util/ListExtension.kt @@ -0,0 +1,9 @@ +package woowacourse.shopping.domain.util + +fun List.safeSubList(startIndex: Int, endIndex: Int): List = + if (startIndex < size) { + val safeEndIndex = if (endIndex < size) endIndex else size + subList(startIndex, safeEndIndex) + } else { + emptyList() + } diff --git a/domain/src/test/java/woowacourse/shopping/domain/BasketTest.kt b/domain/src/test/java/woowacourse/shopping/domain/BasketTest.kt deleted file mode 100644 index dd5bf648d..000000000 --- a/domain/src/test/java/woowacourse/shopping/domain/BasketTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package woowacourse.shopping.domain - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -class BasketTest { - @Test - fun `장바구니에 상품을 담는다`() { - val products = listOf() - val basket = Basket(products) - val product = Product(0, "새상품", Price(1000), "") - - val actual = basket.add(product) - val expected = Basket(products + product) - - assertThat(actual).isEqualTo(expected) - } - - @Test - fun `장바구니에 상품을 삭제한다`() { - val products = - listOf(Product(0, "새상품", Price(1000), ""), Product(1, "새상품", Price(1000), "")) - val basket = Basket(products) - val product = Product(0, "새상품", Price(1000), "") - - val actual = basket.delete(product) - val expected = Basket(products - product) - - assertThat(actual).isEqualTo(expected) - } -} diff --git a/domain/src/test/java/woowacourse/shopping/domain/PageNumberTest.kt b/domain/src/test/java/woowacourse/shopping/domain/PageNumberTest.kt deleted file mode 100644 index 47be9faf1..000000000 --- a/domain/src/test/java/woowacourse/shopping/domain/PageNumberTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -package woowacourse.shopping.domain - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.ValueSource - -internal class PageNumberTest { - - @ParameterizedTest - @ValueSource(ints = [0, -1, -2, -3, -4, -5]) - internal fun `페이지 번호가 1보다 작으면 예외가 발생한다`(number: Int) { - assertThrows { PageNumber(number) } - } - - @ParameterizedTest - @ValueSource(ints = [2, 3, 4, 5, 6, 100]) - internal fun `페이지 번호가 1보다 크면 이전 페이지가 존재한다`(number: Int) { - // given - val page = PageNumber(number) - - // when - val actual = page.hasPrevious() - - // then - assertThat(actual).isTrue - } - - @Test - internal fun `페이지 번호가 1이면 이전 페이지가 존재하지 않는다`() { - // given - val page = PageNumber(1) - - // when - val actual = page.hasPrevious() - - // then - assertThat(actual).isFalse - } - - @Test - internal fun `페이지 번호가 1이면, 감소 시켰을 때 더 이상 감소하지 않는다`() { - // given - val expected = PageNumber(1) - var page = PageNumber(1) - - // when - val actual = --page - - // then - assertThat(actual).isEqualTo(expected) - } - - @ParameterizedTest - @ValueSource(ints = [2, 3, 4, 5, 6, 100]) - internal fun `페이지 번호가 1보다 크면, 감소 시켰을 때 1만큼 감소한다`(currentNumber: Int) { - // given - var page = PageNumber(currentNumber) - val expected = PageNumber(currentNumber - 1) - - // when - val actual = --page - - // then - assertThat(actual).isEqualTo(expected) - } - - @ParameterizedTest - @ValueSource(ints = [2, 3, 4, 5, 6, 100]) - internal fun `페이지 번호를 증가시켰을 때, 1만큼 증가한다`(currentNumber: Int) { - // given - var page = PageNumber(currentNumber) - val expected = PageNumber(currentNumber + 1) - - // when - val actual = ++page - - // then - assertThat(actual).isEqualTo(expected) - } -} diff --git a/domain/src/test/java/woowacourse/shopping/domain/ProductsTest.kt b/domain/src/test/java/woowacourse/shopping/domain/ProductsTest.kt deleted file mode 100644 index bcf739351..000000000 --- a/domain/src/test/java/woowacourse/shopping/domain/ProductsTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package woowacourse.shopping.domain - -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import org.junit.jupiter.params.provider.ValueSource - -internal class ProductsTest { - @ParameterizedTest - @ValueSource(ints = [1, 10, 100]) - internal fun `마지막 아이디를_반환한다`(count: Int) { - // given - val items = List(count) { id -> Product(id + 1, "상품 $id", Price(1000), "") } - val products = Products(items) - - // when - val actual = products.lastId - - // then - assertThat(actual).isEqualTo(count) - } - - @ParameterizedTest - @CsvSource("2, 1", "15, 10", "101, 100") - internal fun `아이템_개수가_로딩_단위_보다_많으면_데이터를_더_불러올_수_있는_상태이다`(itemCount: Int, loadUnit: Int) { - // given - val items = List(itemCount) { id -> Product(id + 1, "상품 $id", Price(1000), "") } - val products = Products(items, loadUnit) - - // when - val actual = products.canLoadMore() - - // then - assertThat(actual).isTrue - } - - @ParameterizedTest - @CsvSource("5, 2, 4", "15, 10, 10", "101, 20, 100") - internal fun `아이템을_로딩_단위별로_가져온다`(itemCount: Int, loadUnit: Int, expected: Int) { - // given - val items = List(itemCount) { id -> Product(id + 1, "상품 $id", Price(1000), "") } - val products = Products(items, loadUnit) - - // when - val actual = products.getItemsByUnit().size - - // then - assertThat(actual).isEqualTo(expected) - } -} diff --git a/domain/src/test/java/woowacourse/shopping/domain/fixture/CartFixture.kt b/domain/src/test/java/woowacourse/shopping/domain/fixture/CartFixture.kt new file mode 100644 index 000000000..e7955cfb8 --- /dev/null +++ b/domain/src/test/java/woowacourse/shopping/domain/fixture/CartFixture.kt @@ -0,0 +1,40 @@ +package woowacourse.shopping.domain.fixture + +import woowacourse.shopping.domain.model.Cart +import woowacourse.shopping.domain.model.CartProduct +import woowacourse.shopping.domain.model.Price +import woowacourse.shopping.domain.model.Product +import woowacourse.shopping.domain.model.ProductCount + +internal val ALL_UNCHECKED_CARTS: Cart = Cart( + items = List(100) { id -> + CartProduct( + id = id, + Product(id = id, name = "상품$id", price = Price(1000), imageUrl = ""), + selectedCount = ProductCount(1), + isChecked = false + ) + } +) + +internal val ALL_CHECKED_CARTS: Cart = Cart( + items = List(100) { id -> + CartProduct( + id = id, + Product(id = id, name = "상품$id", price = Price(1000), imageUrl = ""), + selectedCount = ProductCount(1), + isChecked = true + ) + } +) + +internal fun getCart(size: Int): Cart = Cart( + items = List(size) { id -> + CartProduct( + id = id, + Product(id = id, name = "상품$id", price = Price(1000), imageUrl = ""), + selectedCount = ProductCount(1), + isChecked = false + ) + } +) diff --git a/domain/src/test/java/woowacourse/shopping/domain/model/CartProductTest.kt b/domain/src/test/java/woowacourse/shopping/domain/model/CartProductTest.kt new file mode 100644 index 000000000..ee4878e58 --- /dev/null +++ b/domain/src/test/java/woowacourse/shopping/domain/model/CartProductTest.kt @@ -0,0 +1,123 @@ +package woowacourse.shopping.domain.model + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class CartProductTest { + @Test + internal fun `선택된 제품 개수를 증가시킨다`() { + // given + val product = Product(1, "상품", Price(1000), "") + val cartProduct = CartProduct(product = product, isChecked = false) + val expected = + CartProduct(product = product, selectedCount = ProductCount(2), isChecked = false) + + // when + val actual = cartProduct.plusCount(2) + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + internal fun `선택된 제품 개수를 감소시킨다`() { + // given + val product = Product(1, "상품", Price(1000), "") + val cartProduct = CartProduct( + product = product, + selectedCount = ProductCount(3), + isChecked = false + ) + val expected = CartProduct( + product = product, + selectedCount = ProductCount(1), + isChecked = false + ) + + // when + val actual = cartProduct.minusCount(2) + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + internal fun `제품을 선택한다`() { + // given + val product = Product(1, "상품", Price(1000), "") + val cartProduct = CartProduct( + product = product, + selectedCount = ProductCount(2), + isChecked = false + ) + val expected = CartProduct( + product = product, + selectedCount = ProductCount(2), + isChecked = true + ) + + // when + val actual = cartProduct.select() + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + internal fun `제품을 선택을 해제한다`() { + // given + val product = Product(1, "상품", Price(1000), "") + val cartProduct = CartProduct( + product = product, + selectedCount = ProductCount(2), + isChecked = true + ) + val expected = CartProduct( + product = product, + selectedCount = ProductCount(2), + isChecked = false + ) + + // when + val actual = cartProduct.unselect() + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + internal fun `체크된 제품의 개수와 가격을 곱하여 총 합을 반환한다`() { + // given + val product = Product(1, "상품", Price(1000), "") + val cartProduct = CartProduct( + product = product, + selectedCount = ProductCount(2), + isChecked = true + ) + val expected = 2000 + + // when + val actual = cartProduct.getTotalPrice(onlyChecked = true) + + // then + assertThat(actual).isEqualTo(expected) + } + + @Test + internal fun `체크되지 않은 제품은 총 가격에 포함하지 않는다`() { + // given + val product = Product(1, "상품", Price(1000), "") + val cartProduct = CartProduct( + product = product, + selectedCount = ProductCount(2), + isChecked = false + ) + val expected = 0 + + // when + val actual = cartProduct.getTotalPrice(onlyChecked = true) + + // then + assertThat(actual).isEqualTo(expected) + } +} diff --git a/domain/src/test/java/woowacourse/shopping/domain/model/CartTest.kt b/domain/src/test/java/woowacourse/shopping/domain/model/CartTest.kt new file mode 100644 index 000000000..1da133a22 --- /dev/null +++ b/domain/src/test/java/woowacourse/shopping/domain/model/CartTest.kt @@ -0,0 +1,34 @@ +package woowacourse.shopping.domain.model + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import woowacourse.shopping.domain.model.Cart +import woowacourse.shopping.domain.model.Price +import woowacourse.shopping.domain.model.Product + +class CartTest { +// @Test +// fun `장바구니에 상품을 담는다`() { +// val products = listOf() +// val cart = Cart(products) +// val product = Product(0, "새상품", Price(1000), "") +// +// val actual = cart.increaseProductCount(product) +// val expected = Cart(products + product) +// +// assertThat(actual).isEqualTo(expected) +// } +// +// @Test +// fun `장바구니에 상품을 삭제한다`() { +// val products = +// listOf(Product(0, "새상품", Price(1000), ""), Product(1, "새상품", Price(1000), "")) +// val cart = Cart(products) +// val product = Product(0, "새상품", Price(1000), "") +// +// val actual = cart.delete(product) +// val expected = Cart(products - product) +// +// assertThat(actual).isEqualTo(expected) +// } +} diff --git a/domain/src/test/java/woowacourse/shopping/domain/model/LoadMoreTest.kt b/domain/src/test/java/woowacourse/shopping/domain/model/LoadMoreTest.kt new file mode 100644 index 000000000..dc05d4802 --- /dev/null +++ b/domain/src/test/java/woowacourse/shopping/domain/model/LoadMoreTest.kt @@ -0,0 +1,94 @@ +package woowacourse.shopping.domain.model + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import woowacourse.shopping.domain.fixture.ALL_CHECKED_CARTS +import woowacourse.shopping.domain.fixture.ALL_UNCHECKED_CARTS +import woowacourse.shopping.domain.model.page.LoadMore + +internal class LoadMoreTest { + @Test + internal fun `첫 번째 페이지를 반환한다`() { + // given + val page = LoadMore(3) + + // when + val actual = page.getStartPage() + + // then + assertThat(actual).usingRecursiveComparison().isEqualTo(LoadMore(1)) + } + + @ParameterizedTest + @ValueSource(ints = [0, -1, -2, -3, -4, -5]) + internal fun `페이지 번호가 1보다 작으면 예외가 발생한다`(number: Int) { + assertThrows { LoadMore(number) } + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 100]) + internal fun `페이지 번호가 1보다 크면 이전 페이지가 존재한다`(number: Int) { + // given + val page = LoadMore(number) + + // when + val actual = page.hasPrevious() + + // then + assertThat(actual).isTrue + } + + @Test + internal fun `페이지 번호가 1이면 이전 페이지가 존재하지 않는다`() { + // given + val page = LoadMore(1) + + // when + val actual = page.hasPrevious() + + // then + assertThat(actual).isFalse + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 100]) + internal fun `페이지 번호를 증가시켰을 때, 1만큼 증가한다`(currentNumber: Int) { + // given + val page = LoadMore(currentNumber) + val expected = LoadMore(currentNumber + 1) + + // when + val actual = page.next() + + // then + assertThat(actual).usingRecursiveComparison().isEqualTo(expected) + } + + @Test + internal fun `페이지에 해당하는 상품 목록을 가져온다`() { + // given + val page = LoadMore(2, 5) + + // when + val actual = page.takeItems(ALL_UNCHECKED_CARTS) + + // then + assertThat(actual).isEqualTo(ALL_UNCHECKED_CARTS.items.take(10)) + } + + @Test + internal fun `체크된 모든 상품 목록 개수를 반환한다`() { + // given + val page = LoadMore(2, 30) + val carts = ALL_CHECKED_CARTS + + // when + val actual = page.getCheckedProductSize(carts) + + // then + assertThat(actual).isEqualTo(60) + } +} diff --git a/domain/src/test/java/woowacourse/shopping/domain/model/PaginationTest.kt b/domain/src/test/java/woowacourse/shopping/domain/model/PaginationTest.kt new file mode 100644 index 000000000..0b3bef987 --- /dev/null +++ b/domain/src/test/java/woowacourse/shopping/domain/model/PaginationTest.kt @@ -0,0 +1,122 @@ +package woowacourse.shopping.domain.model + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import woowacourse.shopping.domain.fixture.ALL_CHECKED_CARTS +import woowacourse.shopping.domain.fixture.ALL_UNCHECKED_CARTS +import woowacourse.shopping.domain.fixture.getCart +import woowacourse.shopping.domain.model.page.Pagination +import woowacourse.shopping.domain.util.safeSubList + +internal class PaginationTest { + @Test + internal fun `첫 번째 페이지를 반환한다`() { + // given + val page = Pagination(3) + + // when + val actual = page.getStartPage() + + // then + assertThat(actual).usingRecursiveComparison().isEqualTo(Pagination(1)) + } + + @ParameterizedTest + @ValueSource(ints = [0, -1, -2, -3, -4, -5]) + internal fun `페이지 번호가 1보다 작으면 예외가 발생한다`(number: Int) { + assertThrows { Pagination(number) } + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 100]) + internal fun `페이지 번호가 1보다 크면 이전 페이지가 존재한다`(number: Int) { + // given + val page = Pagination(number) + + // when + val actual = page.hasPrevious() + + // then + assertThat(actual).isTrue + } + + @Test + internal fun `페이지 번호가 1이면 이전 페이지가 존재하지 않는다`() { + // given + val page = Pagination(1) + + // when + val actual = page.hasPrevious() + + // then + assertThat(actual).isFalse + } + + @Test + internal fun `총 카트 아이템 개수가 페이지가 수용 가능한 크기보다 크면, 다음 페이지가 존재한다`() { + // given + val page = Pagination(1, 5) + val cart = ALL_UNCHECKED_CARTS + + // when + val actual = page.hasNext(cart) + + // then + assertThat(actual).isTrue + } + + @Test + internal fun `총 카트 아이템 개수가 페이지가 수용 가능한 크기보다 적으면, 다음 페이지가 존재하지 않는다`() { + // given + val page = Pagination(1, 5) + val cart = getCart(3) + + // when + val actual = page.hasNext(cart) + + // then + assertThat(actual).isFalse() + } + + @ParameterizedTest + @ValueSource(ints = [2, 3, 4, 5, 6, 100]) + internal fun `페이지 번호를 증가시켰을 때, 1만큼 증가한다`(currentNumber: Int) { + // given + val page = Pagination(currentNumber) + val expected = Pagination(currentNumber + 1) + + // when + val actual = page.next() + + // then + assertThat(actual).usingRecursiveComparison().isEqualTo(expected) + } + + @Test + internal fun `페이지에 해당하는 상품 목록을 가져온다`() { + // given + val page = Pagination(2, 5) + + // when + val actual = page.takeItems(ALL_UNCHECKED_CARTS) + + // then + assertThat(actual).isEqualTo(ALL_UNCHECKED_CARTS.items.safeSubList(5, 10)) + } + + @Test + internal fun `현재 페이지에 체크된 모든 상품 목록 개수를 반환한다`() { + // given + val page = Pagination(2, 30) + val carts = ALL_CHECKED_CARTS + + // when + val actual = page.getCheckedProductSize(carts) + + // then + assertThat(actual).isEqualTo(30) + } +} diff --git a/domain/src/test/java/woowacourse/shopping/domain/PriceTest.kt b/domain/src/test/java/woowacourse/shopping/domain/model/PriceTest.kt similarity index 71% rename from domain/src/test/java/woowacourse/shopping/domain/PriceTest.kt rename to domain/src/test/java/woowacourse/shopping/domain/model/PriceTest.kt index 25f6cacc7..10df9e05c 100644 --- a/domain/src/test/java/woowacourse/shopping/domain/PriceTest.kt +++ b/domain/src/test/java/woowacourse/shopping/domain/model/PriceTest.kt @@ -1,7 +1,8 @@ -package woowacourse.shopping.domain +package woowacourse.shopping.domain.model import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import woowacourse.shopping.domain.model.Price class PriceTest { @Test diff --git a/domain/src/test/java/woowacourse/shopping/domain/model/ProductCountTest.kt b/domain/src/test/java/woowacourse/shopping/domain/model/ProductCountTest.kt new file mode 100644 index 000000000..2f9fbe6de --- /dev/null +++ b/domain/src/test/java/woowacourse/shopping/domain/model/ProductCountTest.kt @@ -0,0 +1,30 @@ +package woowacourse.shopping.domain.model + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +class ProductCountTest { + @Test + internal fun `제품 개수를 증가시킨다`() { + // given + val productCount = ProductCount(1, 1) + + // when + val actual = productCount.plus(2) + + // then + assertEquals(actual, ProductCount(3, 1)) + } + + @Test + internal fun `제품 개수를 감소시킬 때 최소 개수보다 줄어들 수 없다`() { + // given + val productCount = ProductCount(5, 1) + + // when + val actual = productCount.minus(10) + + // then + assertEquals(actual, ProductCount(1, 1)) + } +} diff --git a/domain/src/test/java/woowacourse/shopping/domain/RecentProductsTest.kt b/domain/src/test/java/woowacourse/shopping/domain/model/RecentProductsTest.kt similarity index 55% rename from domain/src/test/java/woowacourse/shopping/domain/RecentProductsTest.kt rename to domain/src/test/java/woowacourse/shopping/domain/model/RecentProductsTest.kt index e97e71a29..4f39f3f4c 100644 --- a/domain/src/test/java/woowacourse/shopping/domain/RecentProductsTest.kt +++ b/domain/src/test/java/woowacourse/shopping/domain/model/RecentProductsTest.kt @@ -1,6 +1,8 @@ -package woowacourse.shopping.domain +package woowacourse.shopping.domain.model import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource @@ -21,4 +23,22 @@ internal class RecentProductsTest { val actual = recentProducts.getItems().size Assertions.assertThat(actual).isEqualTo(addCount.coerceAtMost(maxCount)) } + + @Test + internal fun `마지막에 본 상품을 반환한다`() { + // given + var recentProducts = RecentProducts(maxCount = 10) + + (1..10).forEach { id -> + val item = RecentProduct(0, Product(id, "상품 $id", Price(1000), "")) + recentProducts = recentProducts.add(item) + } + val expected = RecentProduct(0, Product(10, "상품 10", Price(1000), "")) + + // when + val actual = recentProducts.getLatest() + + // then + assertThat(actual).isEqualTo(expected) + } }