diff --git a/README.md b/README.md index b0d9ac6dd..58eb5a2c6 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ -# android-shopping-cart \ No newline at end of file +# android-shopping-cart +## Domain +### ProductRepository +- [ ] 상품 목록을 가져올 수 있다. +### CartRepository +- [ ] 장바구니의 상품 목록을 가져올 수 있다. +- [ ] 장바구니에 상품을 추가할 수 있다. +- [ ] 장바구니에 상품을 삭제할 수 있다. +### RecentViewedRepository +- [ ] 사용자가 상품의 상세 정보를 조회하면 목록에 추가한다. +- [ ] 만약 10개가 넘어갔을 경우 가장 오래 된 상품을 삭제한다. +- [ ] 최근 본 상품 목록을 가져올 수 있다. +### Product +- name : 이름 +- imageUrl : 이미지 URL +- price : 가격 +### Price +- price : 가격 +## View +- [ ] 앱이 종료돼도 최근 본 상품 목록과 장바구니 데이터는 유지돼야 한다. +### ProductListActivity +- [ ] 상품을 클릭하면 상품 상세로 이동한다. +- [ ] 최근 본 상품이 있는 경우 상품 목록 상단에서 10개까지 확인할 수 있다. +- [X] 툴바 안의 카트 버튼을 누르면 장바구니로 이동한다. +### ProductDetailActivity +- [ ] 사용자는 상품을 장바구니에 추가할 수 있다. +### CartActivity +- [ ] 장바구니에서 원하는 상품을 삭제할 수 있다. +- [X] 툴바 안의 백버튼을 누르면 뒤로 이동한다. + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 15de5c381..9f9ee2b22 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("kotlin-parcelize") } android { @@ -22,7 +23,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -33,6 +34,11 @@ android { kotlinOptions { jvmTarget = "11" } + + buildFeatures { + dataBinding = true + viewBinding = true + } } dependencies { @@ -40,7 +46,10 @@ dependencies { implementation("androidx.appcompat:appcompat:1.6.0") implementation("com.google.android.material:material:1.7.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") + implementation(project(mapOf("path" to ":domain"))) testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("com.github.bumptech.glide:glide:4.15.1") + testImplementation("io.mockk:mockk:1.13.5") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f67a04baa..3505b5dd3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + diff --git a/app/src/main/java/woowacourse/shopping/MainActivity.kt b/app/src/main/java/woowacourse/shopping/MainActivity.kt deleted file mode 100644 index c153e45d9..000000000 --- a/app/src/main/java/woowacourse/shopping/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package woowacourse.shopping - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/java/woowacourse/shopping/data/CartDbRepository.kt b/app/src/main/java/woowacourse/shopping/data/CartDbRepository.kt new file mode 100644 index 000000000..15cf64b56 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/CartDbRepository.kt @@ -0,0 +1,38 @@ +package woowacourse.shopping.data + +import android.content.Context +import woowacourse.shopping.data.db.CartDBHelper +import woowacourse.shopping.domain.CartProduct +import woowacourse.shopping.domain.CartRepository + +class CartDbRepository(context: Context) : CartRepository { + private val dbHelper = CartDBHelper(context) + override fun findAll(): List { + return dbHelper.selectAll() + } + + private fun find(id: Int): CartProduct? { + return dbHelper.selectWhereId(id) + } + + override fun add(id: Int, count: Int) { + val cardProduct = find(id) + if (cardProduct != null) { + dbHelper.update(id, count + cardProduct.count) + return + } + dbHelper.insert(id, count) + } + + override fun remove(id: Int) { + dbHelper.remove(id) + } + + override fun findRange(mark: Int, rangeSize: Int): List { + return dbHelper.selectRange(mark, rangeSize) + } + + override fun isExistByMark(mark: Int): Boolean { + return dbHelper.getSize(mark) + } +} diff --git a/app/src/main/java/woowacourse/shopping/data/ProductMockRepository.kt b/app/src/main/java/woowacourse/shopping/data/ProductMockRepository.kt new file mode 100644 index 000000000..a03b2c4a7 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/ProductMockRepository.kt @@ -0,0 +1,286 @@ +package woowacourse.shopping.data + +import woowacourse.shopping.domain.Price +import woowacourse.shopping.domain.Product +import woowacourse.shopping.domain.ProductRepository + +object ProductMockRepository : ProductRepository { + private val products = listOf( + Product( + 0, + "락토핏", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/6769030628798948-183ad194-f24c-44e6-b92f-1ed198b347cd.jpg", + Price( + 10000, + ), + ), + Product( + 1, + "현미밥", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1237954167000478-5b27108a-ee70-4e14-b605-181191a57bcb.jpg", + Price( + 10000, + ), + ), + Product( + 2, + "헛개차", + "https://thumbnail9.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2418649993082166-2bfb64be-78dc-4c05-a2e3-1749f856fef8.jpg", + Price( + 10000, + ), + ), + Product( + 3, + "키", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1721669748108539-877f91ca-5964-4761-b3e0-bff7b970c31c.jpg", + Price( + 10000, + ), + ), + Product( + 4, + "닭가슴살", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2700754094560515-ebc4cbaa-4c4f-4750-8b41-2e6ae5ab26ed.jpg", + Price( + 10000, + ), + ), + Product( + 5, + "enffl", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/6769030628798948-183ad194-f24c-44e6-b92f-1ed198b347cd.jpg", + Price( + 10000, + ), + ), + Product( + 6, + "뽀또", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1237954167000478-5b27108a-ee70-4e14-b605-181191a57bcb.jpg", + Price( + 10000, + ), + ), + Product( + 7, + "둘리", + "https://thumbnail9.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2418649993082166-2bfb64be-78dc-4c05-a2e3-1749f856fef8.jpg", + Price( + 10000, + ), + ), + Product( + 8, + "안녕", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1721669748108539-877f91ca-5964-4761-b3e0-bff7b970c31c.jpg", + Price( + 10000, + ), + ), + Product( + 9, + "9", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2700754094560515-ebc4cbaa-4c4f-4750-8b41-2e6ae5ab26ed.jpg", + Price( + 10000, + ), + ), + Product( + 10, + "10", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/6769030628798948-183ad194-f24c-44e6-b92f-1ed198b347cd.jpg", + Price( + 10000, + ), + ), + Product( + 11, + "11", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1237954167000478-5b27108a-ee70-4e14-b605-181191a57bcb.jpg", + Price( + 10000, + ), + ), + Product( + 12, + "12", + "https://thumbnail9.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2418649993082166-2bfb64be-78dc-4c05-a2e3-1749f856fef8.jpg", + Price( + 10000, + ), + ), + Product( + 13, + "13", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1721669748108539-877f91ca-5964-4761-b3e0-bff7b970c31c.jpg", + Price( + 10000, + ), + ), + Product( + 14, + "14", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2700754094560515-ebc4cbaa-4c4f-4750-8b41-2e6ae5ab26ed.jpg", + Price( + 10000, + ), + ), + Product( + 15, + "15", + "https://thumbnail9.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2418649993082166-2bfb64be-78dc-4c05-a2e3-1749f856fef8.jpg", + Price( + 10000, + ), + ), + Product( + 16, + "16", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1721669748108539-877f91ca-5964-4761-b3e0-bff7b970c31c.jpg", + Price( + 10000, + ), + ), + Product( + 17, + "17", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2700754094560515-ebc4cbaa-4c4f-4750-8b41-2e6ae5ab26ed.jpg", + Price( + 10000, + ), + ), + Product( + 18, + "18", + "https://thumbnail9.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2418649993082166-2bfb64be-78dc-4c05-a2e3-1749f856fef8.jpg", + Price( + 10000, + ), + ), + Product( + 19, + "19", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1721669748108539-877f91ca-5964-4761-b3e0-bff7b970c31c.jpg", + Price( + 10000, + ), + ), + Product( + 20, + "20", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2700754094560515-ebc4cbaa-4c4f-4750-8b41-2e6ae5ab26ed.jpg", + Price( + 10000, + ), + ), + Product( + 21, + "21", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/6769030628798948-183ad194-f24c-44e6-b92f-1ed198b347cd.jpg", + Price( + 10000, + ), + ), + Product( + 22, + "22", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1237954167000478-5b27108a-ee70-4e14-b605-181191a57bcb.jpg", + Price( + 10000, + ), + ), + Product( + 23, + "23", + "https://thumbnail9.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2418649993082166-2bfb64be-78dc-4c05-a2e3-1749f856fef8.jpg", + Price( + 10000, + ), + ), + Product( + 24, + "24", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1721669748108539-877f91ca-5964-4761-b3e0-bff7b970c31c.jpg", + Price( + 10000, + ), + ), + Product( + 25, + "25", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2700754094560515-ebc4cbaa-4c4f-4750-8b41-2e6ae5ab26ed.jpg", + Price( + 10000, + ), + ), + Product( + 26, + "26", + "https://thumbnail9.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2418649993082166-2bfb64be-78dc-4c05-a2e3-1749f856fef8.jpg", + Price( + 10000, + ), + ), + Product( + 27, + "27", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1721669748108539-877f91ca-5964-4761-b3e0-bff7b970c31c.jpg", + Price( + 10000, + ), + ), + Product( + 28, + "28", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2700754094560515-ebc4cbaa-4c4f-4750-8b41-2e6ae5ab26ed.jpg", + Price( + 10000, + ), + ), + Product( + 29, + "29", + "https://thumbnail9.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2418649993082166-2bfb64be-78dc-4c05-a2e3-1749f856fef8.jpg", + Price( + 10000, + ), + ), + Product( + 30, + "30", + "https://thumbnail6.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/1721669748108539-877f91ca-5964-4761-b3e0-bff7b970c31c.jpg", + Price( + 10000, + ), + ), + Product( + 31, + "31", + "https://thumbnail7.coupangcdn.com/thumbnails/remote/230x230ex/image/retail/images/2700754094560515-ebc4cbaa-4c4f-4750-8b41-2e6ae5ab26ed.jpg", + Price( + 10000, + ), + ), + + ) + + override fun findAll(): List { + return products + } + + override fun find(id: Int): Product { + return products[id] + } + + override fun findRange(start: Int, size: Int): List { + if (products.size <= start + size) { + return products.subList(start, products.lastIndex) + } + return products.subList(start, start + size) + } + + override fun isExistByMark(mark: Int): Boolean { + return products.size > mark + } +} diff --git a/app/src/main/java/woowacourse/shopping/data/RecentViewedDbRepository.kt b/app/src/main/java/woowacourse/shopping/data/RecentViewedDbRepository.kt new file mode 100644 index 000000000..c3d695680 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/RecentViewedDbRepository.kt @@ -0,0 +1,32 @@ +package woowacourse.shopping.data + +import android.content.Context +import woowacourse.shopping.data.db.RecentViewedDBHelper +import woowacourse.shopping.domain.RecentViewedRepository + +class RecentViewedDbRepository(context: Context) : + RecentViewedRepository { + private val dbHelper = RecentViewedDBHelper(context) + + override fun findAll(): List { + return dbHelper.selectAll() + } + + override fun add(id: Int) { + if (find(id) != null) { + dbHelper.remove(id) + } + if (findAll().size == 10) { + dbHelper.removeOldest() + } + dbHelper.insert(id) + } + + private fun find(id: Int): Int? { + return dbHelper.selectWhereId(id) + } + + override fun remove(id: Int) { + return dbHelper.remove(id) + } +} diff --git a/app/src/main/java/woowacourse/shopping/data/db/CartConstract.kt b/app/src/main/java/woowacourse/shopping/data/db/CartConstract.kt new file mode 100644 index 000000000..13c455c12 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/db/CartConstract.kt @@ -0,0 +1,9 @@ +package woowacourse.shopping.data.db + +import android.provider.BaseColumns + +object CartConstract : BaseColumns { + const val TABLE_NAME = "cart" + const val TABLE_COLUMN_ID = "id" + const val TABLE_COLUMN_COUNT = "count" +} diff --git a/app/src/main/java/woowacourse/shopping/data/db/CartDBHelper.kt b/app/src/main/java/woowacourse/shopping/data/db/CartDBHelper.kt new file mode 100644 index 000000000..4cfa4aa0e --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/db/CartDBHelper.kt @@ -0,0 +1,81 @@ +package woowacourse.shopping.data.db + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import woowacourse.shopping.domain.CartProduct + +class CartDBHelper(context: Context) : SQLiteOpenHelper(context, "cart", null, 1) { + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL( + "CREATE TABLE ${CartConstract.TABLE_NAME} (" + + " ${CartConstract.TABLE_COLUMN_ID} Int PRIMARY KEY not null," + + " ${CartConstract.TABLE_COLUMN_COUNT} Int not null" + + ");", + ) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL("DROP TABLE IF EXISTS ${CartConstract.TABLE_NAME}") + onCreate(db) + } + + fun insert(id: Int, count: Int) { + val values = ContentValues() + values.put(CartConstract.TABLE_COLUMN_ID, id) + values.put(CartConstract.TABLE_COLUMN_COUNT, count) + writableDatabase.insert(CartConstract.TABLE_NAME, null, values) + } + + fun update(id: Int, count: Int) { + writableDatabase.execSQL("UPDATE ${CartConstract.TABLE_NAME} SET ${CartConstract.TABLE_COLUMN_COUNT}=$count WHERE ${CartConstract.TABLE_COLUMN_ID}=$id") + } + + fun remove(id: Int) { + writableDatabase.execSQL("DELETE FROM ${CartConstract.TABLE_NAME} WHERE ${CartConstract.TABLE_COLUMN_ID}=$id") + } + + fun selectAll(): List { + val products = mutableListOf() + val sql = "select * from ${CartConstract.TABLE_NAME}" + val cursor = readableDatabase.rawQuery(sql, null) + while (cursor.moveToNext()) { + val id = cursor.getInt(cursor.getColumnIndexOrThrow(CartConstract.TABLE_COLUMN_ID)) + val count = cursor.getInt(cursor.getColumnIndexOrThrow(CartConstract.TABLE_COLUMN_COUNT)) + products.add(CartProduct(id, count)) + } + cursor.close() + return products + } + + fun selectWhereId(id: Int): CartProduct? { + val sql = "select * from ${CartConstract.TABLE_NAME} where ${CartConstract.TABLE_COLUMN_ID}=$id" + val cursor = readableDatabase.rawQuery(sql, null) + while (cursor.moveToNext()) { + val id = cursor.getInt(cursor.getColumnIndexOrThrow(CartConstract.TABLE_COLUMN_ID)) + val count = cursor.getInt(cursor.getColumnIndexOrThrow(CartConstract.TABLE_COLUMN_COUNT)) + cursor.close() + return CartProduct(id, count) + } + return null + } + + fun selectRange(mark: Int, rangeSize: Int): List { + val products = mutableListOf() + val sql = "select * from ${CartConstract.TABLE_NAME} limit $rangeSize offset $mark;" + val cursor = readableDatabase.rawQuery(sql, null) + while (cursor.moveToNext()) { + val id = cursor.getInt(cursor.getColumnIndexOrThrow(CartConstract.TABLE_COLUMN_ID)) + val count = cursor.getInt(cursor.getColumnIndexOrThrow(CartConstract.TABLE_COLUMN_COUNT)) + products.add(CartProduct(id, count)) + } + cursor.close() + return products + } + + fun getSize(mark: Int): Boolean { + val itemsSize = selectAll().size + return mark in 0 until itemsSize + } +} diff --git a/app/src/main/java/woowacourse/shopping/data/db/RecentViewedContract.kt b/app/src/main/java/woowacourse/shopping/data/db/RecentViewedContract.kt new file mode 100644 index 000000000..78e748287 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/db/RecentViewedContract.kt @@ -0,0 +1,6 @@ +package woowacourse.shopping.data.db + +object RecentViewedContract { + const val TABLE_NAME = "recentViewed" + const val TABLE_COLUMN_ID = "id" +} diff --git a/app/src/main/java/woowacourse/shopping/data/db/RecentViewedDBHelper.kt b/app/src/main/java/woowacourse/shopping/data/db/RecentViewedDBHelper.kt new file mode 100644 index 000000000..07f7ba410 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/db/RecentViewedDBHelper.kt @@ -0,0 +1,58 @@ +package woowacourse.shopping.data.db + +import android.content.ContentValues +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper + +class RecentViewedDBHelper(context: Context) : SQLiteOpenHelper(context, "recent_viewed", null, 1) { + override fun onCreate(db: SQLiteDatabase?) { + db?.execSQL( + "CREATE TABLE ${RecentViewedContract.TABLE_NAME} (" + + " ${RecentViewedContract.TABLE_COLUMN_ID} Int PRIMARY KEY not null" + + ");", + ) + } + + override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { + db?.execSQL("DROP TABLE IF EXISTS ${RecentViewedContract.TABLE_NAME}") + onCreate(db) + } + + fun insert(id: Int) { + val values = ContentValues() + values.put(RecentViewedContract.TABLE_COLUMN_ID, id) + writableDatabase.insert(RecentViewedContract.TABLE_NAME, null, values) + } + + fun remove(id: Int) { + writableDatabase.execSQL("DELETE FROM ${RecentViewedContract.TABLE_NAME} WHERE ${RecentViewedContract.TABLE_COLUMN_ID}=$id") + } + + fun selectWhereId(id: Int): Int? { + val sql = "select * from ${RecentViewedContract.TABLE_NAME} WHERE ${RecentViewedContract.TABLE_COLUMN_ID}=$id" + val cursor = readableDatabase.rawQuery(sql, null) + while (cursor.moveToNext()) { + val id = cursor.getInt(cursor.getColumnIndexOrThrow(RecentViewedContract.TABLE_COLUMN_ID)) + cursor.close() + return id + } + return null + } + + fun selectAll(): List { + val viewedProducts = mutableListOf() + val sql = "select * from ${RecentViewedContract.TABLE_NAME}" + val cursor = readableDatabase.rawQuery(sql, null) + while (cursor.moveToNext()) { + val id = cursor.getInt(cursor.getColumnIndexOrThrow(RecentViewedContract.TABLE_COLUMN_ID)) + viewedProducts.add(id) + } + cursor.close() + return viewedProducts + } + + fun removeOldest() { + writableDatabase.execSQL("DELETE FROM ${RecentViewedContract.TABLE_NAME} WHERE rowid = (SELECT MIN(rowid) FROM ${RecentViewedContract.TABLE_NAME});") + } +} diff --git a/app/src/main/java/woowacourse/shopping/model/CartPageStatus.kt b/app/src/main/java/woowacourse/shopping/model/CartPageStatus.kt new file mode 100644 index 000000000..0716aca6d --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/CartPageStatus.kt @@ -0,0 +1,7 @@ +package woowacourse.shopping.model + +data class CartPageStatus( + val isPrevEnabled: Boolean, + val isNextEnabled: Boolean, + val count: Int +) diff --git a/app/src/main/java/woowacourse/shopping/model/CartProductMapper.kt b/app/src/main/java/woowacourse/shopping/model/CartProductMapper.kt new file mode 100644 index 000000000..216962d46 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/CartProductMapper.kt @@ -0,0 +1,7 @@ +package woowacourse.shopping.model + +import woowacourse.shopping.domain.CartProduct +import woowacourse.shopping.domain.Product + +fun CartProduct.toUiModel(product: Product): CartProductModel = + CartProductModel(id, product.name, product.imageUrl, count, product.price.price * count) diff --git a/app/src/main/java/woowacourse/shopping/model/CartProductModel.kt b/app/src/main/java/woowacourse/shopping/model/CartProductModel.kt new file mode 100644 index 000000000..f7e99a67b --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/CartProductModel.kt @@ -0,0 +1,3 @@ +package woowacourse.shopping.model + +data class CartProductModel(val id: Int, val name: String, val imageUrl: String, val count: Int, val totalPrice: Int) diff --git a/app/src/main/java/woowacourse/shopping/model/NextPagination.kt b/app/src/main/java/woowacourse/shopping/model/NextPagination.kt new file mode 100644 index 000000000..ac16734b6 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/NextPagination.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.model + +interface NextPagination { + val isNextEnabled: Boolean + get() = nextItemExist() + + fun nextItems(): List + + fun nextItemExist(): Boolean +} diff --git a/app/src/main/java/woowacourse/shopping/model/PrevPagination.kt b/app/src/main/java/woowacourse/shopping/model/PrevPagination.kt new file mode 100644 index 000000000..c048128d7 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/PrevPagination.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.model + +interface PrevPagination { + val isPrevEnabled: Boolean + get() = prevItemExist() + + fun prevItems(): List + + fun prevItemExist(): Boolean +} diff --git a/app/src/main/java/woowacourse/shopping/model/ProductMapper.kt b/app/src/main/java/woowacourse/shopping/model/ProductMapper.kt new file mode 100644 index 000000000..62b6d2762 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/ProductMapper.kt @@ -0,0 +1,5 @@ +package woowacourse.shopping.model + +import woowacourse.shopping.domain.Product + +fun Product.toUiModel() = ProductModel(id, name, imageUrl, price.price) diff --git a/app/src/main/java/woowacourse/shopping/model/ProductModel.kt b/app/src/main/java/woowacourse/shopping/model/ProductModel.kt new file mode 100644 index 000000000..5a4d2c27c --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/model/ProductModel.kt @@ -0,0 +1,7 @@ +package woowacourse.shopping.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ProductModel(val id: Int, val name: String, val imageUrl: String, val price: Int) : Parcelable diff --git a/app/src/main/java/woowacourse/shopping/util/IntentExtension.kt b/app/src/main/java/woowacourse/shopping/util/IntentExtension.kt new file mode 100644 index 000000000..bfd761f48 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/IntentExtension.kt @@ -0,0 +1,13 @@ +package woowacourse.shopping.util + +import android.content.Intent +import android.os.Build +import android.os.Parcelable + +inline fun Intent.getParcelableCompat(key: String): T? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return getParcelableExtra(key, T::class.java) + } + @Suppress("DEPRECATION") + return getParcelableExtra(key) as? T +} diff --git a/app/src/main/java/woowacourse/shopping/util/PriceFormatter.kt b/app/src/main/java/woowacourse/shopping/util/PriceFormatter.kt new file mode 100644 index 000000000..68aa89a5f --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/util/PriceFormatter.kt @@ -0,0 +1,9 @@ +package woowacourse.shopping.util + +import java.text.DecimalFormat + +object PriceFormatter { + fun format(price: Int): String { + return DecimalFormat("#,###").format(price) + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/cart/CartActivity.kt b/app/src/main/java/woowacourse/shopping/view/cart/CartActivity.kt new file mode 100644 index 000000000..a7db389d0 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/cart/CartActivity.kt @@ -0,0 +1,78 @@ +package woowacourse.shopping.view.cart + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.appcompat.app.AppCompatActivity +import woowacourse.shopping.data.CartDbRepository +import woowacourse.shopping.data.ProductMockRepository +import woowacourse.shopping.databinding.ActivityCartBinding + +class CartActivity : AppCompatActivity(), CartContract.View { + private lateinit var binding: ActivityCartBinding + private lateinit var presenter: CartContract.Presenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpBinding() + setContentView(binding.root) + setUpActionBar() + setUpPresenter() + presenter.fetchProducts() + } + + private fun setUpBinding() { + binding = ActivityCartBinding.inflate(layoutInflater) + } + + private fun setUpActionBar() { + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.title = TITLE + } + + private fun setUpPresenter() { + presenter = CartPresenter(this, CartDbRepository(this), ProductMockRepository) + } + + override fun showProducts(items: List) { + binding.recyclerCart.adapter = CartAdapter( + items, + object : CartAdapter.OnItemClick { + override fun onRemoveClick(id: Int) { + presenter.removeProduct(id) + } + + override fun onNextClick() { + presenter.fetchNextPage() + } + + override fun onPrevClick() { + presenter.fetchPrevPage() + } + } + ) + } + + override fun showOtherPage() { + binding.recyclerCart.adapter?.notifyDataSetChanged() + } + + override fun notifyRemoveItem(position: Int) { + binding.recyclerCart.adapter?.notifyItemRemoved(position) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + } + } + return super.onOptionsItemSelected(item) + } + + companion object { + private const val TITLE = "Cart" + fun newIntent(context: Context): Intent = Intent(context, CartActivity::class.java) + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/cart/CartAdapter.kt b/app/src/main/java/woowacourse/shopping/view/cart/CartAdapter.kt new file mode 100644 index 000000000..ae5d522bb --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/cart/CartAdapter.kt @@ -0,0 +1,33 @@ +package woowacourse.shopping.view.cart + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +class CartAdapter( + private val items: List, + private val onItemClick: OnItemClick, +) : RecyclerView.Adapter() { + + interface OnItemClick { + fun onRemoveClick(id: Int) + fun onNextClick() + fun onPrevClick() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CartItemViewHolder { + return CartItemViewHolder.of(parent, CartViewType.values()[viewType], onItemClick) + } + + override fun getItemViewType(position: Int): Int { + return items[position].type.ordinal + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: CartItemViewHolder, position: Int) { + when (holder) { + is CartItemViewHolder.CartProductViewHolder -> holder.bind(items[position] as CartViewItem.CartProductItem) + is CartItemViewHolder.CartPaginationViewHolder -> holder.bind(items[position] as CartViewItem.PaginationItem) + } + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/cart/CartContract.kt b/app/src/main/java/woowacourse/shopping/view/cart/CartContract.kt new file mode 100644 index 000000000..6c3b30d09 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/cart/CartContract.kt @@ -0,0 +1,16 @@ +package woowacourse.shopping.view.cart + +interface CartContract { + interface View { + fun showProducts(items: List) + fun notifyRemoveItem(position: Int) + fun showOtherPage() + } + + interface Presenter { + fun fetchProducts() + fun removeProduct(id: Int) + fun fetchNextPage() + fun fetchPrevPage() + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/cart/CartItemViewHolder.kt b/app/src/main/java/woowacourse/shopping/view/cart/CartItemViewHolder.kt new file mode 100644 index 000000000..74e615aaa --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/cart/CartItemViewHolder.kt @@ -0,0 +1,66 @@ +package woowacourse.shopping.view.cart + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import woowacourse.shopping.R +import woowacourse.shopping.databinding.ItemCartBinding +import woowacourse.shopping.databinding.ItemCartPaginationBinding +import woowacourse.shopping.util.PriceFormatter + +sealed class CartItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { + class CartProductViewHolder( + private val binding: ItemCartBinding, + onItemClick: CartAdapter.OnItemClick + ) : + CartItemViewHolder(binding.root) { + init { + binding.onItemClick = onItemClick + } + + fun bind(item: CartViewItem.CartProductItem) { + binding.cartProduct = item.product + binding.textPrice.text = binding.root.context.getString( + R.string.korean_won, + PriceFormatter.format(item.product.totalPrice) + ) + Glide.with(binding.root.context).load(item.product.imageUrl).into(binding.imgProduct) + } + } + + class CartPaginationViewHolder( + private val binding: ItemCartPaginationBinding, + onItemClick: CartAdapter.OnItemClick + ) : + CartItemViewHolder(binding.root) { + init { + binding.onItemClick = onItemClick + } + + fun bind(item: CartViewItem.PaginationItem) { + binding.status = item.status + } + } + + companion object { + fun of( + parent: ViewGroup, + type: CartViewType, + onItemClick: CartAdapter.OnItemClick + ): CartItemViewHolder { + val view = LayoutInflater.from(parent.context).inflate(type.id, parent, false) + return when (type) { + CartViewType.CART_PRODUCT_ITEM -> CartProductViewHolder( + ItemCartBinding.bind(view), + onItemClick + ) + CartViewType.PAGINATION_ITEM -> CartPaginationViewHolder( + ItemCartPaginationBinding.bind(view), + onItemClick + ) + } + } + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/cart/CartPagination.kt b/app/src/main/java/woowacourse/shopping/view/cart/CartPagination.kt new file mode 100644 index 000000000..d646953df --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/cart/CartPagination.kt @@ -0,0 +1,41 @@ +package woowacourse.shopping.view.cart + +import woowacourse.shopping.domain.CartProduct +import woowacourse.shopping.domain.CartRepository +import woowacourse.shopping.model.CartPageStatus +import woowacourse.shopping.model.NextPagination +import woowacourse.shopping.model.PrevPagination + +class CartPagination(private val rangeSize: Int, private val cartRepository: CartRepository) : + NextPagination, PrevPagination { + private var mark = 0 + private val page: Int + get() = mark / rangeSize + val status: CartPageStatus + get() = CartPageStatus(isPrevEnabled, isNextEnabled, page) + + override fun nextItems(): List { + if (nextItemExist()) { + val items = cartRepository.findRange(mark, rangeSize) + mark += rangeSize + return items + } + return emptyList() + } + + override fun prevItems(): List { + if (prevItemExist()) { + mark -= rangeSize + return cartRepository.findRange(mark - rangeSize, rangeSize) + } + return emptyList() + } + + override fun nextItemExist(): Boolean { + return cartRepository.isExistByMark(mark) + } + + override fun prevItemExist(): Boolean { + return cartRepository.isExistByMark(mark - rangeSize - 1) + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/cart/CartPresenter.kt b/app/src/main/java/woowacourse/shopping/view/cart/CartPresenter.kt new file mode 100644 index 000000000..06f8fc9e1 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/cart/CartPresenter.kt @@ -0,0 +1,65 @@ +package woowacourse.shopping.view.cart + +import woowacourse.shopping.domain.CartProduct +import woowacourse.shopping.domain.CartRepository +import woowacourse.shopping.domain.ProductRepository +import woowacourse.shopping.model.toUiModel + +class CartPresenter( + private val view: CartContract.View, + private val cartRepository: CartRepository, + private val productRepository: ProductRepository +) : CartContract.Presenter { + private val cartPagination = CartPagination(PAGINATION_SIZE, cartRepository) + + private val currentCartProducts = + convertToCartProductModels(cartPagination.nextItems()).toMutableList() + private val cartItems = + ( + currentCartProducts.map { CartViewItem.CartProductItem(it) } + + CartViewItem.PaginationItem(cartPagination.status) + ).toMutableList() + + override fun fetchProducts() { + view.showProducts(cartItems) + } + + override fun removeProduct(id: Int) { + val removedItem = currentCartProducts.find { it.id == id } + cartRepository.remove(id) + view.notifyRemoveItem(currentCartProducts.indexOf(removedItem)) + cartItems.removeAt(currentCartProducts.indexOf(removedItem)) + currentCartProducts.remove(removedItem) + } + + override fun fetchNextPage() { + val items = cartPagination.nextItems() + if (items.isNotEmpty()) { + changeListItems(items) + view.showOtherPage() + } + } + + override fun fetchPrevPage() { + val items = cartPagination.prevItems() + if (items.isNotEmpty()) { + changeListItems(items) + view.showOtherPage() + } + } + + private fun convertToCartProductModels(cartProducts: List) = + cartProducts.asSequence().map { it.toUiModel(productRepository.find(it.id)) }.toList() + + private fun changeListItems(items: List) { + currentCartProducts.clear() + currentCartProducts.addAll(convertToCartProductModels(items)) + cartItems.clear() + cartItems.addAll(currentCartProducts.map { CartViewItem.CartProductItem(it) }) + cartItems.add(CartViewItem.PaginationItem(cartPagination.status)) + } + + companion object { + private const val PAGINATION_SIZE = 5 + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/cart/CartViewItem.kt b/app/src/main/java/woowacourse/shopping/view/cart/CartViewItem.kt new file mode 100644 index 000000000..cda6d8c5e --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/cart/CartViewItem.kt @@ -0,0 +1,14 @@ +package woowacourse.shopping.view.cart + +import woowacourse.shopping.model.CartPageStatus +import woowacourse.shopping.model.CartProductModel + +sealed interface CartViewItem { + val type: CartViewType + data class CartProductItem(val product: CartProductModel) : CartViewItem { + override val type = CartViewType.CART_PRODUCT_ITEM + } + data class PaginationItem(val status: CartPageStatus) : CartViewItem { + override val type = CartViewType.PAGINATION_ITEM + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/cart/CartViewType.kt b/app/src/main/java/woowacourse/shopping/view/cart/CartViewType.kt new file mode 100644 index 000000000..68fcc7534 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/cart/CartViewType.kt @@ -0,0 +1,9 @@ +package woowacourse.shopping.view.cart + +import androidx.annotation.LayoutRes +import woowacourse.shopping.R + +enum class CartViewType(@LayoutRes val id: Int) { + CART_PRODUCT_ITEM(R.layout.item_cart), + PAGINATION_ITEM(R.layout.item_cart_pagination), +} diff --git a/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailActivity.kt b/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailActivity.kt new file mode 100644 index 000000000..1a1a49597 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailActivity.kt @@ -0,0 +1,98 @@ +package woowacourse.shopping.view.productdetail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import com.bumptech.glide.Glide +import woowacourse.shopping.R +import woowacourse.shopping.data.CartDbRepository +import woowacourse.shopping.data.RecentViewedDbRepository +import woowacourse.shopping.databinding.ActivityProductDetailBinding +import woowacourse.shopping.model.ProductModel +import woowacourse.shopping.util.PriceFormatter +import woowacourse.shopping.util.getParcelableCompat +import woowacourse.shopping.view.cart.CartActivity +import woowacourse.shopping.view.productlist.ProductListActivity.Companion.ID +import woowacourse.shopping.view.productlist.ProductListActivity.Companion.RESULT_VIEWED + +class ProductDetailActivity : AppCompatActivity(), ProductDetailContract.View { + private lateinit var binding: ActivityProductDetailBinding + private lateinit var presenter: ProductDetailContract.Presenter + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpPresenter() + setUpBinding() + setContentView(binding.root) + val product = intent.getParcelableCompat(PRODUCT) + if (product == null) { + forceQuit() + return + } + setUpInitView(product) + setUpResult(product.id) + presenter.updateRecentViewedProducts(product.id) + } + + private fun setUpBinding() { + binding = ActivityProductDetailBinding.inflate(layoutInflater) + } + + private fun setUpPresenter() { + presenter = + ProductDetailPresenter(this, CartDbRepository(this), RecentViewedDbRepository(this)) + } + + private fun forceQuit() { + Toast.makeText(this, NOT_EXIST_DATA_ERROR, Toast.LENGTH_LONG).show() + finish() + } + + private fun setUpInitView(product: ProductModel) { + binding.product = product + binding.presenter = presenter + Glide.with(binding.root.context).load(product.imageUrl).into(binding.imgProduct) + binding.textPrice.text = + getString(R.string.korean_won, PriceFormatter.format(product.price)) + } + + private fun setUpResult(id: Int) { + intent.putExtra(ID, id) + setResult(RESULT_VIEWED, intent) + } + + override fun startCartActivity() { + val intent = CartActivity.newIntent(this) + startActivity(intent) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + val inflater: MenuInflater = menuInflater + inflater.inflate(R.menu.menu_close, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.close -> { + finish() + } + } + return super.onOptionsItemSelected(item) + } + + companion object { + const val PRODUCT = "PRODUCT" + private const val NOT_EXIST_DATA_ERROR = "데이터가 넘어오지 않았습니다." + + fun newIntent(context: Context, product: ProductModel): Intent { + val intent = Intent(context, ProductDetailActivity::class.java) + intent.putExtra(PRODUCT, product) + return intent + } + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailContract.kt b/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailContract.kt new file mode 100644 index 000000000..bbc692700 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailContract.kt @@ -0,0 +1,14 @@ +package woowacourse.shopping.view.productdetail + +import woowacourse.shopping.model.ProductModel + +interface ProductDetailContract { + interface View { + fun startCartActivity() + } + + interface Presenter { + fun putInCart(product: ProductModel) + fun updateRecentViewedProducts(id: Int) + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailPresenter.kt b/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailPresenter.kt new file mode 100644 index 000000000..06140ac6b --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productdetail/ProductDetailPresenter.kt @@ -0,0 +1,20 @@ +package woowacourse.shopping.view.productdetail + +import woowacourse.shopping.domain.CartRepository +import woowacourse.shopping.domain.RecentViewedRepository +import woowacourse.shopping.model.ProductModel + +class ProductDetailPresenter( + private val view: ProductDetailContract.View, + private val cartRepository: CartRepository, + private val recentViewedRepository: RecentViewedRepository, +) : ProductDetailContract.Presenter { + override fun putInCart(product: ProductModel) { + cartRepository.add(product.id, 1) + view.startCartActivity() + } + + override fun updateRecentViewedProducts(id: Int) { + recentViewedRepository.add(id) + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/GridLayoutManagerWrapper.kt b/app/src/main/java/woowacourse/shopping/view/productlist/GridLayoutManagerWrapper.kt new file mode 100644 index 000000000..156694023 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/GridLayoutManagerWrapper.kt @@ -0,0 +1,12 @@ +package woowacourse.shopping.view.productlist + +import android.content.Context +import androidx.recyclerview.widget.GridLayoutManager + +class GridLayoutManagerWrapper : GridLayoutManager { + constructor(context: Context, spanCount: Int) : super(context, spanCount) + + override fun supportsPredictiveItemAnimations(): Boolean { + return false + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/ProductListActivity.kt b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListActivity.kt new file mode 100644 index 000000000..f5bf784ad --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListActivity.kt @@ -0,0 +1,113 @@ +package woowacourse.shopping.view.productlist + +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.GridLayoutManager +import woowacourse.shopping.R +import woowacourse.shopping.data.ProductMockRepository +import woowacourse.shopping.data.RecentViewedDbRepository +import woowacourse.shopping.databinding.ActivityProductListBinding +import woowacourse.shopping.model.ProductModel +import woowacourse.shopping.view.cart.CartActivity +import woowacourse.shopping.view.productdetail.ProductDetailActivity + +class ProductListActivity : AppCompatActivity(), ProductListContract.View { + private lateinit var binding: ActivityProductListBinding + private lateinit var presenter: ProductListContract.Presenter + private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == RESULT_VIEWED) { + val id = it.data?.getIntExtra("id", -1) + presenter.updateRecentViewed(id ?: -1) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpBinding() + setContentView(binding.root) + setUpPresenter() + setUpActionBar() + presenter.fetchProducts() + } + + private fun setUpBinding() { + binding = ActivityProductListBinding.inflate(layoutInflater) + } + + private fun setUpPresenter() { + presenter = + ProductListPresenter(this, ProductMockRepository, RecentViewedDbRepository(this)) + } + + private fun setUpActionBar() { + supportActionBar?.setDisplayShowCustomEnabled(true) + } + + override fun showProducts(items: List) { + val gridLayoutManager = GridLayoutManagerWrapper(this, 2) + gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + val isHeader = items[position].type == ProductListViewType.RECENT_VIEWED_ITEM + val isFooter = items[position].type == ProductListViewType.SHOW_MORE_ITEM + return if (isHeader || isFooter) { + HEADER_FOOTER_SPAN + } else { + PRODUCT_ITEM_SPAN + } + } + } + binding.gridProducts.layoutManager = gridLayoutManager + binding.gridProducts.adapter = ProductListAdapter( + items, + object : ProductListAdapter.OnItemClick { + override fun onProductClick(product: ProductModel) { + showProductDetail(product) + } + + override fun onShowMoreClick() { + presenter.showMoreProducts() + } + }, + ) + } + + override fun notifyAddProducts(position: Int, size: Int) { + binding.gridProducts.adapter?.notifyItemRangeInserted(position, size) + } + + override fun notifyRecentViewedChanged() { + binding.gridProducts.adapter?.notifyItemChanged(0) + } + + private fun showProductDetail(product: ProductModel) { + val intent = ProductDetailActivity.newIntent(binding.root.context, product) + resultLauncher.launch(intent) + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + val inflater: MenuInflater = menuInflater + inflater.inflate(R.menu.menu_item, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.cart -> { + val intent = Intent(this, CartActivity::class.java) + startActivity(intent) + } + } + return super.onOptionsItemSelected(item) + } + companion object { + private const val HEADER_FOOTER_SPAN = 2 + private const val PRODUCT_ITEM_SPAN = 1 + const val RESULT_VIEWED = 200 + const val ID = "id" + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/ProductListAdapter.kt b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListAdapter.kt new file mode 100644 index 000000000..2c5512884 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListAdapter.kt @@ -0,0 +1,39 @@ +package woowacourse.shopping.view.productlist + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import woowacourse.shopping.model.ProductModel + +class ProductListAdapter( + private val items: List, + private val onItemClick: OnItemClick, +) : RecyclerView.Adapter() { + interface OnItemClick { + fun onProductClick(product: ProductModel) + fun onShowMoreClick() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder { + return ProductViewHolder.of(parent, ProductListViewType.values()[viewType], onItemClick) + } + + override fun getItemViewType(position: Int): Int { + return items[position].type.ordinal + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: ProductViewHolder, position: Int) { + when (holder) { + is ProductViewHolder.RecentViewedViewHolder -> { + holder.bind(items[position] as ProductListViewItem.RecentViewedItem, onItemClick) + } + is ProductViewHolder.ProductItemViewHolder -> { + holder.bind(items[position] as ProductListViewItem.ProductItem) + } + is ProductViewHolder.ShowMoreViewHolder -> { + return + } + } + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/ProductListContract.kt b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListContract.kt new file mode 100644 index 000000000..ffb40d842 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListContract.kt @@ -0,0 +1,16 @@ +package woowacourse.shopping.view.productlist + +interface ProductListContract { + interface View { + fun showProducts(items: List) + fun notifyAddProducts(position: Int, size: Int) + + fun notifyRecentViewedChanged() + } + + interface Presenter { + fun fetchProducts() + fun showMoreProducts() + fun updateRecentViewed(id: Int) + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/ProductListPagination.kt b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListPagination.kt new file mode 100644 index 000000000..f97c7f01a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListPagination.kt @@ -0,0 +1,25 @@ +package woowacourse.shopping.view.productlist + +import woowacourse.shopping.domain.Product +import woowacourse.shopping.domain.ProductRepository +import woowacourse.shopping.model.NextPagination + +class ProductListPagination( + private val rangeSize: Int, + private val productRepository: ProductRepository +) : NextPagination { + private var mark = 0 + + override fun nextItems(): List { + if (isNextEnabled) { + val items = productRepository.findRange(mark, rangeSize) + mark += rangeSize + return items + } + return emptyList() + } + + override fun nextItemExist(): Boolean { + return productRepository.isExistByMark(mark) + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/ProductListPresenter.kt b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListPresenter.kt new file mode 100644 index 000000000..f67fd1f35 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListPresenter.kt @@ -0,0 +1,57 @@ +package woowacourse.shopping.view.productlist + +import woowacourse.shopping.domain.ProductRepository +import woowacourse.shopping.domain.RecentViewedRepository +import woowacourse.shopping.model.toUiModel + +class ProductListPresenter( + private val view: ProductListContract.View, + private val productRepository: ProductRepository, + private val recentViewedRepository: RecentViewedRepository, +) : ProductListContract.Presenter { + private val productListPagination = ProductListPagination(PAGINATION_SIZE, productRepository) + private val products = productListPagination.nextItems().map { it.toUiModel() }.toMutableList() + private val viewedProducts = recentViewedRepository.findAll().toMutableList() + + private val productsListItems = mutableListOf() + override fun fetchProducts() { + // 최근 본 항목 + val viewedProducts = recentViewedRepository.findAll().reversed() + val viewedProductsItem = + ProductListViewItem.RecentViewedItem(viewedProducts.map { convertIdToProductModel(it) }) + productsListItems.add(viewedProductsItem) + // 상품 리스트 + productsListItems.addAll(products.map { ProductListViewItem.ProductItem(it) }) + // 더보기 + if (productListPagination.isNextEnabled) productsListItems.add(ProductListViewItem.ShowMoreItem()) + view.showProducts(productsListItems) + } + + override fun showMoreProducts() { + val mark = if (isExistRecentViewed()) products.size + 1 else products.size + val nextProducts = productListPagination.nextItems().map { it.toUiModel() } + products.addAll(nextProducts) + // RecyclerView Items 수정 + productsListItems.removeLast() + productsListItems.addAll(nextProducts.map { ProductListViewItem.ProductItem(it) }) + if (productListPagination.isNextEnabled) productsListItems.add(ProductListViewItem.ShowMoreItem()) + // Notify + view.notifyAddProducts(mark, PAGINATION_SIZE) + } + + override fun updateRecentViewed(id: Int) { + if (id == -1) return + viewedProducts.add(0, id) + if (isExistRecentViewed()) productsListItems.removeAt(0) + productsListItems.add(0, ProductListViewItem.RecentViewedItem(viewedProducts.map { convertIdToProductModel(it) })) + view.notifyRecentViewedChanged() + } + + private fun convertIdToProductModel(id: Int) = productRepository.find(id).toUiModel() + + private fun isExistRecentViewed(): Boolean = productsListItems[0] is ProductListViewItem.RecentViewedItem + + companion object { + private const val PAGINATION_SIZE = 20 + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/ProductListViewItem.kt b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListViewItem.kt new file mode 100644 index 000000000..67af9df9a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListViewItem.kt @@ -0,0 +1,16 @@ +package woowacourse.shopping.view.productlist + +import woowacourse.shopping.model.ProductModel + +sealed interface ProductListViewItem { + val type: ProductListViewType + data class RecentViewedItem(val products: List) : ProductListViewItem { + override val type = ProductListViewType.RECENT_VIEWED_ITEM + } + data class ProductItem(val product: ProductModel) : ProductListViewItem { + override val type = ProductListViewType.PRODUCT_ITEM + } + class ShowMoreItem : ProductListViewItem { + override val type = ProductListViewType.SHOW_MORE_ITEM + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/ProductListViewType.kt b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListViewType.kt new file mode 100644 index 000000000..ea23c3e95 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/ProductListViewType.kt @@ -0,0 +1,10 @@ +package woowacourse.shopping.view.productlist + +import androidx.annotation.LayoutRes +import woowacourse.shopping.R + +enum class ProductListViewType(@LayoutRes val id: Int) { + RECENT_VIEWED_ITEM(R.layout.item_recent_viewed), + PRODUCT_ITEM(R.layout.item_product), + SHOW_MORE_ITEM(R.layout.item_show_more) +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/ProductViewHolder.kt b/app/src/main/java/woowacourse/shopping/view/productlist/ProductViewHolder.kt new file mode 100644 index 000000000..42d826386 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/ProductViewHolder.kt @@ -0,0 +1,80 @@ +package woowacourse.shopping.view.productlist + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import woowacourse.shopping.R +import woowacourse.shopping.databinding.ItemProductBinding +import woowacourse.shopping.databinding.ItemRecentViewedBinding +import woowacourse.shopping.databinding.ItemShowMoreBinding +import woowacourse.shopping.util.PriceFormatter +import woowacourse.shopping.view.productlist.recentviewed.RecentViewedAdapter + +sealed class ProductViewHolder(view: View) : RecyclerView.ViewHolder(view) { + class RecentViewedViewHolder(private val binding: ItemRecentViewedBinding) : + ProductViewHolder(binding.root) { + fun bind( + item: ProductListViewItem.RecentViewedItem, + onItemClick: ProductListAdapter.OnItemClick + ) { + binding.recyclerRecentViewed.adapter = + RecentViewedAdapter(item.products, onItemClick) + } + } + + class ProductItemViewHolder( + private val binding: ItemProductBinding, + onItemClick: ProductListAdapter.OnItemClick + ) : ProductViewHolder(binding.root) { + init { + binding.onItemClick = onItemClick + } + + fun bind(item: ProductListViewItem.ProductItem) { + binding.product = item.product + binding.textPrice.text = binding.root.context.getString( + R.string.korean_won, + PriceFormatter.format(item.product.price) + ) + Glide.with(binding.root.context).load(item.product.imageUrl).into(binding.imgProduct) + } + } + + class ShowMoreViewHolder( + binding: ItemShowMoreBinding, + onItemClick: ProductListAdapter.OnItemClick + ) : ProductViewHolder(binding.root) { + init { + binding.onItemClick = onItemClick + } + } + + companion object { + fun of( + parent: ViewGroup, + type: ProductListViewType, + onItemClick: ProductListAdapter.OnItemClick + ): ProductViewHolder { + val view = LayoutInflater.from(parent.context).inflate(type.id, parent, false) + return when (type) { + ProductListViewType.RECENT_VIEWED_ITEM -> RecentViewedViewHolder( + ItemRecentViewedBinding.bind(view) + ) + ProductListViewType.PRODUCT_ITEM -> ProductItemViewHolder( + ItemProductBinding.bind( + view + ), + onItemClick + ) + ProductListViewType.SHOW_MORE_ITEM -> ShowMoreViewHolder( + ItemShowMoreBinding.bind( + view + ), + onItemClick + ) + } + } + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/recentviewed/RecentViewedAdapter.kt b/app/src/main/java/woowacourse/shopping/view/productlist/recentviewed/RecentViewedAdapter.kt new file mode 100644 index 000000000..635321806 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/recentviewed/RecentViewedAdapter.kt @@ -0,0 +1,27 @@ +package woowacourse.shopping.view.productlist.recentviewed + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import woowacourse.shopping.R +import woowacourse.shopping.databinding.ItemRecentViewedProductBinding +import woowacourse.shopping.model.ProductModel +import woowacourse.shopping.view.productlist.ProductListAdapter + +class RecentViewedAdapter( + private val products: List, + private val onItemClick: ProductListAdapter.OnItemClick, +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentViewedItemViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_recent_viewed_product, parent, false) + return RecentViewedItemViewHolder(ItemRecentViewedProductBinding.bind(view)) + } + + override fun getItemCount(): Int = products.size + + override fun onBindViewHolder(holder: RecentViewedItemViewHolder, position: Int) { + holder.bind(products[position], onItemClick) + } +} diff --git a/app/src/main/java/woowacourse/shopping/view/productlist/recentviewed/RecentViewedItemViewHolder.kt b/app/src/main/java/woowacourse/shopping/view/productlist/recentviewed/RecentViewedItemViewHolder.kt new file mode 100644 index 000000000..a8cdedeb4 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/view/productlist/recentviewed/RecentViewedItemViewHolder.kt @@ -0,0 +1,16 @@ +package woowacourse.shopping.view.productlist.recentviewed + +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import woowacourse.shopping.databinding.ItemRecentViewedProductBinding +import woowacourse.shopping.model.ProductModel +import woowacourse.shopping.view.productlist.ProductListAdapter + +class RecentViewedItemViewHolder(private val binding: ItemRecentViewedProductBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(product: ProductModel, onItemClick: ProductListAdapter.OnItemClick) { + binding.product = product + Glide.with(binding.root.context).load(product.imageUrl).into(binding.imgProduct) + binding.onItemClick = onItemClick + } +} diff --git a/app/src/main/res/drawable/back_rounded.xml b/app/src/main/res/drawable/back_rounded.xml new file mode 100644 index 000000000..9338192ad --- /dev/null +++ b/app/src/main/res/drawable/back_rounded.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/back_rounded_bold.xml b/app/src/main/res/drawable/back_rounded_bold.xml new file mode 100644 index 000000000..1c4dd1041 --- /dev/null +++ b/app/src/main/res/drawable/back_rounded_bold.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/cart.png b/app/src/main/res/drawable/cart.png new file mode 100644 index 000000000..9214ed908 Binary files /dev/null and b/app/src/main/res/drawable/cart.png differ diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..8a3ceff64 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/icon_left_page_false.xml b/app/src/main/res/drawable/icon_left_page_false.xml new file mode 100644 index 000000000..825898841 --- /dev/null +++ b/app/src/main/res/drawable/icon_left_page_false.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/icon_left_page_true.xml b/app/src/main/res/drawable/icon_left_page_true.xml new file mode 100644 index 000000000..9e6b4693a --- /dev/null +++ b/app/src/main/res/drawable/icon_left_page_true.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/icon_right_page_false.xml b/app/src/main/res/drawable/icon_right_page_false.xml new file mode 100644 index 000000000..3418ff340 --- /dev/null +++ b/app/src/main/res/drawable/icon_right_page_false.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/icon_right_page_true.xml b/app/src/main/res/drawable/icon_right_page_true.xml new file mode 100644 index 000000000..285f0d19f --- /dev/null +++ b/app/src/main/res/drawable/icon_right_page_true.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/selector_page_left_button.xml b/app/src/main/res/drawable/selector_page_left_button.xml new file mode 100644 index 000000000..8c715e4cf --- /dev/null +++ b/app/src/main/res/drawable/selector_page_left_button.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/selector_page_right_button.xml b/app/src/main/res/drawable/selector_page_right_button.xml new file mode 100644 index 000000000..2f7ce8535 --- /dev/null +++ b/app/src/main/res/drawable/selector_page_right_button.xml @@ -0,0 +1,5 @@ + + + + + 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..565f59739 --- /dev/null +++ b/app/src/main/res/layout/activity_cart.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_product_detail.xml b/app/src/main/res/layout/activity_product_detail.xml new file mode 100644 index 000000000..0c5b0e761 --- /dev/null +++ b/app/src/main/res/layout/activity_product_detail.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_product_list.xml similarity index 56% rename from app/src/main/res/layout/activity_main.xml rename to app/src/main/res/layout/activity_product_list.xml index c75d0576c..538553f52 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_product_list.xml @@ -4,15 +4,18 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context=".view.productlist.ProductListActivity"> - + app:layout_constraintTop_toTopOf="parent" + app:spanCount="2" + tools:listitem="@layout/item_product" /> diff --git a/app/src/main/res/layout/item_cart.xml b/app/src/main/res/layout/item_cart.xml new file mode 100644 index 000000000..3227625f7 --- /dev/null +++ b/app/src/main/res/layout/item_cart.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + +