diff --git a/README.md b/README.md
index 6df14d369d..42536180f7 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,59 @@
- 서버 통신을 위한 JSON 직렬화 라이브러리를 선택하고 PR에 선택 이유를 남긴다.
- 기능 요구 사항에 대한 테스트를 작성해야 한다.
+> Keyword: 데이터 포맷, Retrofit
+
+## 🧱 기능 요구 사항 (3,4단계)
+
+### 📕 기획 명세
+
+- Coroutines을 적용하여 비동기 요청을 리팩터링한다.
+ - 단, Flow를 사용하진 않는다.
+- 장바구니에 담긴 상품을 최종 주문할 수 있다.
+ - 배송비는 기본 3,000원이다.
+- 결제 화면에서 적용 가능한 쿠폰을 조회하고 적용할 수 있다.
+ - 쿠폰은 1개만 적용 가능하다.
+- 결제 수단은 구현하지 않는다.
+ - 결제하기 버튼을 누르면 바로 최종 주문이 완료된다.
+ - 최종 주문이 완료되면 상품 목록으로 이동과 함께 주문 완료 토스트 메시지를 노출한다.
+
+> Keyword: 코루틴, UI 핸들링
+
+### 📗 API 명세
+
+- 최종 주문이 완료되면 장바구니에서 주문된 상품이 초기화되는 것이 정상이다.
+- 쿠폰을 사용해도 사라지지 않는 것이 정상이다.
+
+### 📘 이벤트 명세
+
+- 다음과 같은 4가지 유형의 쿠폰을 사용할 수 있어야 한다. (쿠폰은 데이터는 API 문서에서 확인할 수 있다) 각 쿠폰의 특성을 잘 이해하고, 해당 쿠폰을 적용했을 때의 할인 효과를 정확히 계산할 수 있도록 구현한다.
+
+1. 5,000원 할인 쿠폰
+ - 쿠폰 코드: FIXED5000
+ - 할인 금액: 5,000원
+ - 최소 주문 금액: 100,000원
+ - 만료일: 2024년 11월 30일
+
+2. 2개 구매 시 1개 무료 쿠폰
+ - 쿠폰 코드: BOGO
+ - 구매 수량: 2
+ - 무료 제공 수량: 1
+ - 만료일: 2024년 5월 30일
+ - BOGO 쿠폰은 장바구니에 동일한 제품을 3개를 담은 상태에서 사용하면, 1개 분량의 금액을 할인한다.
+ - 3개씩 담은 제품이 여러개인 경우, 1개당 금액이 가장 비싼 제품에 적용한다.
+
+3. 5만원 이상 구매 시 무료 배송 쿠폰
+ - 쿠폰 코드: FREESHIPPING
+ - 최소 주문 금액: 50,000원
+ - 만료일: 2024년 8월 31일
+ - 배송비 무료 쿠폰은 도서 및 산간 지역인 경우에도 무료 배송이 가능하다.
+
+4. 미라클모닝 30% 할인 쿠폰
+ - 쿠폰 코드: MIRACLESALE
+ - 할인율: 30%
+ - 사용 가능 시간: 오전 4시부터 7시까지
+ - 만료일: 2024년 7월 31일
+
## 🛠️ 구현할 기능 (1,2단계)
- [x] 상품 로딩 전 스켈레톤 UI를 구현한다.
@@ -31,3 +84,13 @@
- [x] 추천 상품은 10개까지 노출한다.
- [x] 장바구니에 이미 추가된 상품은 추천 상품에 보이지 않는다.
- [x] 상품을 주문할 수 있다.
+
+## 🛠️ 구현할 기능 (3,4단계)
+
+- [x] 모든 API 및 DAO 함수에 suspend를 적용한다.
+- [x] 배송비를 3,000원 추가한다.
+- [x] 결제 화면에서 쿠폰을 조회한다.
+- [x] 결제 화면에서 쿠폰을 적용한다.
+- [x] 쿠폰은 1개만 적용 가능하다.
+- [x] 결제하기 버튼을 누르면 주문이 완료된다.
+- [x] 주문이 완료되면 주문 완료 토스트 메시지를 보여준다.
diff --git a/app/src/androidTest/java/woowacourse/shopping/data/dao/HistoryDaoTest.kt b/app/src/androidTest/java/woowacourse/shopping/data/dao/HistoryDaoTest.kt
index 1edd839f64..c2b617fccc 100644
--- a/app/src/androidTest/java/woowacourse/shopping/data/dao/HistoryDaoTest.kt
+++ b/app/src/androidTest/java/woowacourse/shopping/data/dao/HistoryDaoTest.kt
@@ -4,13 +4,19 @@ import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
+import org.junit.jupiter.api.extension.ExtendWith
import woowacourse.shopping.DUMMY_HISTORY_PRODUCT_1
import woowacourse.shopping.DUMMY_HISTORY_PRODUCT_2
import woowacourse.shopping.data.database.ShoppingDatabase
+import woowacourse.shopping.util.CoroutinesTestExtension
+@ExperimentalCoroutinesApi
+@ExtendWith(CoroutinesTestExtension::class)
@Suppress("ktlint:standard:function-naming")
class HistoryDaoTest {
private lateinit var database: ShoppingDatabase
@@ -28,108 +34,114 @@ class HistoryDaoTest {
}
@Test
- fun 검색_기록을_저장하고_조회한다() {
- // given
- val historyProduct = DUMMY_HISTORY_PRODUCT_1
- dao.insertHistory(historyProduct)
-
- // when
- val result = dao.getHistoryProducts()
-
- // then
- assertThat(result).hasSize(1)
- assertThat(result[0].productId).isEqualTo(DUMMY_HISTORY_PRODUCT_1.productId)
- }
+ fun 검색_기록을_저장하고_조회한다() =
+ runTest {
+ // given
+ val historyProduct = DUMMY_HISTORY_PRODUCT_1
+ dao.insertHistory(historyProduct)
+
+ // when
+ val result = dao.getHistoryProducts()
+
+ // then
+ assertThat(result).hasSize(1)
+ assertThat(result[0].productId).isEqualTo(DUMMY_HISTORY_PRODUCT_1.productId)
+ }
@Test
- fun 검색_기록을_최신순으로_조회한다() {
- // given
- dao.insertHistory(DUMMY_HISTORY_PRODUCT_1.copy(timestamp = 1))
- dao.insertHistory(DUMMY_HISTORY_PRODUCT_2.copy(timestamp = 0))
-
- // when
- val result = dao.getHistoryProducts()
-
- // then
- assertThat(
- result.map { it.productId },
- ).containsExactly(DUMMY_HISTORY_PRODUCT_1.productId, DUMMY_HISTORY_PRODUCT_2.productId).inOrder()
- }
+ fun 검색_기록을_최신순으로_조회한다() =
+ runTest {
+ // given
+ dao.insertHistory(DUMMY_HISTORY_PRODUCT_1.copy(timestamp = 1))
+ dao.insertHistory(DUMMY_HISTORY_PRODUCT_2.copy(timestamp = 0))
+
+ // when
+ val result = dao.getHistoryProducts()
+
+ // then
+ assertThat(
+ result.map { it.productId },
+ ).containsExactly(DUMMY_HISTORY_PRODUCT_1.productId, DUMMY_HISTORY_PRODUCT_2.productId).inOrder()
+ }
@Test
- fun 검색_기록이_제한을_초과하면_오래된_기록부터_삭제된다() {
- // given
- repeat(5) { index ->
- dao.insertHistory(
- DUMMY_HISTORY_PRODUCT_1.copy(
- productId = index.toLong(),
- timestamp = index.toLong(),
- ),
+ fun 검색_기록이_제한을_초과하면_오래된_기록부터_삭제된다() =
+ runTest {
+ // given
+ repeat(5) { index ->
+ dao.insertHistory(
+ DUMMY_HISTORY_PRODUCT_1.copy(
+ productId = index.toLong(),
+ timestamp = index.toLong(),
+ ),
+ )
+ }
+
+ // when
+ dao.insertHistoryWithLimit(
+ history = DUMMY_HISTORY_PRODUCT_2,
+ limit = 5,
)
- }
- // when
- dao.insertHistoryWithLimit(
- history = DUMMY_HISTORY_PRODUCT_2,
- limit = 5,
- )
-
- // then
- val result = dao.getHistoryProducts()
- assertThat(result).hasSize(5)
- assertThat(result.any { it.productId == 0L }).isFalse()
- assertThat(result.first().productId).isEqualTo(DUMMY_HISTORY_PRODUCT_2.productId)
- }
+ // then
+ val result = dao.getHistoryProducts()
+ assertThat(result).hasSize(5)
+ assertThat(result.any { it.productId == 0L }).isFalse()
+ assertThat(result.first().productId).isEqualTo(DUMMY_HISTORY_PRODUCT_2.productId)
+ }
@Test
- fun 최근_검색_기록을_조회한다() {
- // given
- dao.insertHistory(DUMMY_HISTORY_PRODUCT_1.copy(timestamp = 0))
- dao.insertHistory(DUMMY_HISTORY_PRODUCT_2.copy(timestamp = 1))
+ fun 최근_검색_기록을_조회한다() =
+ runTest {
+ // given
+ dao.insertHistory(DUMMY_HISTORY_PRODUCT_1.copy(timestamp = 0))
+ dao.insertHistory(DUMMY_HISTORY_PRODUCT_2.copy(timestamp = 1))
- // when
- val result = dao.getRecentHistoryProduct()
+ // when
+ val result = dao.getRecentHistoryProduct()
- // then
- assertThat(result?.productId).isEqualTo(DUMMY_HISTORY_PRODUCT_2.productId)
- }
+ // then
+ assertThat(result?.productId).isEqualTo(DUMMY_HISTORY_PRODUCT_2.productId)
+ }
@Test
- fun 총_검색_기록_개수를_조회한다() {
- // given
- dao.insertHistory(DUMMY_HISTORY_PRODUCT_1)
- dao.insertHistory(DUMMY_HISTORY_PRODUCT_2)
+ fun 총_검색_기록_개수를_조회한다() =
+ runTest {
+ // given
+ dao.insertHistory(DUMMY_HISTORY_PRODUCT_1)
+ dao.insertHistory(DUMMY_HISTORY_PRODUCT_2)
- // when
- val count = dao.getHistoryCount()
+ // when
+ val count = dao.getHistoryCount()
- // then
- assertThat(count).isEqualTo(2)
- }
+ // then
+ assertThat(count).isEqualTo(2)
+ }
@Test
- fun 오래된_검색_기록을_원하는_개수만큼_삭제한다() {
- // given
- val base = System.currentTimeMillis()
- repeat(5) { index ->
- dao.insertHistory(
- DUMMY_HISTORY_PRODUCT_1.copy(
- productId = index.toLong(),
- timestamp = base + index,
- ),
- )
+ fun 오래된_검색_기록을_원하는_개수만큼_삭제한다() =
+ runTest {
+ // given
+ val base = System.currentTimeMillis()
+ repeat(5) { index ->
+ dao.insertHistory(
+ DUMMY_HISTORY_PRODUCT_1.copy(
+ productId = index.toLong(),
+ timestamp = base + index,
+ ),
+ )
+ }
+
+ // when
+ dao.deleteOldestHistories(2)
+
+ // then
+ val result = dao.getHistoryProducts()
+ assertThat(result).hasSize(3)
+ assertThat(result.map { it.productId }).doesNotContain(0)
+ assertThat(result.map { it.productId }).doesNotContain(1)
}
- // when
- dao.deleteOldestHistories(2)
-
- // then
- val result = dao.getHistoryProducts()
- assertThat(result).hasSize(3)
- assertThat(result.map { it.productId }).doesNotContain(0)
- assertThat(result.map { it.productId }).doesNotContain(1)
- }
-
@After
fun tearDown() {
database.close()
diff --git a/app/src/androidTest/java/woowacourse/shopping/study/data/dao/CartDaoStudyTest.kt b/app/src/androidTest/java/woowacourse/shopping/study/data/dao/CartDaoStudyTest.kt
index dc8bf921cc..ece8d488da 100644
--- a/app/src/androidTest/java/woowacourse/shopping/study/data/dao/CartDaoStudyTest.kt
+++ b/app/src/androidTest/java/woowacourse/shopping/study/data/dao/CartDaoStudyTest.kt
@@ -1,6 +1,7 @@
+@file:Suppress("ktlint:standard:no-empty-file")
+
package woowacourse.shopping.study.data.dao
-class CartDaoStudyTest
/*
import android.content.Context
import android.os.SystemClock
diff --git a/app/src/androidTest/java/woowacourse/shopping/util/CoroutinesTestExtension.kt b/app/src/androidTest/java/woowacourse/shopping/util/CoroutinesTestExtension.kt
new file mode 100644
index 0000000000..298c34ef4d
--- /dev/null
+++ b/app/src/androidTest/java/woowacourse/shopping/util/CoroutinesTestExtension.kt
@@ -0,0 +1,25 @@
+package woowacourse.shopping.util
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.jupiter.api.extension.AfterEachCallback
+import org.junit.jupiter.api.extension.BeforeEachCallback
+import org.junit.jupiter.api.extension.ExtensionContext
+
+@ExperimentalCoroutinesApi
+class CoroutinesTestExtension(
+ private val dispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+) : BeforeEachCallback,
+ AfterEachCallback {
+ override fun beforeEach(context: ExtensionContext?) {
+ Dispatchers.setMain(dispatcher)
+ }
+
+ override fun afterEach(context: ExtensionContext?) {
+ Dispatchers.resetMain()
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 94ee3079b1..2939be77ac 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -18,7 +18,8 @@
tools:targetApi="31">
+ android:exported="false"
+ android:parentActivityName=".ui.catalog.CatalogActivity" />
@@ -30,7 +31,11 @@
+ android:exported="false"
+ android:parentActivityName=".ui.catalog.CatalogActivity" />
+
-
diff --git a/app/src/main/java/woowacourse/shopping/ShoppingApp.kt b/app/src/main/java/woowacourse/shopping/ShoppingApp.kt
index d60ed9ed0e..14fa0904f5 100644
--- a/app/src/main/java/woowacourse/shopping/ShoppingApp.kt
+++ b/app/src/main/java/woowacourse/shopping/ShoppingApp.kt
@@ -1,13 +1,13 @@
package woowacourse.shopping
import android.app.Application
-import woowacourse.shopping.di.DatabaseModule
-import woowacourse.shopping.di.PreferenceModule
+import woowacourse.shopping.di.DatabaseInjection
+import woowacourse.shopping.di.PreferenceInjection
class ShoppingApp : Application() {
override fun onCreate() {
super.onCreate()
- DatabaseModule.init(this)
- PreferenceModule.init(this)
+ DatabaseInjection.init(this)
+ PreferenceInjection.init(this)
}
}
diff --git a/app/src/main/java/woowacourse/shopping/data/api/CartApi.kt b/app/src/main/java/woowacourse/shopping/data/api/CartApi.kt
index d91875058c..5def9d47b4 100644
--- a/app/src/main/java/woowacourse/shopping/data/api/CartApi.kt
+++ b/app/src/main/java/woowacourse/shopping/data/api/CartApi.kt
@@ -1,6 +1,5 @@
package woowacourse.shopping.data.api
-import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
@@ -15,27 +14,27 @@ import woowacourse.shopping.data.model.response.CartItemsResponse
interface CartApi {
@GET("/cart-items")
- fun getCartItems(
+ suspend fun getCartItems(
@Query("page") page: Int,
@Query("size") size: Int,
- ): Call
+ ): CartItemsResponse
@POST("/cart-items")
- fun postCartItem(
+ suspend fun postCartItem(
@Body cartItemRequest: CartItemRequest,
- ): Call
+ ): Unit
@DELETE("/cart-items/{id}")
- fun deleteCartItem(
+ suspend fun deleteCartItem(
@Path("id") id: Long,
- ): Call
+ ): Unit
@PATCH("/cart-items/{id}")
- fun patchCartItem(
+ suspend fun patchCartItem(
@Path("id") id: Long,
@Body cartItemQuantityRequest: CartItemQuantityRequest,
- ): Call
+ ): Unit
@GET("/cart-items/counts")
- fun getCartItemsCount(): Call
+ suspend fun getCartItemsCount(): CartItemsQuantityResponse
}
diff --git a/app/src/main/java/woowacourse/shopping/data/api/CouponApi.kt b/app/src/main/java/woowacourse/shopping/data/api/CouponApi.kt
new file mode 100644
index 0000000000..1cf1ba559b
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/data/api/CouponApi.kt
@@ -0,0 +1,9 @@
+package woowacourse.shopping.data.api
+
+import retrofit2.http.GET
+import woowacourse.shopping.data.model.response.CouponResponse
+
+interface CouponApi {
+ @GET("/coupons")
+ suspend fun getCoupons(): List
+}
diff --git a/app/src/main/java/woowacourse/shopping/data/api/OrderApi.kt b/app/src/main/java/woowacourse/shopping/data/api/OrderApi.kt
index e9056a672e..d097fd66e1 100644
--- a/app/src/main/java/woowacourse/shopping/data/api/OrderApi.kt
+++ b/app/src/main/java/woowacourse/shopping/data/api/OrderApi.kt
@@ -1,13 +1,12 @@
package woowacourse.shopping.data.api
-import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST
import woowacourse.shopping.data.model.request.OrderProductsRequest
interface OrderApi {
@POST("/orders")
- fun postOrderProducts(
+ suspend fun postOrderProducts(
@Body orderProductsRequest: OrderProductsRequest,
- ): Call
+ ): Unit
}
diff --git a/app/src/main/java/woowacourse/shopping/data/api/ProductApi.kt b/app/src/main/java/woowacourse/shopping/data/api/ProductApi.kt
index 87ecb28a28..80d1ce5c7b 100644
--- a/app/src/main/java/woowacourse/shopping/data/api/ProductApi.kt
+++ b/app/src/main/java/woowacourse/shopping/data/api/ProductApi.kt
@@ -1,6 +1,5 @@
package woowacourse.shopping.data.api
-import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
@@ -9,14 +8,14 @@ import woowacourse.shopping.data.model.response.ProductsResponse
interface ProductApi {
@GET("/products")
- fun getProducts(
+ suspend fun getProducts(
@Query("category") category: String? = null,
@Query("page") page: Int,
@Query("size") size: Int,
- ): Call
+ ): ProductsResponse
@GET("/products/{id}")
- fun getProductDetail(
+ suspend fun getProduct(
@Path("id") id: Long,
- ): Call
+ ): ProductDetailResponse
}
diff --git a/app/src/main/java/woowacourse/shopping/data/dao/HistoryDao.kt b/app/src/main/java/woowacourse/shopping/data/dao/HistoryDao.kt
index f5d4d086ca..2f30b920fc 100644
--- a/app/src/main/java/woowacourse/shopping/data/dao/HistoryDao.kt
+++ b/app/src/main/java/woowacourse/shopping/data/dao/HistoryDao.kt
@@ -10,10 +10,10 @@ import woowacourse.shopping.data.model.entity.HistoryProductEntity
@Dao
interface HistoryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
- fun insertHistory(history: HistoryProductEntity)
+ suspend fun insertHistory(history: HistoryProductEntity)
@Transaction
- fun insertHistoryWithLimit(
+ suspend fun insertHistoryWithLimit(
history: HistoryProductEntity,
limit: Int,
) {
@@ -26,10 +26,10 @@ interface HistoryDao {
}
@Query("SELECT * FROM search_history ORDER BY timestamp DESC")
- fun getHistoryProducts(): List
+ suspend fun getHistoryProducts(): List
@Query("SELECT COUNT(*) FROM search_history")
- fun getHistoryCount(): Int
+ suspend fun getHistoryCount(): Int
@Query(
"""
@@ -42,8 +42,8 @@ interface HistoryDao {
)
""",
)
- fun deleteOldestHistories(count: Int)
+ suspend fun deleteOldestHistories(count: Int)
@Query("SELECT * FROM search_history ORDER BY timestamp DESC LIMIT 1")
- fun getRecentHistoryProduct(): HistoryProductEntity?
+ suspend fun getRecentHistoryProduct(): HistoryProductEntity?
}
diff --git a/app/src/main/java/woowacourse/shopping/data/interceptor/ShoppingAuthInterceptor.kt b/app/src/main/java/woowacourse/shopping/data/interceptor/ShoppingAuthInterceptor.kt
index 521c1be36a..e8c6dce77a 100644
--- a/app/src/main/java/woowacourse/shopping/data/interceptor/ShoppingAuthInterceptor.kt
+++ b/app/src/main/java/woowacourse/shopping/data/interceptor/ShoppingAuthInterceptor.kt
@@ -3,13 +3,14 @@ package woowacourse.shopping.data.interceptor
import android.util.Base64
import okhttp3.Interceptor
import okhttp3.Response
+import woowacourse.shopping.BuildConfig
+import woowacourse.shopping.data.preference.AuthSharedPreference
class ShoppingAuthInterceptor(
- private val username: String,
- private val password: String,
+ private val authSharedPreference: AuthSharedPreference,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
- val credentials = "$username:$password"
+ val credentials = "${getUsername()}:${getPassword()}"
val basic = "Basic " + Base64.encodeToString(credentials.toByteArray(), Base64.NO_WRAP)
val newRequest =
@@ -21,4 +22,16 @@ class ShoppingAuthInterceptor(
return chain.proceed(newRequest)
}
+
+ private fun getUsername(): String =
+ authSharedPreference.getAuthUsername()
+ ?: BuildConfig.DEFAULT_USERNAME.apply {
+ authSharedPreference.putAuthId(this)
+ }
+
+ private fun getPassword(): String =
+ authSharedPreference.getAuthPassword()
+ ?: BuildConfig.DEFAULT_PASSWORD.apply {
+ authSharedPreference.putAuthPassword(this)
+ }
}
diff --git a/app/src/main/java/woowacourse/shopping/data/model/response/CartItemsResponse.kt b/app/src/main/java/woowacourse/shopping/data/model/response/CartItemsResponse.kt
index f71b5ea2d2..f2391ac9e3 100644
--- a/app/src/main/java/woowacourse/shopping/data/model/response/CartItemsResponse.kt
+++ b/app/src/main/java/woowacourse/shopping/data/model/response/CartItemsResponse.kt
@@ -18,9 +18,9 @@ data class CartItemsResponse(
@SerialName("number")
val number: Int,
@SerialName("sort")
- val sort: Sort,
+ val sort: SortResponse,
@SerialName("pageable")
- val pageable: Pageable,
+ val pageable: PageableResponse,
@SerialName("first")
val first: Boolean,
@SerialName("last")
@@ -69,40 +69,4 @@ data class CartItemsResponse(
)
}
}
-
- @Serializable
- data class Sort(
- @SerialName("empty")
- val empty: Boolean,
- @SerialName("sorted")
- val sorted: Boolean,
- @SerialName("unsorted")
- val unsorted: Boolean,
- )
-
- @Serializable
- data class Pageable(
- @SerialName("offset")
- val offset: Long,
- @SerialName("sort")
- val sort: Sort,
- @SerialName("paged")
- val paged: Boolean,
- @SerialName("pageNumber")
- val pageNumber: Int,
- @SerialName("pageSize")
- val pageSize: Int,
- @SerialName("unpaged")
- val unpaged: Boolean,
- ) {
- @Serializable
- data class Sort(
- @SerialName("empty")
- val empty: Boolean,
- @SerialName("sorted")
- val sorted: Boolean,
- @SerialName("unsorted")
- val unsorted: Boolean,
- )
- }
}
diff --git a/app/src/main/java/woowacourse/shopping/data/model/response/CouponResponse.kt b/app/src/main/java/woowacourse/shopping/data/model/response/CouponResponse.kt
new file mode 100644
index 0000000000..1724d1c133
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/data/model/response/CouponResponse.kt
@@ -0,0 +1,80 @@
+package woowacourse.shopping.data.model.response
+
+import android.util.Log
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import woowacourse.shopping.domain.model.CouponDetail
+import woowacourse.shopping.domain.model.CouponDiscountType
+import woowacourse.shopping.domain.model.CouponDiscountType.BUY_X_GET_Y
+import woowacourse.shopping.domain.model.CouponDiscountType.FIXED
+import woowacourse.shopping.domain.model.CouponDiscountType.FREE_SHIPPING
+import woowacourse.shopping.domain.model.CouponDiscountType.PERCENTAGE
+import java.time.LocalDate
+import java.time.LocalTime
+
+@Serializable
+data class CouponResponse(
+ @SerialName("id")
+ val id: Int,
+ @SerialName("code")
+ val code: String,
+ @SerialName("description")
+ val description: String,
+ @SerialName("expirationDate")
+ val expirationDate: String,
+ @SerialName("discount")
+ val discount: Int?,
+ @SerialName("minimumAmount")
+ val minimumAmount: Int?,
+ @SerialName("discountType")
+ val discountType: String,
+ @SerialName("buyQuantity")
+ val buyQuantity: Int?,
+ @SerialName("getQuantity")
+ val getQuantity: Int?,
+ @SerialName("availableTime")
+ val availableTime: AvailableTime?,
+) {
+ @Serializable
+ data class AvailableTime(
+ @SerialName("start")
+ val start: String,
+ @SerialName("end")
+ val end: String,
+ )
+
+ companion object {
+ fun CouponResponse.toDomain(): CouponDetail? {
+ return CouponDetail(
+ id = id,
+ code = code,
+ name = description,
+ expirationDate = LocalDate.parse(expirationDate),
+ discount = discount,
+ minimumPurchase = minimumAmount,
+ discountType = discountType.toDiscountType() ?: return null,
+ buyQuantity = buyQuantity,
+ getQuantity = getQuantity,
+ availableTime =
+ availableTime?.let {
+ CouponDetail.AvailableTime(
+ start = LocalTime.parse(it.start),
+ end = LocalTime.parse(it.end),
+ )
+ },
+ )
+ }
+
+ private fun String.toDiscountType(): CouponDiscountType? =
+ when (this) {
+ "fixed" -> FIXED
+ "buyXgetY" -> BUY_X_GET_Y
+ "freeShipping" -> FREE_SHIPPING
+ "percentage" -> PERCENTAGE
+ else -> {
+ Log.e("[CouponResponse]", "유효하지 않은 쿠폰 타입이 존재합니다!")
+ null
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/data/model/response/PageableResponse.kt b/app/src/main/java/woowacourse/shopping/data/model/response/PageableResponse.kt
new file mode 100644
index 0000000000..d30991ac99
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/data/model/response/PageableResponse.kt
@@ -0,0 +1,20 @@
+package woowacourse.shopping.data.model.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class PageableResponse(
+ @SerialName("offset")
+ val offset: Long,
+ @SerialName("sort")
+ val sort: SortResponse,
+ @SerialName("paged")
+ val paged: Boolean,
+ @SerialName("pageNumber")
+ val pageNumber: Int,
+ @SerialName("pageSize")
+ val pageSize: Int,
+ @SerialName("unpaged")
+ val unpaged: Boolean,
+)
diff --git a/app/src/main/java/woowacourse/shopping/data/model/response/ProductDetailResponse.kt b/app/src/main/java/woowacourse/shopping/data/model/response/ProductDetailResponse.kt
index abcd281fb8..0ca6a78cfa 100644
--- a/app/src/main/java/woowacourse/shopping/data/model/response/ProductDetailResponse.kt
+++ b/app/src/main/java/woowacourse/shopping/data/model/response/ProductDetailResponse.kt
@@ -2,6 +2,7 @@ package woowacourse.shopping.data.model.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import woowacourse.shopping.domain.model.ProductDetail
@Serializable
data class ProductDetailResponse(
@@ -15,4 +16,15 @@ data class ProductDetailResponse(
val imageUrl: String,
@SerialName("category")
val category: String,
-)
+) {
+ companion object {
+ fun ProductDetailResponse.toDomain(): ProductDetail =
+ ProductDetail(
+ id = id,
+ name = name,
+ category = category,
+ imageUrl = imageUrl,
+ price = price,
+ )
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/data/model/response/ProductsResponse.kt b/app/src/main/java/woowacourse/shopping/data/model/response/ProductsResponse.kt
index 9d038f004c..242fc02f4a 100644
--- a/app/src/main/java/woowacourse/shopping/data/model/response/ProductsResponse.kt
+++ b/app/src/main/java/woowacourse/shopping/data/model/response/ProductsResponse.kt
@@ -18,9 +18,9 @@ data class ProductsResponse(
@SerialName("number")
val number: Int,
@SerialName("sort")
- val sort: Sort,
+ val sort: SortResponse,
@SerialName("pageable")
- val pageable: Pageable,
+ val pageable: PageableResponse,
@SerialName("first")
val first: Boolean,
@SerialName("last")
@@ -58,40 +58,4 @@ data class ProductsResponse(
)
}
}
-
- @Serializable
- data class Sort(
- @SerialName("empty")
- val empty: Boolean,
- @SerialName("sorted")
- val sorted: Boolean,
- @SerialName("unsorted")
- val unsorted: Boolean,
- )
-
- @Serializable
- data class Pageable(
- @SerialName("offset")
- val offset: Long,
- @SerialName("sort")
- val sort: Sort,
- @SerialName("paged")
- val paged: Boolean,
- @SerialName("pageNumber")
- val pageNumber: Int,
- @SerialName("pageSize")
- val pageSize: Int,
- @SerialName("unpaged")
- val unpaged: Boolean,
- ) {
- @Serializable
- data class Sort(
- @SerialName("empty")
- val empty: Boolean,
- @SerialName("sorted")
- val sorted: Boolean,
- @SerialName("unsorted")
- val unsorted: Boolean,
- )
- }
}
diff --git a/app/src/main/java/woowacourse/shopping/data/model/response/SortResponse.kt b/app/src/main/java/woowacourse/shopping/data/model/response/SortResponse.kt
new file mode 100644
index 0000000000..4af05b81c4
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/data/model/response/SortResponse.kt
@@ -0,0 +1,14 @@
+package woowacourse.shopping.data.model.response
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SortResponse(
+ @SerialName("empty")
+ val empty: Boolean,
+ @SerialName("sorted")
+ val sorted: Boolean,
+ @SerialName("unsorted")
+ val unsorted: Boolean,
+)
diff --git a/app/src/main/java/woowacourse/shopping/data/repository/CartRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/CartRepository.kt
index 4691ed028d..fef8b5bf17 100644
--- a/app/src/main/java/woowacourse/shopping/data/repository/CartRepository.kt
+++ b/app/src/main/java/woowacourse/shopping/data/repository/CartRepository.kt
@@ -1,13 +1,8 @@
package woowacourse.shopping.data.repository
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
import woowacourse.shopping.data.api.CartApi
import woowacourse.shopping.data.model.request.CartItemQuantityRequest
import woowacourse.shopping.data.model.request.CartItemRequest
-import woowacourse.shopping.data.model.response.CartItemsQuantityResponse
-import woowacourse.shopping.data.model.response.CartItemsResponse
import woowacourse.shopping.data.model.response.CartItemsResponse.Content.Companion.toDomain
import woowacourse.shopping.domain.model.Page
import woowacourse.shopping.domain.model.Products
@@ -16,129 +11,43 @@ import woowacourse.shopping.domain.repository.CartRepository
class CartRepository(
private val api: CartApi,
) : CartRepository {
- override fun fetchCartProducts(
+ override suspend fun fetchCartProducts(
page: Int,
size: Int,
- callback: (Result) -> Unit,
- ) {
- api.getCartItems(page, size).enqueue(
- object : Callback {
- override fun onResponse(
- call: Call,
- response: Response,
- ) {
- val body = response.body()
- val items = body?.content?.map { it.toDomain() } ?: emptyList()
- val pageInfo = Page(page, body?.first ?: true, body?.last ?: true)
- callback(Result.success(Products(items, pageInfo)))
- }
-
- override fun onFailure(
- call: Call,
- t: Throwable,
- ) {
- callback(Result.failure(t))
- }
- },
- )
+ ): Products {
+ val response = api.getCartItems(page, size)
+ val items = response.content.map { it.toDomain() }
+ val pageInfo = Page(page, response.first, response.last)
+ return Products(items, pageInfo)
}
- override fun fetchAllCartProducts(callback: (Result) -> Unit) {
+ override suspend fun fetchAllCartProducts(): Products {
val firstPage = 0
val maxSize = Int.MAX_VALUE
- fetchCartProducts(firstPage, maxSize, callback)
- }
- override fun fetchCartItemCount(callback: (Result) -> Unit) {
- api.getCartItemsCount().enqueue(
- object : Callback {
- override fun onResponse(
- call: Call,
- response: Response,
- ) {
- callback(Result.success(response.body()?.quantity ?: 0))
- }
+ return fetchCartProducts(firstPage, maxSize)
+ }
- override fun onFailure(
- call: Call,
- t: Throwable,
- ) {
- callback(Result.failure(t))
- }
- },
- )
+ override suspend fun fetchCartItemCount(): Int {
+ val response = api.getCartItemsCount()
+ return response.quantity
}
- override fun addCartProduct(
+ override suspend fun addCartProduct(
productId: Long,
quantity: Int,
- callback: (Result) -> Unit,
) {
- val request = CartItemRequest(productId = productId, quantity = quantity)
- api.postCartItem(request).enqueue(
- object : Callback {
- override fun onResponse(
- call: Call,
- response: Response,
- ) {
- callback(Result.success(Unit))
- }
-
- override fun onFailure(
- call: Call,
- t: Throwable,
- ) {
- callback(Result.failure(t))
- }
- },
- )
+ api.postCartItem(CartItemRequest(productId = productId, quantity = quantity))
}
- override fun updateCartProduct(
+ override suspend fun updateCartProduct(
cartId: Long,
quantity: Int,
- callback: (Result) -> Unit,
) {
- val request = CartItemQuantityRequest(quantity)
- api.patchCartItem(cartId, request).enqueue(
- object : Callback {
- override fun onResponse(
- call: Call,
- response: Response,
- ) {
- callback(Result.success(Unit))
- }
-
- override fun onFailure(
- call: Call,
- t: Throwable,
- ) {
- callback(Result.failure(t))
- }
- },
- )
+ api.patchCartItem(cartId, CartItemQuantityRequest(quantity))
}
- override fun deleteCartProduct(
- cartId: Long,
- callback: (Result) -> Unit,
- ) {
- api.deleteCartItem(cartId).enqueue(
- object : Callback {
- override fun onResponse(
- call: Call,
- response: Response,
- ) {
- callback(Result.success(Unit))
- }
-
- override fun onFailure(
- call: Call,
- t: Throwable,
- ) {
- callback(Result.failure(t))
- }
- },
- )
+ override suspend fun deleteCartProduct(cartId: Long) {
+ api.deleteCartItem(cartId)
}
}
diff --git a/app/src/main/java/woowacourse/shopping/data/repository/CouponRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/CouponRepository.kt
new file mode 100644
index 0000000000..57ede9cc35
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/data/repository/CouponRepository.kt
@@ -0,0 +1,16 @@
+package woowacourse.shopping.data.repository
+
+import woowacourse.shopping.data.api.CouponApi
+import woowacourse.shopping.data.model.response.CouponResponse.Companion.toDomain
+import woowacourse.shopping.domain.model.Coupon
+import woowacourse.shopping.domain.model.CouponDetail.Companion.toCoupon
+import woowacourse.shopping.domain.repository.CouponRepository
+
+class CouponRepository(
+ private val api: CouponApi,
+) : CouponRepository {
+ override suspend fun fetchAllCoupons(): List =
+ api
+ .getCoupons()
+ .mapNotNull { it.toDomain()?.toCoupon() }
+}
diff --git a/app/src/main/java/woowacourse/shopping/data/repository/HistoryRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/HistoryRepository.kt
index 6aa1f2c975..de5500535c 100644
--- a/app/src/main/java/woowacourse/shopping/data/repository/HistoryRepository.kt
+++ b/app/src/main/java/woowacourse/shopping/data/repository/HistoryRepository.kt
@@ -6,42 +6,27 @@ import woowacourse.shopping.data.model.entity.HistoryProductEntity.Companion.toD
import woowacourse.shopping.domain.model.HistoryProduct
import woowacourse.shopping.domain.model.ProductDetail
import woowacourse.shopping.domain.repository.HistoryRepository
-import kotlin.concurrent.thread
class HistoryRepository(
private val dao: HistoryDao,
) : HistoryRepository {
- override fun fetchAllHistory(callback: (List) -> Unit) {
- thread {
- callback(
- dao.getHistoryProducts().map { it.toDomain() },
- )
- }
- }
+ override suspend fun fetchAllHistory(): List = dao.getHistoryProducts().map { it.toDomain() }
+
+ override suspend fun fetchRecentHistory(): HistoryProduct? = dao.getRecentHistoryProduct()?.toDomain()
- override fun addHistoryWithLimit(
+ override suspend fun addHistoryWithLimit(
productDetail: ProductDetail,
limit: Int,
) {
- thread {
- dao.insertHistoryWithLimit(
- history =
- HistoryProductEntity(
- productId = productDetail.id,
- name = productDetail.name,
- imageUrl = productDetail.imageUrl,
- category = productDetail.category,
- ),
- limit = limit,
- )
- }
- }
-
- override fun fetchRecentHistory(callback: (HistoryProduct?) -> Unit) {
- thread {
- callback(
- dao.getRecentHistoryProduct()?.toDomain(),
- )
- }
+ dao.insertHistoryWithLimit(
+ history =
+ HistoryProductEntity(
+ productId = productDetail.id,
+ name = productDetail.name,
+ imageUrl = productDetail.imageUrl,
+ category = productDetail.category,
+ ),
+ limit = limit,
+ )
}
}
diff --git a/app/src/main/java/woowacourse/shopping/data/repository/OrderRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/OrderRepository.kt
index a84f14b4d0..90ea369a08 100644
--- a/app/src/main/java/woowacourse/shopping/data/repository/OrderRepository.kt
+++ b/app/src/main/java/woowacourse/shopping/data/repository/OrderRepository.kt
@@ -1,8 +1,5 @@
package woowacourse.shopping.data.repository
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
import woowacourse.shopping.data.api.OrderApi
import woowacourse.shopping.data.model.request.OrderProductsRequest
import woowacourse.shopping.domain.repository.OrderRepository
@@ -10,26 +7,7 @@ import woowacourse.shopping.domain.repository.OrderRepository
class OrderRepository(
private val api: OrderApi,
) : OrderRepository {
- override fun postOrderProducts(
- cartIds: List,
- callback: (Result) -> Unit,
- ) {
- api.postOrderProducts(OrderProductsRequest(cartIds)).enqueue(
- object : Callback {
- override fun onResponse(
- call: Call,
- response: Response,
- ) {
- callback(Result.success(Unit))
- }
-
- override fun onFailure(
- call: Call,
- t: Throwable,
- ) {
- callback(Result.failure(t))
- }
- },
- )
+ override suspend fun postOrderProducts(cartIds: List) {
+ api.postOrderProducts(OrderProductsRequest(cartIds))
}
}
diff --git a/app/src/main/java/woowacourse/shopping/data/repository/ProductRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/ProductRepository.kt
index dbdf3bdf4c..8af2484dad 100644
--- a/app/src/main/java/woowacourse/shopping/data/repository/ProductRepository.kt
+++ b/app/src/main/java/woowacourse/shopping/data/repository/ProductRepository.kt
@@ -1,68 +1,34 @@
package woowacourse.shopping.data.repository
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
import woowacourse.shopping.data.api.ProductApi
-import woowacourse.shopping.data.model.response.ProductsResponse
+import woowacourse.shopping.data.model.response.ProductDetailResponse.Companion.toDomain
import woowacourse.shopping.data.model.response.ProductsResponse.Content.Companion.toDomain
import woowacourse.shopping.domain.model.Page
import woowacourse.shopping.domain.model.Product
+import woowacourse.shopping.domain.model.ProductDetail
import woowacourse.shopping.domain.model.Products
import woowacourse.shopping.domain.repository.ProductRepository
class ProductRepository(
private val api: ProductApi,
) : ProductRepository {
- override fun fetchProducts(
+ override suspend fun fetchProducts(
page: Int,
size: Int,
category: String?,
- callback: (Result) -> Unit,
- ) {
- api.getProducts(category, page, size).enqueue(
- object : Callback {
- override fun onResponse(
- call: Call,
- response: Response,
- ) {
- val body = response.body()
- val items = body?.content?.map { it.toDomain() } ?: emptyList()
- val pageInfo = Page(page, body?.first ?: false, body?.last ?: false)
- callback(Result.success(Products(items, pageInfo)))
- }
-
- override fun onFailure(
- call: Call,
- t: Throwable,
- ) {
- callback(Result.failure(t))
- }
- },
- )
+ ): Products {
+ val response = api.getProducts(category, page, size)
+ val items = response.content.map { it.toDomain() }
+ val pageInfo = Page(page, response.first, response.last)
+ return Products(items, pageInfo)
}
- override fun fetchAllProducts(callback: (Result>) -> Unit) {
+ override suspend fun fetchAllProducts(): List {
val firstPage = 0
val maxSize = Int.MAX_VALUE
-
- api.getProducts(page = firstPage, size = maxSize).enqueue(
- object : Callback {
- override fun onResponse(
- call: Call,
- response: Response,
- ) {
- val products = response.body()?.content?.map { it.toDomain() } ?: emptyList()
- callback(Result.success(products))
- }
-
- override fun onFailure(
- call: Call,
- t: Throwable,
- ) {
- callback(Result.failure(t))
- }
- },
- )
+ val response = api.getProducts(page = firstPage, size = maxSize)
+ return response.content.map { it.toDomain() }
}
+
+ override suspend fun fetchProduct(productId: Long): ProductDetail = api.getProduct(productId).toDomain()
}
diff --git a/app/src/main/java/woowacourse/shopping/di/DatabaseModule.kt b/app/src/main/java/woowacourse/shopping/di/DatabaseInjection.kt
similarity index 94%
rename from app/src/main/java/woowacourse/shopping/di/DatabaseModule.kt
rename to app/src/main/java/woowacourse/shopping/di/DatabaseInjection.kt
index 7ef48d3429..078d825cbd 100644
--- a/app/src/main/java/woowacourse/shopping/di/DatabaseModule.kt
+++ b/app/src/main/java/woowacourse/shopping/di/DatabaseInjection.kt
@@ -3,7 +3,7 @@ package woowacourse.shopping.di
import android.content.Context
import woowacourse.shopping.data.database.ShoppingDatabase
-object DatabaseModule {
+object DatabaseInjection {
private var _database: ShoppingDatabase? = null
val database: ShoppingDatabase get() = _database ?: throw IllegalStateException()
diff --git a/app/src/main/java/woowacourse/shopping/di/NetworkModule.kt b/app/src/main/java/woowacourse/shopping/di/NetworkInjection.kt
similarity index 64%
rename from app/src/main/java/woowacourse/shopping/di/NetworkModule.kt
rename to app/src/main/java/woowacourse/shopping/di/NetworkInjection.kt
index c493220ce3..702d39efea 100644
--- a/app/src/main/java/woowacourse/shopping/di/NetworkModule.kt
+++ b/app/src/main/java/woowacourse/shopping/di/NetworkInjection.kt
@@ -5,26 +5,18 @@ import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import woowacourse.shopping.BuildConfig
import woowacourse.shopping.data.api.CartApi
+import woowacourse.shopping.data.api.CouponApi
import woowacourse.shopping.data.api.OrderApi
import woowacourse.shopping.data.api.ProductApi
import woowacourse.shopping.data.interceptor.ShoppingAuthInterceptor
-import woowacourse.shopping.di.PreferenceModule.authSharedPreference
+import woowacourse.shopping.di.PreferenceInjection.authSharedPreference
-object NetworkModule {
+object NetworkInjection {
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient
.Builder()
.addInterceptor(
- ShoppingAuthInterceptor(
- authSharedPreference.getAuthUsername()
- ?: BuildConfig.DEFAULT_USERNAME.apply {
- authSharedPreference.putAuthId(this)
- },
- authSharedPreference.getAuthPassword()
- ?: BuildConfig.DEFAULT_PASSWORD.apply {
- authSharedPreference.putAuthPassword(this)
- },
- ),
+ ShoppingAuthInterceptor(authSharedPreference),
).build()
}
@@ -46,4 +38,7 @@ object NetworkModule {
val orderApi: OrderApi by lazy {
retrofit.create(OrderApi::class.java)
}
+ val couponApi: CouponApi by lazy {
+ retrofit.create(CouponApi::class.java)
+ }
}
diff --git a/app/src/main/java/woowacourse/shopping/di/PreferenceModule.kt b/app/src/main/java/woowacourse/shopping/di/PreferenceInjection.kt
similarity index 94%
rename from app/src/main/java/woowacourse/shopping/di/PreferenceModule.kt
rename to app/src/main/java/woowacourse/shopping/di/PreferenceInjection.kt
index c9946318b3..47d8fcfcd9 100644
--- a/app/src/main/java/woowacourse/shopping/di/PreferenceModule.kt
+++ b/app/src/main/java/woowacourse/shopping/di/PreferenceInjection.kt
@@ -5,7 +5,7 @@ import android.content.Context
import woowacourse.shopping.data.preference.AuthSharedPreference
@SuppressLint("StaticFieldLeak")
-object PreferenceModule {
+object PreferenceInjection {
private var _authSharedPreference: AuthSharedPreference? = null
val authSharedPreference: AuthSharedPreference get() = _authSharedPreference ?: throw IllegalStateException()
diff --git a/app/src/main/java/woowacourse/shopping/di/RepositoryInjection.kt b/app/src/main/java/woowacourse/shopping/di/RepositoryInjection.kt
new file mode 100644
index 0000000000..515bbd4684
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/di/RepositoryInjection.kt
@@ -0,0 +1,39 @@
+package woowacourse.shopping.di
+
+import woowacourse.shopping.di.DatabaseInjection.database
+import woowacourse.shopping.di.NetworkInjection.cartApi
+import woowacourse.shopping.di.NetworkInjection.couponApi
+import woowacourse.shopping.di.NetworkInjection.orderApi
+import woowacourse.shopping.di.NetworkInjection.productApi
+import woowacourse.shopping.domain.repository.CartRepository
+import woowacourse.shopping.domain.repository.CouponRepository
+import woowacourse.shopping.domain.repository.HistoryRepository
+import woowacourse.shopping.domain.repository.OrderRepository
+import woowacourse.shopping.domain.repository.ProductRepository
+
+object RepositoryInjection {
+ val cartRepository: CartRepository by lazy {
+ woowacourse.shopping.data.repository
+ .CartRepository(cartApi)
+ }
+
+ val productRepository: ProductRepository by lazy {
+ woowacourse.shopping.data.repository
+ .ProductRepository(productApi)
+ }
+
+ val historyRepository: HistoryRepository by lazy {
+ woowacourse.shopping.data.repository
+ .HistoryRepository(database.historyDao())
+ }
+
+ val orderRepository: OrderRepository by lazy {
+ woowacourse.shopping.data.repository
+ .OrderRepository(orderApi)
+ }
+
+ val couponRepository: CouponRepository by lazy {
+ woowacourse.shopping.data.repository
+ .CouponRepository(couponApi)
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/di/RepositoryModule.kt b/app/src/main/java/woowacourse/shopping/di/RepositoryModule.kt
deleted file mode 100644
index 261405e7ad..0000000000
--- a/app/src/main/java/woowacourse/shopping/di/RepositoryModule.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package woowacourse.shopping.di
-
-import woowacourse.shopping.di.DatabaseModule.database
-import woowacourse.shopping.di.NetworkModule.cartApi
-import woowacourse.shopping.di.NetworkModule.orderApi
-import woowacourse.shopping.di.NetworkModule.productApi
-
-object RepositoryModule {
- val cartRepository: woowacourse.shopping.domain.repository.CartRepository by lazy {
- woowacourse.shopping.data.repository
- .CartRepository(cartApi)
- }
-
- val productRepository: woowacourse.shopping.domain.repository.ProductRepository by lazy {
- woowacourse.shopping.data.repository
- .ProductRepository(productApi)
- }
-
- val historyRepository: woowacourse.shopping.domain.repository.HistoryRepository by lazy {
- woowacourse.shopping.data.repository
- .HistoryRepository(database.historyDao())
- }
-
- val orderRepository: woowacourse.shopping.domain.repository.OrderRepository by lazy {
- woowacourse.shopping.data.repository
- .OrderRepository(orderApi)
- }
-}
diff --git a/app/src/main/java/woowacourse/shopping/di/UseCaseModule.kt b/app/src/main/java/woowacourse/shopping/di/UseCaseInjection.kt
similarity index 75%
rename from app/src/main/java/woowacourse/shopping/di/UseCaseModule.kt
rename to app/src/main/java/woowacourse/shopping/di/UseCaseInjection.kt
index 6f11ae71bb..aebf55968a 100644
--- a/app/src/main/java/woowacourse/shopping/di/UseCaseModule.kt
+++ b/app/src/main/java/woowacourse/shopping/di/UseCaseInjection.kt
@@ -1,17 +1,19 @@
package woowacourse.shopping.di
-import woowacourse.shopping.di.RepositoryModule.cartRepository
-import woowacourse.shopping.di.RepositoryModule.historyRepository
-import woowacourse.shopping.di.RepositoryModule.orderRepository
-import woowacourse.shopping.di.RepositoryModule.productRepository
+import woowacourse.shopping.di.RepositoryInjection.cartRepository
+import woowacourse.shopping.di.RepositoryInjection.couponRepository
+import woowacourse.shopping.di.RepositoryInjection.historyRepository
+import woowacourse.shopping.di.RepositoryInjection.orderRepository
+import woowacourse.shopping.di.RepositoryInjection.productRepository
import woowacourse.shopping.domain.usecase.AddSearchHistoryUseCase
import woowacourse.shopping.domain.usecase.DecreaseCartProductQuantityUseCase
import woowacourse.shopping.domain.usecase.GetCartProductsQuantityUseCase
import woowacourse.shopping.domain.usecase.GetCartProductsUseCase
import woowacourse.shopping.domain.usecase.GetCartRecommendProductsUseCase
import woowacourse.shopping.domain.usecase.GetCatalogProductUseCase
-import woowacourse.shopping.domain.usecase.GetCatalogProductsByIdsUseCase
+import woowacourse.shopping.domain.usecase.GetCatalogProductsByProductIdsUseCase
import woowacourse.shopping.domain.usecase.GetCatalogProductsUseCase
+import woowacourse.shopping.domain.usecase.GetCouponsUseCase
import woowacourse.shopping.domain.usecase.GetRecentSearchHistoryUseCase
import woowacourse.shopping.domain.usecase.GetSearchHistoryUseCase
import woowacourse.shopping.domain.usecase.IncreaseCartProductQuantityUseCase
@@ -19,7 +21,7 @@ import woowacourse.shopping.domain.usecase.OrderProductsUseCase
import woowacourse.shopping.domain.usecase.RemoveCartProductUseCase
import woowacourse.shopping.domain.usecase.UpdateCartProductUseCase
-object UseCaseModule {
+object UseCaseInjection {
val getCartProductsUseCase by lazy {
GetCartProductsUseCase(cartRepository)
}
@@ -60,8 +62,8 @@ object UseCaseModule {
GetCatalogProductUseCase(productRepository, cartRepository)
}
- val getCatalogProductsByIdsUseCase by lazy {
- GetCatalogProductsByIdsUseCase(productRepository, cartRepository)
+ val getCatalogProductsByProductIdsUseCase by lazy {
+ GetCatalogProductsByProductIdsUseCase(productRepository, cartRepository)
}
val getCartProductsQuantityUseCase by lazy {
@@ -73,6 +75,9 @@ object UseCaseModule {
}
val orderProductsUseCase by lazy {
- OrderProductsUseCase(productRepository, cartRepository, orderRepository)
+ OrderProductsUseCase(orderRepository)
+ }
+ val getCouponsUseCase by lazy {
+ GetCouponsUseCase(couponRepository)
}
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/Coupon.kt b/app/src/main/java/woowacourse/shopping/domain/model/Coupon.kt
new file mode 100644
index 0000000000..60c514e6d7
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/Coupon.kt
@@ -0,0 +1,24 @@
+package woowacourse.shopping.domain.model
+
+import java.time.LocalDateTime
+
+sealed interface Coupon {
+ val detail: CouponDetail
+
+ val isSelected: Boolean
+
+ fun apply(
+ products: Products,
+ nowDateTime: LocalDateTime = LocalDateTime.now(),
+ ): Price
+
+ fun getIsAvailable(
+ products: Products,
+ nowDateTime: LocalDateTime = LocalDateTime.now(),
+ ): Boolean
+
+ fun copy(
+ detail: CouponDetail = this.detail,
+ isSelected: Boolean = this.isSelected,
+ ): Coupon
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/CouponDetail.kt b/app/src/main/java/woowacourse/shopping/domain/model/CouponDetail.kt
new file mode 100644
index 0000000000..6d61189aba
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/CouponDetail.kt
@@ -0,0 +1,36 @@
+package woowacourse.shopping.domain.model
+
+import woowacourse.shopping.domain.model.CouponDiscountType.BUY_X_GET_Y
+import woowacourse.shopping.domain.model.CouponDiscountType.FIXED
+import woowacourse.shopping.domain.model.CouponDiscountType.FREE_SHIPPING
+import woowacourse.shopping.domain.model.CouponDiscountType.PERCENTAGE
+import java.time.LocalDate
+import java.time.LocalTime
+
+data class CouponDetail(
+ val id: Int,
+ val code: String,
+ val name: String,
+ val expirationDate: LocalDate,
+ val discount: Int?,
+ val minimumPurchase: Int?,
+ val discountType: CouponDiscountType,
+ val buyQuantity: Int?,
+ val getQuantity: Int?,
+ val availableTime: AvailableTime?,
+) {
+ data class AvailableTime(
+ val start: LocalTime,
+ val end: LocalTime,
+ )
+
+ companion object {
+ fun CouponDetail.toCoupon(): Coupon =
+ when (this.discountType) {
+ FIXED -> FixedDiscountCoupon(this)
+ PERCENTAGE -> PercentageDiscountCoupon(this)
+ FREE_SHIPPING -> FreeShippingCoupon(this)
+ BUY_X_GET_Y -> QuantityBonusCoupon(this)
+ }
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/CouponDiscountType.kt b/app/src/main/java/woowacourse/shopping/domain/model/CouponDiscountType.kt
new file mode 100644
index 0000000000..09335abc06
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/CouponDiscountType.kt
@@ -0,0 +1,8 @@
+package woowacourse.shopping.domain.model
+
+enum class CouponDiscountType {
+ FIXED,
+ BUY_X_GET_Y,
+ FREE_SHIPPING,
+ PERCENTAGE,
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/Coupons.kt b/app/src/main/java/woowacourse/shopping/domain/model/Coupons.kt
new file mode 100644
index 0000000000..a739e62dcf
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/Coupons.kt
@@ -0,0 +1,42 @@
+package woowacourse.shopping.domain.model
+
+import java.time.LocalDateTime
+
+@JvmInline
+value class Coupons(
+ val value: List,
+) {
+ fun selectCoupon(couponId: Int): Coupons {
+ val selectedCoupon = value.find { it.detail.id == couponId } ?: return this
+
+ return Coupons(
+ value.map {
+ when {
+ it.detail.id == couponId -> it.copy(isSelected = !selectedCoupon.isSelected)
+ else -> it.copy(isSelected = false)
+ }
+ },
+ )
+ }
+
+ fun applyCoupon(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Price =
+ value.find { it.isSelected }?.apply(products, nowDateTime)
+ ?: Price(products.selectedProductsPrice)
+
+ fun filterAvailableCoupons(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Coupons =
+ Coupons(
+ value.filter { coupon ->
+ coupon.getIsAvailable(products, nowDateTime)
+ },
+ )
+
+ companion object {
+ val EMPTY_COUPONS: Coupons = Coupons(emptyList())
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/FixedDiscountCoupon.kt b/app/src/main/java/woowacourse/shopping/domain/model/FixedDiscountCoupon.kt
new file mode 100644
index 0000000000..5e319e0d89
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/FixedDiscountCoupon.kt
@@ -0,0 +1,47 @@
+package woowacourse.shopping.domain.model
+
+import woowacourse.shopping.domain.model.Price.Companion.DEFAULT_SHIPPING_PRICE
+import woowacourse.shopping.domain.model.Price.Companion.MINIMUM_PRICE
+import java.time.LocalDateTime
+
+class FixedDiscountCoupon(
+ override val detail: CouponDetail,
+ override val isSelected: Boolean = false,
+) : Coupon {
+ override fun apply(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Price {
+ val original = products.selectedProductsPrice
+
+ val discount =
+ if (getIsAvailable(products, nowDateTime)) {
+ detail.discount ?: MINIMUM_PRICE
+ } else {
+ MINIMUM_PRICE
+ }
+
+ val shipping = DEFAULT_SHIPPING_PRICE
+
+ return Price(
+ original = original,
+ discount = discount,
+ shipping = shipping,
+ )
+ }
+
+ override fun getIsAvailable(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Boolean {
+ val isExceedingMinimumPurchase = products.selectedProductsPrice >= (detail.minimumPurchase ?: MINIMUM_PRICE)
+ val isDateOkay = detail.expirationDate >= nowDateTime.toLocalDate()
+
+ return isExceedingMinimumPurchase && isDateOkay
+ }
+
+ override fun copy(
+ detail: CouponDetail,
+ isSelected: Boolean,
+ ): Coupon = FixedDiscountCoupon(detail, isSelected)
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/FreeShippingCoupon.kt b/app/src/main/java/woowacourse/shopping/domain/model/FreeShippingCoupon.kt
new file mode 100644
index 0000000000..2772a7271f
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/FreeShippingCoupon.kt
@@ -0,0 +1,44 @@
+package woowacourse.shopping.domain.model
+
+import woowacourse.shopping.domain.model.Price.Companion.DEFAULT_SHIPPING_PRICE
+import woowacourse.shopping.domain.model.Price.Companion.MINIMUM_PRICE
+import java.time.LocalDateTime
+
+class FreeShippingCoupon(
+ override val detail: CouponDetail,
+ override val isSelected: Boolean = false,
+) : Coupon {
+ override fun apply(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Price {
+ val original = products.selectedProductsPrice
+
+ val isAvailable = getIsAvailable(products, nowDateTime)
+
+ val discount = if (isAvailable) DEFAULT_SHIPPING_PRICE else MINIMUM_PRICE
+
+ val shipping = DEFAULT_SHIPPING_PRICE
+
+ return Price(
+ original = original,
+ discount = discount,
+ shipping = shipping,
+ )
+ }
+
+ override fun getIsAvailable(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Boolean {
+ val isExceedingMinimumPurchase = products.selectedProductsPrice >= (detail.minimumPurchase ?: MINIMUM_PRICE)
+ val isDateOkay = detail.expirationDate >= nowDateTime.toLocalDate()
+
+ return isExceedingMinimumPurchase && isDateOkay
+ }
+
+ override fun copy(
+ detail: CouponDetail,
+ isSelected: Boolean,
+ ): Coupon = FreeShippingCoupon(detail, isSelected)
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/Page.kt b/app/src/main/java/woowacourse/shopping/domain/model/Page.kt
index 5ed7c57353..cac0cb3f06 100644
--- a/app/src/main/java/woowacourse/shopping/domain/model/Page.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/model/Page.kt
@@ -5,7 +5,7 @@ data class Page(
val isFirst: Boolean,
val isLast: Boolean,
) {
- val isSingle: Boolean get() = isFirst == isLast
+ val isSingle: Boolean get() = isFirst && isLast
operator fun plus(step: Int): Page = copy(current = current + step)
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/PercentageDiscountCoupon.kt b/app/src/main/java/woowacourse/shopping/domain/model/PercentageDiscountCoupon.kt
new file mode 100644
index 0000000000..001fcb3645
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/PercentageDiscountCoupon.kt
@@ -0,0 +1,53 @@
+package woowacourse.shopping.domain.model
+
+import woowacourse.shopping.domain.model.Price.Companion.DEFAULT_SHIPPING_PRICE
+import woowacourse.shopping.domain.model.Price.Companion.MINIMUM_PRICE
+import java.time.LocalDateTime
+
+class PercentageDiscountCoupon(
+ override val detail: CouponDetail,
+ override val isSelected: Boolean = false,
+) : Coupon {
+ override fun apply(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Price {
+ val original = products.selectedProductsPrice
+
+ val isAvailable = getIsAvailable(products, nowDateTime)
+
+ val discountRate = (detail.discount ?: MINIMUM_PRICE).coerceIn(0, 100)
+ val discount = if (isAvailable) (original * discountRate / 100.0).toInt() else MINIMUM_PRICE
+
+ val shipping = DEFAULT_SHIPPING_PRICE
+
+ return Price(
+ original = original,
+ discount = discount,
+ shipping = shipping,
+ )
+ }
+
+ override fun getIsAvailable(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Boolean {
+ val minimumPurchase = detail.minimumPurchase ?: MINIMUM_PRICE
+ val now = nowDateTime.toLocalTime()
+
+ val isAmountOkay = products.selectedProductsPrice >= minimumPurchase
+ val isTimeOkay =
+ detail.availableTime?.let {
+ now.isAfter(it.start) && now.isBefore(it.end)
+ } ?: true
+
+ val isDateOkay = detail.expirationDate >= nowDateTime.toLocalDate()
+
+ return isAmountOkay && isTimeOkay && isDateOkay
+ }
+
+ override fun copy(
+ detail: CouponDetail,
+ isSelected: Boolean,
+ ): Coupon = PercentageDiscountCoupon(detail, isSelected)
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/Price.kt b/app/src/main/java/woowacourse/shopping/domain/model/Price.kt
new file mode 100644
index 0000000000..7248a0aa66
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/Price.kt
@@ -0,0 +1,15 @@
+package woowacourse.shopping.domain.model
+
+data class Price(
+ val original: Int,
+ val discount: Int = MINIMUM_PRICE,
+ val shipping: Int = DEFAULT_SHIPPING_PRICE,
+) {
+ val result: Int get() = (original - discount + shipping).coerceAtLeast(MINIMUM_PRICE)
+
+ companion object {
+ const val MINIMUM_PRICE: Int = 0
+ const val DEFAULT_SHIPPING_PRICE: Int = 3_000
+ val EMPTY_PRICE: Price = Price(MINIMUM_PRICE)
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/Product.kt b/app/src/main/java/woowacourse/shopping/domain/model/Product.kt
index 58a4a25f0a..4a6f75f223 100644
--- a/app/src/main/java/woowacourse/shopping/domain/model/Product.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/model/Product.kt
@@ -13,14 +13,12 @@ data class Product(
fun increaseQuantity(delta: Int = DEFAULT_QUANTITY_DELTA): Product = copy(quantity = quantity + delta)
fun decreaseQuantity(delta: Int = DEFAULT_QUANTITY_DELTA): Product =
- if (quantity - delta >= 0) {
+ if (quantity - delta >= MINIMUM_QUANTITY) {
copy(quantity = quantity - delta)
} else {
- copy(quantity = 0)
+ copy(quantity = MINIMUM_QUANTITY)
}
- fun toggleSelection(): Product = copy(isSelected = !isSelected)
-
companion object {
const val MINIMUM_QUANTITY = 0
val EMPTY_PRODUCT = Product(EMPTY_PRODUCT_DETAIL, null, MINIMUM_QUANTITY)
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/Products.kt b/app/src/main/java/woowacourse/shopping/domain/model/Products.kt
index 473e0ae947..56739ff426 100644
--- a/app/src/main/java/woowacourse/shopping/domain/model/Products.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/model/Products.kt
@@ -1,13 +1,14 @@
package woowacourse.shopping.domain.model
import woowacourse.shopping.domain.model.Page.Companion.EMPTY_PAGE
-import woowacourse.shopping.domain.model.Product.Companion.MINIMUM_QUANTITY
data class Products(
val products: List,
val page: Page = EMPTY_PAGE,
) {
val isAllSelected: Boolean get() = products.all { it.isSelected }
+ val selectedProductsQuantity: Int get() = products.filter { it.isSelected }.sumOf { it.quantity }
+ val selectedProductsPrice: Int = products.filter { it.isSelected }.sumOf { it.totalPrice }
operator fun plus(other: Products): Products {
val mergedProducts = products + other.products
@@ -17,20 +18,15 @@ data class Products(
)
}
- fun updateProductQuantity(
- productId: Long,
+ fun updateSelection(
+ product: Product,
+ isSelected: Boolean = !product.isSelected,
+ ): Products = updateProduct(product.copy(isSelected = isSelected))
+
+ fun updateQuantity(
+ product: Product,
quantity: Int,
- ): Products {
- val updatedProducts =
- products.map { product ->
- if (product.productDetail.id == productId) {
- product.copy(quantity = quantity)
- } else {
- product
- }
- }
- return copy(products = updatedProducts)
- }
+ ): Products = updateProduct(product.copy(quantity = quantity))
fun updateProduct(newProduct: Product): Products {
val updatedProducts =
@@ -54,32 +50,13 @@ data class Products(
fun getProductByProductId(productId: Long): Product? = products.find { it.productDetail.id == productId }
- fun toggleSelectionByCartId(cartId: Long): Products =
- copy(
- products =
- products.map { product ->
- if (product.cartId == cartId) {
- product.toggleSelection()
- } else {
- product
- }
- },
- )
-
- fun getSelectedCartProductsPrice(): Int = products.filter { it.isSelected }.sumOf { it.productDetail.price * it.quantity }
-
- fun getSelectedCartRecommendProductsPrice(): Int =
- products.filter { it.quantity > MINIMUM_QUANTITY }.sumOf { it.productDetail.price * it.quantity }
-
- fun updateAllSelection(): Products = copy(products = products.map { it.copy(isSelected = !isAllSelected) })
-
- fun getSelectedCartProductIds(): List = products.filter { it.isSelected }.map { it.productDetail.id }
+ fun getProductByCartId(cartId: Long): Product? = products.find { it.cartId == cartId }
- fun getSelectedCartProductQuantity(): Int = products.filter { it.isSelected }.sumOf { it.quantity }
+ fun toggleAllSelection(): Products = copy(products = products.map { it.copy(isSelected = !isAllSelected) })
- fun getSelectedCartRecommendProductIds(): List = products.filter { it.quantity > MINIMUM_QUANTITY }.map { it.productDetail.id }
+ fun getSelectedProductIds(): List = products.filter { it.isSelected }.map { it.productDetail.id }
- fun getSelectedCartRecommendProductQuantity(): Int = products.filter { it.quantity > MINIMUM_QUANTITY }.sumOf { it.quantity }
+ fun getSelectedCartIds(): Set = products.filter { it.isSelected }.mapNotNull { it.cartId }.toSet()
companion object {
val EMPTY_PRODUCTS = Products(emptyList(), EMPTY_PAGE)
diff --git a/app/src/main/java/woowacourse/shopping/domain/model/QuantityBonusCoupon.kt b/app/src/main/java/woowacourse/shopping/domain/model/QuantityBonusCoupon.kt
new file mode 100644
index 0000000000..6a0f9f5ab0
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/model/QuantityBonusCoupon.kt
@@ -0,0 +1,63 @@
+package woowacourse.shopping.domain.model
+
+import woowacourse.shopping.domain.model.Price.Companion.DEFAULT_SHIPPING_PRICE
+import woowacourse.shopping.domain.model.Product.Companion.MINIMUM_QUANTITY
+import java.time.LocalDateTime
+
+class QuantityBonusCoupon(
+ override val detail: CouponDetail,
+ override val isSelected: Boolean = false,
+) : Coupon {
+ override fun apply(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Price {
+ val original = products.selectedProductsPrice
+ if (!getIsAvailable(products, nowDateTime)) return Price(original = original)
+
+ val targetProduct =
+ products.products
+ .filter { it.isSelected && it.quantity >= getRequiredTotalQuantity() }
+ .maxByOrNull { it.productDetail.price }
+ ?: return Price(original = original)
+
+ val productPrice = targetProduct.productDetail.price
+
+ val discount =
+ with(detail) {
+ if (buyQuantity == null || getQuantity == null) return Price(original = original)
+ val quantityForDiscount = targetProduct.quantity / (buyQuantity + getQuantity) * getQuantity
+ productPrice * quantityForDiscount
+ }
+
+ return Price(
+ original = original,
+ discount = discount,
+ shipping = DEFAULT_SHIPPING_PRICE,
+ )
+ }
+
+ override fun getIsAvailable(
+ products: Products,
+ nowDateTime: LocalDateTime,
+ ): Boolean {
+ val isValidQuantity =
+ products.products.any {
+ it.isSelected && it.quantity >= getRequiredTotalQuantity()
+ }
+ val isDateOkay = detail.expirationDate >= nowDateTime.toLocalDate()
+
+ return isValidQuantity && isDateOkay
+ }
+
+ private fun getRequiredTotalQuantity(): Int {
+ val buy = detail.buyQuantity ?: MINIMUM_QUANTITY
+ val get = detail.getQuantity ?: MINIMUM_QUANTITY
+ return buy + get
+ }
+
+ override fun copy(
+ detail: CouponDetail,
+ isSelected: Boolean,
+ ): Coupon = QuantityBonusCoupon(detail, isSelected)
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/repository/CartRepository.kt b/app/src/main/java/woowacourse/shopping/domain/repository/CartRepository.kt
index 3207703719..3718ea89ae 100644
--- a/app/src/main/java/woowacourse/shopping/domain/repository/CartRepository.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/repository/CartRepository.kt
@@ -3,30 +3,24 @@ package woowacourse.shopping.domain.repository
import woowacourse.shopping.domain.model.Products
interface CartRepository {
- fun fetchCartProducts(
+ suspend fun fetchCartProducts(
page: Int,
size: Int,
- callback: (Result) -> Unit,
- )
+ ): Products
- fun fetchAllCartProducts(callback: (Result) -> Unit)
+ suspend fun fetchAllCartProducts(): Products
- fun fetchCartItemCount(callback: (Result) -> Unit)
+ suspend fun fetchCartItemCount(): Int
- fun addCartProduct(
+ suspend fun addCartProduct(
productId: Long,
quantity: Int,
- callback: (Result) -> Unit,
- )
+ ): Unit
- fun deleteCartProduct(
- cartId: Long,
- callback: (Result) -> Unit,
- )
+ suspend fun deleteCartProduct(cartId: Long): Unit
- fun updateCartProduct(
+ suspend fun updateCartProduct(
cartId: Long,
quantity: Int,
- callback: (Result) -> Unit,
- )
+ ): Unit
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/repository/CouponRepository.kt b/app/src/main/java/woowacourse/shopping/domain/repository/CouponRepository.kt
new file mode 100644
index 0000000000..7896a36f11
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/repository/CouponRepository.kt
@@ -0,0 +1,7 @@
+package woowacourse.shopping.domain.repository
+
+import woowacourse.shopping.domain.model.Coupon
+
+interface CouponRepository {
+ suspend fun fetchAllCoupons(): List
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/repository/HistoryRepository.kt b/app/src/main/java/woowacourse/shopping/domain/repository/HistoryRepository.kt
index 7a85dbbc68..ae93059058 100644
--- a/app/src/main/java/woowacourse/shopping/domain/repository/HistoryRepository.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/repository/HistoryRepository.kt
@@ -4,12 +4,12 @@ import woowacourse.shopping.domain.model.HistoryProduct
import woowacourse.shopping.domain.model.ProductDetail
interface HistoryRepository {
- fun fetchAllHistory(callback: (List) -> Unit)
+ suspend fun fetchAllHistory(): List
- fun fetchRecentHistory(callback: (HistoryProduct?) -> Unit)
+ suspend fun fetchRecentHistory(): HistoryProduct?
- fun addHistoryWithLimit(
+ suspend fun addHistoryWithLimit(
productDetail: ProductDetail,
limit: Int,
- )
+ ): Unit
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/repository/OrderRepository.kt b/app/src/main/java/woowacourse/shopping/domain/repository/OrderRepository.kt
index 919e8dfd6d..75d9ff5631 100644
--- a/app/src/main/java/woowacourse/shopping/domain/repository/OrderRepository.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/repository/OrderRepository.kt
@@ -1,8 +1,5 @@
package woowacourse.shopping.domain.repository
interface OrderRepository {
- fun postOrderProducts(
- cartIds: List,
- callback: (Result) -> Unit,
- )
+ suspend fun postOrderProducts(cartIds: List): Unit
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/repository/ProductRepository.kt b/app/src/main/java/woowacourse/shopping/domain/repository/ProductRepository.kt
index dda9aacb8d..67a367221f 100644
--- a/app/src/main/java/woowacourse/shopping/domain/repository/ProductRepository.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/repository/ProductRepository.kt
@@ -1,15 +1,17 @@
package woowacourse.shopping.domain.repository
import woowacourse.shopping.domain.model.Product
+import woowacourse.shopping.domain.model.ProductDetail
import woowacourse.shopping.domain.model.Products
interface ProductRepository {
- fun fetchProducts(
+ suspend fun fetchProducts(
page: Int,
size: Int,
category: String? = null,
- callback: (Result) -> Unit,
- )
+ ): Products
- fun fetchAllProducts(callback: (Result>) -> Unit)
+ suspend fun fetchAllProducts(): List
+
+ suspend fun fetchProduct(productId: Long): ProductDetail
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/AddSearchHistoryUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/AddSearchHistoryUseCase.kt
index f9c835e4e0..88ccc2d22f 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/AddSearchHistoryUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/AddSearchHistoryUseCase.kt
@@ -3,16 +3,16 @@ package woowacourse.shopping.domain.usecase
import woowacourse.shopping.domain.model.ProductDetail
import woowacourse.shopping.domain.model.ProductDetail.Companion.EMPTY_PRODUCT_DETAIL
import woowacourse.shopping.domain.repository.HistoryRepository
-import kotlin.concurrent.thread
class AddSearchHistoryUseCase(
private val repository: HistoryRepository,
) {
- operator fun invoke(productDetail: ProductDetail) {
- thread {
- if (productDetail == EMPTY_PRODUCT_DETAIL) return@thread
- repository.addHistoryWithLimit(productDetail, MAX_HISTORY_COUNT)
+ suspend operator fun invoke(productDetail: ProductDetail) {
+ if (productDetail == EMPTY_PRODUCT_DETAIL) {
+ throw IllegalArgumentException("[AddSearchHistoryUseCase] 유효하지 않은 상품입니다.")
}
+
+ repository.addHistoryWithLimit(productDetail, MAX_HISTORY_COUNT)
}
companion object {
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/DecreaseCartProductQuantityUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/DecreaseCartProductQuantityUseCase.kt
index ac8ac0381b..f3b9b88184 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/DecreaseCartProductQuantityUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/DecreaseCartProductQuantityUseCase.kt
@@ -7,47 +7,20 @@ import woowacourse.shopping.domain.repository.CartRepository
class DecreaseCartProductQuantityUseCase(
private val repository: CartRepository,
) {
- operator fun invoke(
+ suspend operator fun invoke(
product: Product,
step: Int = DEFAULT_QUANTITY_STEP,
- callback: (quantity: Result) -> Unit = {},
- ) {
- if (product.cartId == null) return
+ ): Int {
+ val cartId = product.cartId ?: throw IllegalArgumentException("[DecreaseCartProductQuantityUseCase] 유효하지 않은 상품")
+
val newQuantity = (product.quantity - step).coerceAtLeast(MINIMUM_QUANTITY)
- if (newQuantity <= MINIMUM_QUANTITY) {
- deleteCartProduct(product.cartId, callback)
+ return if (newQuantity <= MINIMUM_QUANTITY) {
+ repository.deleteCartProduct(cartId)
+ MINIMUM_QUANTITY
} else {
- updateCartProduct(product.cartId, newQuantity, callback)
- }
- }
-
- private fun deleteCartProduct(
- cartId: Long,
- callback: (Result) -> Unit,
- ) {
- repository.deleteCartProduct(cartId) { result ->
- result
- .onSuccess {
- callback(Result.success(MINIMUM_QUANTITY))
- }.onFailure {
- callback(Result.failure(it))
- }
- }
- }
-
- private fun updateCartProduct(
- cartId: Long,
- newQuantity: Int,
- callback: (Result) -> Unit,
- ) {
- repository.updateCartProduct(cartId, newQuantity) { result ->
- result
- .onSuccess {
- callback(Result.success(newQuantity))
- }.onFailure {
- callback(Result.failure(it))
- }
+ repository.updateCartProduct(cartId, newQuantity)
+ newQuantity
}
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartProductsQuantityUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartProductsQuantityUseCase.kt
index e00ad52fe1..c7df8c44e8 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartProductsQuantityUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartProductsQuantityUseCase.kt
@@ -5,9 +5,5 @@ import woowacourse.shopping.domain.repository.CartRepository
class GetCartProductsQuantityUseCase(
private val repository: CartRepository,
) {
- operator fun invoke(callback: (quantity: Result) -> Unit) {
- repository.fetchCartItemCount { result ->
- callback(result)
- }
- }
+ suspend operator fun invoke(): Int = repository.fetchCartItemCount()
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartProductsUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartProductsUseCase.kt
index f7ebd06c0f..4075e8f72d 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartProductsUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartProductsUseCase.kt
@@ -6,13 +6,8 @@ import woowacourse.shopping.domain.repository.CartRepository
class GetCartProductsUseCase(
private val repository: CartRepository,
) {
- operator fun invoke(
+ suspend operator fun invoke(
page: Int,
size: Int,
- callback: (products: Result) -> Unit,
- ) {
- repository.fetchCartProducts(page, size) { result ->
- callback(result)
- }
- }
+ ): Products = repository.fetchCartProducts(page, size)
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartRecommendProductsUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartRecommendProductsUseCase.kt
index 831a05b7b9..60db817a5f 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartRecommendProductsUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCartRecommendProductsUseCase.kt
@@ -1,8 +1,6 @@
package woowacourse.shopping.domain.usecase
-import woowacourse.shopping.domain.model.HistoryProduct
import woowacourse.shopping.domain.model.Products
-import woowacourse.shopping.domain.model.Products.Companion.EMPTY_PRODUCTS
import woowacourse.shopping.domain.repository.CartRepository
import woowacourse.shopping.domain.repository.HistoryRepository
import woowacourse.shopping.domain.repository.ProductRepository
@@ -12,48 +10,34 @@ class GetCartRecommendProductsUseCase(
private val cartRepository: CartRepository,
private val historyRepository: HistoryRepository,
) {
- operator fun invoke(callback: (products: Result) -> Unit) {
- historyRepository.fetchRecentHistory { historyProduct ->
- if (historyProduct == null) {
- callback(Result.success(EMPTY_PRODUCTS))
- } else {
- filterProductsByCategory(historyProduct, callback)
- }
- }
+ suspend operator fun invoke(): Products {
+ val recent = historyRepository.fetchRecentHistory()
+ return filterProductsByCategory(recent?.category)
}
- private fun filterProductsByCategory(
- historyProduct: HistoryProduct,
- callback: (products: Result) -> Unit,
- ) {
- productRepository.fetchProducts(0, Int.MAX_VALUE, historyProduct.category) { result ->
- result
- .onSuccess { catalogProducts ->
- combineCartProducts(catalogProducts, callback)
- }.onFailure {
- callback(Result.failure(it))
- }
- }
+ private suspend fun filterProductsByCategory(category: String?): Products {
+ val products =
+ productRepository.fetchProducts(
+ page = 0,
+ size = Int.MAX_VALUE,
+ category = category,
+ )
+ return combineCartProducts(products)
}
- private fun combineCartProducts(
- catalogProducts: Products,
- callback: (products: Result) -> Unit,
- ) {
- cartRepository.fetchAllCartProducts { result ->
- result
- .onSuccess { cartProducts ->
- val cartProductIds = cartProducts.products.map { product -> product.productDetail.id }
- val filteredProducts =
- catalogProducts.products
- .filterNot { product ->
- product.productDetail.id in cartProductIds
- }.take(10)
+ private suspend fun combineCartProducts(catalogProducts: Products): Products {
+ val cartProducts = cartRepository.fetchAllCartProducts()
+ val cartProductIds = cartProducts.products.map { it.productDetail.id }
- callback(Result.success(Products(filteredProducts)))
- }.onFailure {
- callback(Result.failure(it))
- }
- }
+ val filteredProducts =
+ catalogProducts.products
+ .filterNot { it.productDetail.id in cartProductIds }
+ .take(RECOMMEND_PRODUCT_COUNT)
+
+ return Products(filteredProducts)
+ }
+
+ companion object {
+ private const val RECOMMEND_PRODUCT_COUNT = 10
}
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductUseCase.kt
index b7de810776..7e80b1f183 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductUseCase.kt
@@ -8,51 +8,31 @@ class GetCatalogProductUseCase(
private val productRepository: ProductRepository,
private val cartRepository: CartRepository,
) {
- operator fun invoke(
- productId: Long,
- callback: (product: Result) -> Unit,
- ) {
- productRepository.fetchAllProducts { result ->
- result
- .onSuccess { catalogProducts ->
- catalogProducts.find { product -> product.productDetail.id == productId }?.let { catalogProduct ->
- combineCartProduct(productId, catalogProduct, callback)
- } ?: return@fetchAllProducts callback(Result.success(null))
- }.onFailure {
- callback(Result.failure(it))
- }
- }
+ suspend operator fun invoke(productId: Long): Product {
+ val productDetail = productRepository.fetchProduct(productId)
+ val product = Product(productDetail)
+ return combineCartProduct(productId, product)
}
- private fun combineCartProduct(
+ private suspend fun combineCartProduct(
productId: Long,
catalogProduct: Product,
- callback: (product: Result) -> Unit,
- ) {
- cartRepository.fetchAllCartProducts { result ->
- result
- .onSuccess { cartProducts ->
- val cartProductsByProductId: Map =
- cartProducts.products.associateBy { product ->
- product.productDetail.id
- }
- val updatedProduct = getUpdatedProduct(cartProductsByProductId, productId, catalogProduct)
+ ): Product {
+ val cartProducts = cartRepository.fetchAllCartProducts()
+ val cartProductsByProductId = cartProducts.products.associateBy { it.productDetail.id }
- callback(Result.success(updatedProduct))
- }.onFailure {
- callback(Result.failure(it))
- }
- }
+ return updateProduct(cartProductsByProductId, productId, catalogProduct)
}
- private fun getUpdatedProduct(
+ private fun updateProduct(
cartProducts: Map,
productId: Long,
catalogProduct: Product,
- ) = cartProducts[productId]?.let { cartProduct ->
- catalogProduct.copy(
- cartId = cartProduct.cartId,
- quantity = cartProduct.quantity,
- )
- } ?: catalogProduct
+ ): Product =
+ cartProducts[productId]?.let { cartProduct ->
+ catalogProduct.copy(
+ cartId = cartProduct.cartId,
+ quantity = cartProduct.quantity,
+ )
+ } ?: catalogProduct
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsByIdsUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsByIdsUseCase.kt
deleted file mode 100644
index 785fed8118..0000000000
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsByIdsUseCase.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package woowacourse.shopping.domain.usecase
-
-import woowacourse.shopping.domain.model.Product
-import woowacourse.shopping.domain.repository.CartRepository
-import woowacourse.shopping.domain.repository.ProductRepository
-
-class GetCatalogProductsByIdsUseCase(
- private val productRepository: ProductRepository,
- private val cartRepository: CartRepository,
-) {
- operator fun invoke(
- productIds: List,
- callback: (products: Result>) -> Unit,
- ) {
- productRepository.fetchAllProducts { result ->
- result
- .onSuccess { catalogProducts ->
- val filteredCatalogProducts: List = catalogProducts.filter { it.productDetail.id in productIds }
- combineCartProducts(filteredCatalogProducts, callback)
- }.onFailure {
- callback(Result.failure(it))
- }
- }
- }
-
- private fun combineCartProducts(
- filteredCatalogProducts: List,
- callback: (products: Result>) -> Unit,
- ) {
- cartRepository.fetchAllCartProducts { result ->
- result
- .onSuccess { cartProducts ->
- val cartProductsByProductId =
- cartProducts.products.associateBy { product ->
- product.productDetail.id
- }
- val updatedCartProducts = getUpdatedCartProducts(filteredCatalogProducts, cartProductsByProductId)
-
- callback(Result.success(updatedCartProducts))
- }.onFailure {
- callback(Result.failure(it))
- return@fetchAllCartProducts
- }
- }
- }
-
- private fun getUpdatedCartProducts(
- filteredCatalogProducts: List,
- cartProducts: Map,
- ) = filteredCatalogProducts.map { catalogProduct ->
- val cartProduct = cartProducts[catalogProduct.productDetail.id]
- if (cartProduct != null) {
- catalogProduct.copy(
- cartId = cartProduct.cartId,
- quantity = cartProduct.quantity,
- )
- } else {
- catalogProduct
- }
- }
-}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsByProductIdsUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsByProductIdsUseCase.kt
new file mode 100644
index 0000000000..7c93d10892
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsByProductIdsUseCase.kt
@@ -0,0 +1,44 @@
+package woowacourse.shopping.domain.usecase
+
+import woowacourse.shopping.domain.model.Product
+import woowacourse.shopping.domain.model.Products
+import woowacourse.shopping.domain.repository.CartRepository
+import woowacourse.shopping.domain.repository.ProductRepository
+
+class GetCatalogProductsByProductIdsUseCase(
+ private val productRepository: ProductRepository,
+ private val cartRepository: CartRepository,
+) {
+ suspend operator fun invoke(productIds: List): List {
+ val products =
+ productIds.map { id ->
+ val productDetail = productRepository.fetchProduct(id)
+ Product(productDetail)
+ }
+
+ return combineCartProducts(products)
+ }
+
+ private suspend fun combineCartProducts(catalogProducts: List): List {
+ val cartProducts: Products = cartRepository.fetchAllCartProducts()
+ val cartProductsByProductId = cartProducts.products.associateBy { it.productDetail.id }
+
+ return updateProducts(catalogProducts, cartProductsByProductId)
+ }
+
+ private fun updateProducts(
+ catalogProducts: List,
+ cartProducts: Map,
+ ): List =
+ catalogProducts.map { catalogProduct ->
+ val cartProduct = cartProducts[catalogProduct.productDetail.id]
+ if (cartProduct != null) {
+ catalogProduct.copy(
+ cartId = cartProduct.cartId,
+ quantity = cartProduct.quantity,
+ )
+ } else {
+ catalogProduct
+ }
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsUseCase.kt
index eccb8dd530..4c0310c5af 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCatalogProductsUseCase.kt
@@ -9,53 +9,33 @@ class GetCatalogProductsUseCase(
private val productRepository: ProductRepository,
private val cartRepository: CartRepository,
) {
- operator fun invoke(
+ suspend operator fun invoke(
page: Int,
size: Int,
- callback: (products: Result) -> Unit,
- ) {
- productRepository.fetchProducts(page, size) { result ->
- result
- .onSuccess { catalogProducts ->
- combineCartProducts(catalogProducts, callback)
- }.onFailure {
- callback(Result.failure(it))
- }
- }
+ ): Products {
+ val catalogProducts = productRepository.fetchProducts(page, size)
+ return combineCartProducts(catalogProducts)
}
- private fun combineCartProducts(
- catalogProducts: Products,
- callback: (products: Result) -> Unit,
- ) {
- cartRepository.fetchAllCartProducts { result ->
- result
- .onSuccess { cartProducts ->
- val cartProductsByProductId =
- cartProducts.products.associateBy { product ->
- product.productDetail.id
- }
- val updatedProducts = getUpdatedProducts(catalogProducts, cartProductsByProductId)
+ private suspend fun combineCartProducts(catalogProducts: Products): Products {
+ val cartProducts = cartRepository.fetchAllCartProducts()
+ val cartProductsByProductId = cartProducts.products.associateBy { it.productDetail.id }
- callback(Result.success(catalogProducts.copy(products = updatedProducts)))
- }.onFailure {
- callback(Result.failure(it))
- }
- }
+ val updatedProducts = updateProducts(catalogProducts, cartProductsByProductId)
+
+ return catalogProducts.copy(products = updatedProducts)
}
- private fun getUpdatedProducts(
+ private fun updateProducts(
catalogProducts: Products,
cartProducts: Map,
- ) = catalogProducts.products.map { catalogProduct ->
- val cartProduct = cartProducts[catalogProduct.productDetail.id]
- if (cartProduct != null) {
- catalogProduct.copy(
- cartId = cartProduct.cartId,
- quantity = cartProduct.quantity,
- )
- } else {
- catalogProduct
+ ): List =
+ catalogProducts.products.map { catalogProduct ->
+ cartProducts[catalogProduct.productDetail.id]?.let { cartProduct ->
+ catalogProduct.copy(
+ cartId = cartProduct.cartId,
+ quantity = cartProduct.quantity,
+ )
+ } ?: catalogProduct
}
- }
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetCouponsUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCouponsUseCase.kt
new file mode 100644
index 0000000000..237db002fd
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetCouponsUseCase.kt
@@ -0,0 +1,13 @@
+package woowacourse.shopping.domain.usecase
+
+import woowacourse.shopping.domain.model.Coupons
+import woowacourse.shopping.domain.repository.CouponRepository
+
+class GetCouponsUseCase(
+ private val repository: CouponRepository,
+) {
+ suspend operator fun invoke(): Coupons {
+ val coupons = repository.fetchAllCoupons()
+ return Coupons(coupons)
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetRecentSearchHistoryUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetRecentSearchHistoryUseCase.kt
index 39d4d2870d..4515c4f1b3 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/GetRecentSearchHistoryUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetRecentSearchHistoryUseCase.kt
@@ -6,9 +6,5 @@ import woowacourse.shopping.domain.repository.HistoryRepository
class GetRecentSearchHistoryUseCase(
private val repository: HistoryRepository,
) {
- operator fun invoke(callback: (product: HistoryProduct?) -> Unit) {
- repository.fetchRecentHistory { historyProduct ->
- callback(historyProduct)
- }
- }
+ suspend operator fun invoke(): HistoryProduct? = repository.fetchRecentHistory()
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/GetSearchHistoryUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/GetSearchHistoryUseCase.kt
index 28ab209aaa..f11a5be95f 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/GetSearchHistoryUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/GetSearchHistoryUseCase.kt
@@ -6,9 +6,5 @@ import woowacourse.shopping.domain.repository.HistoryRepository
class GetSearchHistoryUseCase(
private val repository: HistoryRepository,
) {
- operator fun invoke(callback: (products: List) -> Unit) {
- repository.fetchAllHistory { historyProducts ->
- callback(historyProducts)
- }
- }
+ suspend operator fun invoke(): List = repository.fetchAllHistory()
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/IncreaseCartProductQuantityUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/IncreaseCartProductQuantityUseCase.kt
index 54d562c572..a1bb4abff2 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/IncreaseCartProductQuantityUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/IncreaseCartProductQuantityUseCase.kt
@@ -6,48 +6,33 @@ import woowacourse.shopping.domain.repository.CartRepository
class IncreaseCartProductQuantityUseCase(
private val repository: CartRepository,
) {
- operator fun invoke(
+ suspend operator fun invoke(
product: Product,
step: Int = DEFAULT_QUANTITY_STEP,
- callback: (quantity: Result) -> Unit = {},
- ) {
+ ): Int {
val newQuantity = product.quantity + step
if (product.cartId == null) {
- addCartProduct(product, newQuantity, callback)
+ addCartProduct(product, newQuantity)
} else {
- updateCartProduct(product.cartId, newQuantity, callback)
+ updateCartProduct(product.cartId, newQuantity)
}
+
+ return newQuantity
}
- private fun addCartProduct(
+ private suspend fun addCartProduct(
product: Product,
newQuantity: Int,
- callback: (quantity: Result) -> Unit,
) {
- repository.addCartProduct(product.productDetail.id, newQuantity) { result ->
- result
- .onSuccess {
- callback(Result.success(newQuantity))
- }.onFailure {
- callback(Result.failure(it))
- }
- }
+ repository.addCartProduct(product.productDetail.id, newQuantity)
}
- private fun updateCartProduct(
+ private suspend fun updateCartProduct(
cartId: Long,
newQuantity: Int,
- callback: (quantity: Result) -> Unit,
) {
- repository.updateCartProduct(cartId, newQuantity) { result ->
- result
- .onSuccess {
- callback(Result.success(newQuantity))
- }.onFailure {
- callback(Result.failure(it))
- }
- }
+ repository.updateCartProduct(cartId, newQuantity)
}
companion object {
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/OrderProductsUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/OrderProductsUseCase.kt
index 4cbf6f64f9..7b54fb5f86 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/OrderProductsUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/OrderProductsUseCase.kt
@@ -1,78 +1,9 @@
package woowacourse.shopping.domain.usecase
-import woowacourse.shopping.domain.model.Product
-import woowacourse.shopping.domain.repository.CartRepository
import woowacourse.shopping.domain.repository.OrderRepository
-import woowacourse.shopping.domain.repository.ProductRepository
class OrderProductsUseCase(
- private val productRepository: ProductRepository,
- private val cartRepository: CartRepository,
private val orderRepository: OrderRepository,
) {
- operator fun invoke(
- productIds: Set,
- callback: (Result) -> Unit = {},
- ) {
- getCatalogProductsByIds(productIds) { result ->
- result
- .onSuccess { catalogProducts ->
- val cartIds = catalogProducts.mapNotNull { it.cartId }
- orderRepository.postOrderProducts(cartIds, callback)
- }.onFailure {
- callback(Result.failure(it))
- }
- }
- }
-
- private fun getCatalogProductsByIds(
- productIds: Set,
- callback: (products: Result>) -> Unit,
- ) {
- productRepository.fetchAllProducts { result ->
- result
- .onSuccess { catalogProducts ->
- val filteredCatalogProducts: List = catalogProducts.filter { it.productDetail.id in productIds }
- combineCartProducts(filteredCatalogProducts, callback)
- }.onFailure {
- callback(Result.failure(it))
- }
- }
- }
-
- private fun combineCartProducts(
- filteredCatalogProducts: List,
- callback: (products: Result>) -> Unit,
- ) {
- cartRepository.fetchAllCartProducts { result ->
- result
- .onSuccess { cartProducts ->
- val cartProductsByProductId =
- cartProducts.products.associateBy { product ->
- product.productDetail.id
- }
- val updatedCartProducts = getUpdatedCartProducts(filteredCatalogProducts, cartProductsByProductId)
-
- callback(Result.success(updatedCartProducts))
- }.onFailure {
- callback(Result.failure(it))
- return@fetchAllCartProducts
- }
- }
- }
-
- private fun getUpdatedCartProducts(
- filteredCatalogProducts: List,
- cartProducts: Map,
- ) = filteredCatalogProducts.map { catalogProduct ->
- val cartProduct = cartProducts[catalogProduct.productDetail.id]
- if (cartProduct != null) {
- catalogProduct.copy(
- cartId = cartProduct.cartId,
- quantity = cartProduct.quantity,
- )
- } else {
- catalogProduct
- }
- }
+ suspend operator fun invoke(cartIds: Set): Unit = orderRepository.postOrderProducts(cartIds.toList())
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/RemoveCartProductUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/RemoveCartProductUseCase.kt
index 73672059a8..3c40bba8f2 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/RemoveCartProductUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/RemoveCartProductUseCase.kt
@@ -5,12 +5,5 @@ import woowacourse.shopping.domain.repository.CartRepository
class RemoveCartProductUseCase(
private val repository: CartRepository,
) {
- operator fun invoke(
- cartId: Long,
- callback: (Result) -> Unit,
- ) {
- repository.deleteCartProduct(cartId) { result ->
- callback(result)
- }
- }
+ suspend operator fun invoke(cartId: Long): Unit = repository.deleteCartProduct(cartId)
}
diff --git a/app/src/main/java/woowacourse/shopping/domain/usecase/UpdateCartProductUseCase.kt b/app/src/main/java/woowacourse/shopping/domain/usecase/UpdateCartProductUseCase.kt
index 386c12aa19..113d2427ec 100644
--- a/app/src/main/java/woowacourse/shopping/domain/usecase/UpdateCartProductUseCase.kt
+++ b/app/src/main/java/woowacourse/shopping/domain/usecase/UpdateCartProductUseCase.kt
@@ -5,20 +5,14 @@ import woowacourse.shopping.domain.repository.CartRepository
class UpdateCartProductUseCase(
private val repository: CartRepository,
) {
- operator fun invoke(
+ suspend operator fun invoke(
productId: Long,
cartId: Long?,
quantity: Int,
- callback: (Result) -> Unit,
- ) {
+ ): Unit =
if (cartId == null) {
- repository.addCartProduct(productId, quantity) { result ->
- callback(result)
- }
+ repository.addCartProduct(productId, quantity)
} else {
- repository.updateCartProduct(cartId, quantity) { result ->
- callback(result)
- }
+ repository.updateCartProduct(cartId, quantity)
}
- }
}
diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt
index 7197dc16ad..7d4d0197e7 100644
--- a/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt
@@ -13,6 +13,8 @@ import woowacourse.shopping.databinding.ActivityCartBinding
import woowacourse.shopping.ui.cart.CartActivity.OnClickHandler
import woowacourse.shopping.ui.common.DataBindingActivity
import woowacourse.shopping.ui.model.ActivityResult
+import woowacourse.shopping.ui.model.CartProductUiModel
+import woowacourse.shopping.ui.payment.PaymentActivity
class CartActivity : DataBindingActivity(R.layout.activity_cart) {
private val viewModel: CartViewModel by viewModels { CartViewModel.Factory }
@@ -61,32 +63,37 @@ class CartActivity : DataBindingActivity(R.layout.activity_
binding.cartCheckAllButtonText.visibility = View.GONE
}
- is CartRecommendFragment -> viewModel.orderProducts()
+ is CartRecommendFragment -> {
+ val intent = PaymentActivity.newIntent(this, viewModel.getSelectedProductIds().toLongArray())
+ startActivity(intent)
+ finish()
+ }
}
}
}
private fun initObservers() {
- viewModel.editedProductIds.observe(this) { editedProductIds ->
- setResult(
- ActivityResult.CART_PRODUCT_EDITED.code,
- Intent().apply {
- putExtra(
- ActivityResult.CART_PRODUCT_EDITED.key,
- editedProductIds.toLongArray(),
- )
- },
- )
+ viewModel.uiModel.observe(this) { uiModel ->
+ handleCartProducts(uiModel)
+ handleErrorMessage(uiModel)
}
+ }
- viewModel.isOrdered.observe(this) {
- finish()
- }
+ private fun handleCartProducts(uiModel: CartProductUiModel) {
+ setResult(
+ ActivityResult.CART_PRODUCT_EDITED.code,
+ Intent().apply {
+ putExtra(
+ ActivityResult.CART_PRODUCT_EDITED.key,
+ uiModel.editedProductIds.toLongArray(),
+ )
+ },
+ )
+ }
- viewModel.isError.observe(this) { errorMessage ->
- errorMessage?.let {
- Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
- }
+ private fun handleErrorMessage(uiModel: CartProductUiModel) {
+ uiModel.connectionErrorMessage?.let {
+ Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
}
}
diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartProductFragment.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartProductFragment.kt
index fa90023f4b..ea06b73871 100644
--- a/app/src/main/java/woowacourse/shopping/ui/cart/CartProductFragment.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartProductFragment.kt
@@ -32,9 +32,8 @@ class CartProductFragment : DataBindingFragment(R.la
}
private fun initObservers() {
- viewModel.cartProducts.observe(requireActivity()) { products ->
- cartProductAdapter.submitItems(products.products)
- viewModel.updateOrderInfo()
+ viewModel.uiModel.observe(requireActivity()) { uiModel ->
+ cartProductAdapter.submitItems(uiModel.cartProducts.products)
}
}
diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartRecommendFragment.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartRecommendFragment.kt
index dc59fbb513..3579ff3db8 100644
--- a/app/src/main/java/woowacourse/shopping/ui/cart/CartRecommendFragment.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartRecommendFragment.kt
@@ -24,9 +24,8 @@ class CartRecommendFragment : DataBindingFragment(
) {
super.onViewCreated(view, savedInstanceState)
binding.cartItemsRecommendContainer.adapter = adapter
- viewModel.recommendedProducts.observe(requireActivity()) { recommendedProducts ->
- adapter.submitList(recommendedProducts.products)
- viewModel.updateOrderInfo()
+ viewModel.uiModel.observe(requireActivity()) { uiModel ->
+ adapter.submitList(uiModel.recommendedProducts.products)
}
}
diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt
index 8a275d30bf..c65c95430a 100644
--- a/app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt
@@ -5,26 +5,26 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
-import woowacourse.shopping.di.UseCaseModule.decreaseCartProductQuantityUseCase
-import woowacourse.shopping.di.UseCaseModule.getCartProductsUseCase
-import woowacourse.shopping.di.UseCaseModule.getCartRecommendProductsUseCase
-import woowacourse.shopping.di.UseCaseModule.increaseCartProductQuantityUseCase
-import woowacourse.shopping.di.UseCaseModule.orderProductsUseCase
-import woowacourse.shopping.di.UseCaseModule.removeCartProductUseCase
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.launch
+import woowacourse.shopping.di.UseCaseInjection.decreaseCartProductQuantityUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCartProductsUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCartRecommendProductsUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCatalogProductUseCase
+import woowacourse.shopping.di.UseCaseInjection.increaseCartProductQuantityUseCase
+import woowacourse.shopping.di.UseCaseInjection.removeCartProductUseCase
import woowacourse.shopping.domain.model.Page
import woowacourse.shopping.domain.model.Page.Companion.EMPTY_PAGE
import woowacourse.shopping.domain.model.Product.Companion.MINIMUM_QUANTITY
-import woowacourse.shopping.domain.model.Products
-import woowacourse.shopping.domain.model.Products.Companion.EMPTY_PRODUCTS
import woowacourse.shopping.domain.usecase.DecreaseCartProductQuantityUseCase
import woowacourse.shopping.domain.usecase.GetCartProductsUseCase
import woowacourse.shopping.domain.usecase.GetCartRecommendProductsUseCase
+import woowacourse.shopping.domain.usecase.GetCatalogProductUseCase
import woowacourse.shopping.domain.usecase.IncreaseCartProductQuantityUseCase
-import woowacourse.shopping.domain.usecase.OrderProductsUseCase
import woowacourse.shopping.domain.usecase.RemoveCartProductUseCase
-import woowacourse.shopping.util.MutableSingleLiveData
-import woowacourse.shopping.util.SingleLiveData
+import woowacourse.shopping.ui.model.CartProductUiModel
class CartViewModel(
private val getCartProductsUseCase: GetCartProductsUseCase,
@@ -32,46 +32,38 @@ class CartViewModel(
private val increaseCartProductQuantityUseCase: IncreaseCartProductQuantityUseCase,
private val decreaseCartProductQuantityUseCase: DecreaseCartProductQuantityUseCase,
private val getCartRecommendProductsUseCase: GetCartRecommendProductsUseCase,
- private val orderProductsUseCase: OrderProductsUseCase,
+ private val getCatalogProductUseCase: GetCatalogProductUseCase,
) : ViewModel() {
- private val _cartProducts: MutableLiveData = MutableLiveData(EMPTY_PRODUCTS)
- val cartProducts: LiveData get() = _cartProducts
-
- private val _recommendedProducts: MutableLiveData = MutableLiveData(EMPTY_PRODUCTS)
- val recommendedProducts: LiveData get() = _recommendedProducts
-
- private val _editedProductIds: MutableLiveData> = MutableLiveData(emptySet())
- val editedProductIds: LiveData> get() = _editedProductIds
-
- private val _totalOrderPrice: MutableLiveData = MutableLiveData(0)
- val totalOrderPrice: LiveData get() = _totalOrderPrice
-
- private val _isOrdered: MutableSingleLiveData = MutableSingleLiveData()
- val isOrdered: SingleLiveData get() = _isOrdered
-
- private val _isLoading: MutableLiveData = MutableLiveData(true)
- val isLoading: LiveData get() = _isLoading
-
- private val _isError: MutableLiveData = MutableLiveData()
- val isError: LiveData get() = _isError
+ private val _uiModel: MutableLiveData = MutableLiveData(CartProductUiModel())
+ val uiModel: LiveData get() = _uiModel
init {
loadCartProducts()
}
- private fun loadCartProducts(page: Page = cartProducts.value?.page ?: EMPTY_PAGE) {
- _isLoading.value = true
- getCartProductsUseCase(
- page = page.current,
- size = DEFAULT_PAGE_SIZE,
- ) { result ->
- result
- .onSuccess { cartProducts ->
- _cartProducts.postValue(cartProducts)
- _isLoading.postValue(false)
- }.onFailure {
- _isError.postValue(it.message)
- }
+ private fun loadCartProducts(page: Page = uiModel.value?.cartProducts?.page ?: EMPTY_PAGE) {
+ updateUiModel { current ->
+ current.copy(isProductsLoading = true)
+ }
+
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val products =
+ getCartProductsUseCase(
+ page = page.current,
+ size = DEFAULT_PAGE_SIZE,
+ )
+
+ updateUiModel { current ->
+ current.copy(
+ cartProducts = products,
+ isProductsLoading = false,
+ )
+ }
}
}
@@ -79,158 +71,185 @@ class CartViewModel(
cartId: Long,
productId: Long,
) {
- removeCartProductUseCase(cartId) { result ->
- result
- .onSuccess {
- _editedProductIds.postValue(editedProductIds.value?.plus(productId))
- loadCartProducts()
- }.onFailure {
- _isError.postValue(it.message)
- }
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ removeCartProductUseCase(cartId)
+ updateUiModel { current ->
+ current.copy(editedProductIds = current.editedProductIds.plus(productId))
+ }
+ loadCartProducts()
}
}
fun increasePage(step: Int = DEFAULT_PAGE_STEP) {
- val page = cartProducts.value?.page ?: EMPTY_PAGE
+ val page = uiModel.value?.cartProducts?.page ?: EMPTY_PAGE
loadCartProducts(page.copy(current = page.current + step))
}
fun decreasePage(step: Int = DEFAULT_PAGE_STEP) {
- val page = cartProducts.value?.page ?: EMPTY_PAGE
+ val page = uiModel.value?.cartProducts?.page ?: EMPTY_PAGE
loadCartProducts(page.copy(current = page.current - step))
}
fun increaseCartProductQuantity(productId: Long) {
- increaseCartProductQuantityUseCase(
- product = cartProducts.value?.getProductByProductId(productId) ?: return,
- ) { result ->
- result
- .onSuccess { newQuantity ->
- _cartProducts.postValue(
- cartProducts.value?.updateProductQuantity(
- productId,
- newQuantity,
- ),
- )
- _editedProductIds.value = editedProductIds.value?.plus(productId)
- }.onFailure {
- Log.e("CartViewModel", it.message.toString())
- }
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = uiModel.value?.cartProducts?.getProductByProductId(productId) ?: return@launch
+ val newQuantity = increaseCartProductQuantityUseCase(product)
+
+ updateUiModel { current ->
+ current.copy(
+ cartProducts = current.cartProducts.updateQuantity(product, newQuantity),
+ editedProductIds = current.editedProductIds.plus(productId),
+ isProductsLoading = false,
+ )
+ }
}
}
fun decreaseCartProductQuantity(productId: Long) {
- decreaseCartProductQuantityUseCase(
- product = cartProducts.value?.getProductByProductId(productId) ?: return,
- ) { result ->
- result
- .onSuccess { newQuantity ->
- if (newQuantity > MINIMUM_QUANTITY) {
- _cartProducts.postValue(
- cartProducts.value?.updateProductQuantity(
- productId,
- newQuantity,
- ),
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = uiModel.value?.cartProducts?.getProductByProductId(productId) ?: return@launch
+ val newQuantity = decreaseCartProductQuantityUseCase(product)
+
+ when (newQuantity > MINIMUM_QUANTITY) {
+ true -> {
+ updateUiModel { current ->
+ current.copy(
+ cartProducts = current.cartProducts.updateQuantity(product, newQuantity),
+ editedProductIds = current.editedProductIds.plus(productId),
)
- } else {
- loadCartProducts()
}
- _editedProductIds.value = editedProductIds.value?.plus(productId)
- }.onFailure {
- Log.e("CartViewModel", it.message.toString())
}
+
+ false -> loadCartProducts()
+ }
}
}
fun toggleCartProductSelection(cartId: Long) {
- _cartProducts.value = cartProducts.value?.toggleSelectionByCartId(cartId)
- }
+ val cartProduct = uiModel.value?.cartProducts?.getProductByCartId(cartId) ?: return
- fun toggleAllCartProductsSelection() {
- _cartProducts.value = cartProducts.value?.updateAllSelection()
+ updateUiModel { current ->
+ current.copy(cartProducts = current.cartProducts.updateSelection(cartProduct))
+ }
}
- fun updateOrderInfo() {
- _totalOrderPrice.value =
- (cartProducts.value?.getSelectedCartProductsPrice() ?: 0) +
- (recommendedProducts.value?.getSelectedCartRecommendProductsPrice() ?: 0)
+ fun toggleAllCartProductsSelection() {
+ updateUiModel { current ->
+ current.copy(
+ cartProducts = current.cartProducts.toggleAllSelection(),
+ )
+ }
}
fun loadRecommendedProducts() {
- getCartRecommendProductsUseCase { result ->
- result
- .onSuccess { products ->
- _recommendedProducts.postValue(products)
- }.onFailure {
- _isError.postValue(it.message)
- }
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val products = getCartRecommendProductsUseCase()
+
+ updateUiModel { current ->
+ current.copy(recommendedProducts = products)
+ }
}
}
fun increaseRecommendedProductQuantity(productId: Long) {
- increaseCartProductQuantityUseCase(
- product = recommendedProducts.value?.getProductByProductId(productId) ?: return,
- ) { result ->
- result
- .onSuccess { newQuantity ->
- _recommendedProducts.postValue(
- recommendedProducts.value?.updateProductQuantity(
- productId,
- newQuantity,
- ),
- )
- _editedProductIds.value = editedProductIds.value?.plus(productId)
- }.onFailure {
- Log.e("CartViewModel", it.message.toString())
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = uiModel.value?.recommendedProducts?.getProductByProductId(productId) ?: return@launch
+ val newQuantity = increaseCartProductQuantityUseCase(product)
+
+ when (product.quantity <= MINIMUM_QUANTITY) {
+ true -> updateRecommendedProduct(productId)
+ false -> {
+ updateUiModel { current ->
+ current.copy(
+ recommendedProducts = current.recommendedProducts.updateQuantity(product, newQuantity),
+ editedProductIds = current.editedProductIds.plus(productId),
+ )
+ }
}
+ }
+ }
+ }
+
+ private fun updateRecommendedProduct(productId: Long) {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = getCatalogProductUseCase(productId)
+
+ updateUiModel { current ->
+ current.copy(
+ recommendedProducts = current.recommendedProducts.updateProduct(product.copy(isSelected = true)),
+ )
+ }
}
}
fun decreaseRecommendedProductQuantity(productId: Long) {
- decreaseCartProductQuantityUseCase(
- product = recommendedProducts.value?.getProductByProductId(productId) ?: return,
- ) { result ->
- result
- .onSuccess { newQuantity ->
- if (newQuantity > MINIMUM_QUANTITY) {
- _recommendedProducts.postValue(
- recommendedProducts.value?.updateProductQuantity(
- productId,
- newQuantity,
- ),
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = uiModel.value?.recommendedProducts?.getProductByProductId(productId) ?: return@launch
+ val newQuantity = decreaseCartProductQuantityUseCase(product)
+
+ when (product.quantity <= MINIMUM_QUANTITY) {
+ true -> updateRecommendedProduct(productId)
+ false -> {
+ updateUiModel { current ->
+ current.copy(
+ recommendedProducts = current.recommendedProducts.updateQuantity(product, newQuantity),
+ editedProductIds = current.editedProductIds.plus(productId),
)
- } else {
- loadCartProducts()
}
- _editedProductIds.value = editedProductIds.value?.plus(productId)
- }.onFailure {
- Log.e("CartViewModel", it.message.toString())
}
+ }
}
}
- fun orderProducts() {
- val selectedCartProductsIds: List = cartProducts.value?.getSelectedCartProductIds() ?: emptyList()
- val selectedRecommendedProductsIds: List = recommendedProducts.value?.getSelectedCartRecommendProductIds() ?: emptyList()
- val selectedProductIds: Set = (selectedCartProductsIds + selectedRecommendedProductsIds).toSet()
+ fun getSelectedProductIds(): Set {
+ val uiModel = uiModel.value ?: return emptySet()
- if (selectedProductIds.isEmpty()) return
+ val selectedCartProductsIds: List = uiModel.cartProducts.getSelectedProductIds()
+ val selectedRecommendedProductsIds: List = uiModel.recommendedProducts.getSelectedProductIds()
- orderProductsUseCase.invoke(selectedProductIds) { result ->
- result
- .onSuccess {
- _editedProductIds.postValue(selectedProductIds)
- _isOrdered.postValue(Unit)
- }.onFailure {
- _isError.postValue(it.message)
- }
- }
+ return (selectedCartProductsIds + selectedRecommendedProductsIds).toSet()
+ }
+
+ private fun updateUiModel(update: (CartProductUiModel) -> CartProductUiModel) {
+ val current = _uiModel.value ?: return
+ _uiModel.value = update(current)
}
companion object {
const val DEFAULT_PAGE_STEP: Int = 1
const val PAGE_INDEX_OFFSET: Int = 1
- const val DEFAULT_PAGE_SIZE: Int = 5
+ private const val DEFAULT_PAGE_SIZE: Int = 5
+ private const val TAG: String = "CartViewModel"
val Factory: ViewModelProvider.Factory =
object : ViewModelProvider.Factory {
@@ -245,7 +264,7 @@ class CartViewModel(
increaseCartProductQuantityUseCase = increaseCartProductQuantityUseCase,
decreaseCartProductQuantityUseCase = decreaseCartProductQuantityUseCase,
getCartRecommendProductsUseCase = getCartRecommendProductsUseCase,
- orderProductsUseCase = orderProductsUseCase,
+ getCatalogProductUseCase = getCatalogProductUseCase,
) as T
}
}
diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/adapter/CartProductViewHolder.kt b/app/src/main/java/woowacourse/shopping/ui/cart/adapter/CartProductViewHolder.kt
index 2577a00a39..fc46fb1f56 100644
--- a/app/src/main/java/woowacourse/shopping/ui/cart/adapter/CartProductViewHolder.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/cart/adapter/CartProductViewHolder.kt
@@ -13,16 +13,16 @@ class CartProductViewHolder(
binding.onClickHandler = onClickHandler
}
- fun bind(product: Product) {
- binding.product = product
+ fun bind(item: Product) {
+ binding.product = item
binding.cartProductCount.setOnClickHandler(
object : CartCountView.OnClickHandler {
override fun onIncreaseClick() {
- onClickHandler.onIncreaseClick(product.productDetail.id)
+ onClickHandler.onIncreaseClick(item.productDetail.id)
}
override fun onDecreaseClick() {
- onClickHandler.onDecreaseClick(product.productDetail.id)
+ onClickHandler.onDecreaseClick(item.productDetail.id)
}
},
)
diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/adapter/CartRecommendViewHolder.kt b/app/src/main/java/woowacourse/shopping/ui/cart/adapter/CartRecommendViewHolder.kt
index 6b54874a42..61204386a2 100644
--- a/app/src/main/java/woowacourse/shopping/ui/cart/adapter/CartRecommendViewHolder.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/cart/adapter/CartRecommendViewHolder.kt
@@ -13,10 +13,13 @@ class CartRecommendViewHolder private constructor(
) : RecyclerView.ViewHolder(binding.root) {
private lateinit var item: Product
+ init {
+ binding.onClickHandler = onClickHandler
+ }
+
fun bind(item: Product) {
this.item = item
- binding.product = this.item
- binding.onClickHandler = onClickHandler
+ binding.product = item
binding.cartRecommendProductCount.setOnClickHandler(
object : CartCountView.OnClickHandler {
override fun onIncreaseClick() {
diff --git a/app/src/main/java/woowacourse/shopping/ui/catalog/CatalogActivity.kt b/app/src/main/java/woowacourse/shopping/ui/catalog/CatalogActivity.kt
index afb816fdc4..efb297e975 100644
--- a/app/src/main/java/woowacourse/shopping/ui/catalog/CatalogActivity.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/catalog/CatalogActivity.kt
@@ -19,6 +19,7 @@ import woowacourse.shopping.ui.catalog.adapter.product.CatalogAdapter.OnClickHan
import woowacourse.shopping.ui.catalog.adapter.product.CatalogLayoutManager
import woowacourse.shopping.ui.common.DataBindingActivity
import woowacourse.shopping.ui.model.ActivityResult
+import woowacourse.shopping.ui.model.CatalogUiModel
import woowacourse.shopping.ui.productdetail.ProductDetailActivity
class CatalogActivity : DataBindingActivity(R.layout.activity_catalog) {
@@ -76,7 +77,7 @@ class CatalogActivity : DataBindingActivity(R.layout.act
}
override fun onLoadMoreClick() {
- viewModel.loadMoreCartProducts()
+ viewModel.loadMoreCatalogProducts()
}
}
@@ -112,19 +113,25 @@ class CatalogActivity : DataBindingActivity(R.layout.act
}
private fun initObservers() {
- viewModel.products.observe(this) { products ->
- catalogAdapter.submitItems(products.products, !products.page.isLast)
- viewModel.loadCartProductsQuantity()
+ viewModel.uiModel.observe(this) { uiModel ->
+ handleCatalogProducts(uiModel)
+ handleHistoryProducts(uiModel)
+ handlerErrorMessage(uiModel)
}
+ }
- viewModel.historyProducts.observe(this) { products ->
- historyProductAdapter.submitItems(products)
- }
+ private fun handleCatalogProducts(uiModel: CatalogUiModel) {
+ catalogAdapter.submitItems(uiModel.catalogProducts.products, !uiModel.catalogProducts.page.isLast)
+ viewModel.loadCartProductsQuantity()
+ }
- viewModel.isError.observe(this) { errorMessage ->
- errorMessage?.let {
- Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
- }
+ private fun handleHistoryProducts(uiModel: CatalogUiModel) {
+ historyProductAdapter.submitItems(uiModel.historyProducts)
+ }
+
+ private fun handlerErrorMessage(uiModel: CatalogUiModel) {
+ uiModel.connectionErrorMessage?.let {
+ Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
}
}
@@ -152,7 +159,7 @@ class CatalogActivity : DataBindingActivity(R.layout.act
)
ActivityResult.CART_PRODUCT_EDITED.code ->
- viewModel.loadCartProductsByIds(
+ viewModel.loadCartProductsByProductIds(
result.data
?.getLongArrayExtra(ActivityResult.CART_PRODUCT_EDITED.key)
?.toList() ?: emptyList(),
diff --git a/app/src/main/java/woowacourse/shopping/ui/catalog/CatalogViewModel.kt b/app/src/main/java/woowacourse/shopping/ui/catalog/CatalogViewModel.kt
index 2ef33a0078..d491e43bed 100644
--- a/app/src/main/java/woowacourse/shopping/ui/catalog/CatalogViewModel.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/catalog/CatalogViewModel.kt
@@ -5,151 +5,171 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
-import woowacourse.shopping.di.UseCaseModule.decreaseCartProductQuantityUseCase
-import woowacourse.shopping.di.UseCaseModule.getCartProductsQuantityUseCase
-import woowacourse.shopping.di.UseCaseModule.getCatalogProductUseCase
-import woowacourse.shopping.di.UseCaseModule.getCatalogProductsByIdsUseCase
-import woowacourse.shopping.di.UseCaseModule.getCatalogProductsUseCase
-import woowacourse.shopping.di.UseCaseModule.getSearchHistoryUseCase
-import woowacourse.shopping.di.UseCaseModule.increaseCartProductQuantityUseCase
-import woowacourse.shopping.domain.model.HistoryProduct
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.launch
+import woowacourse.shopping.di.UseCaseInjection.decreaseCartProductQuantityUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCartProductsQuantityUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCatalogProductUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCatalogProductsByProductIdsUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCatalogProductsUseCase
+import woowacourse.shopping.di.UseCaseInjection.getSearchHistoryUseCase
+import woowacourse.shopping.di.UseCaseInjection.increaseCartProductQuantityUseCase
import woowacourse.shopping.domain.model.Page.Companion.UNINITIALIZED_PAGE
-import woowacourse.shopping.domain.model.Products
-import woowacourse.shopping.domain.model.Products.Companion.EMPTY_PRODUCTS
import woowacourse.shopping.domain.usecase.DecreaseCartProductQuantityUseCase
import woowacourse.shopping.domain.usecase.GetCartProductsQuantityUseCase
import woowacourse.shopping.domain.usecase.GetCatalogProductUseCase
-import woowacourse.shopping.domain.usecase.GetCatalogProductsByIdsUseCase
+import woowacourse.shopping.domain.usecase.GetCatalogProductsByProductIdsUseCase
import woowacourse.shopping.domain.usecase.GetCatalogProductsUseCase
import woowacourse.shopping.domain.usecase.GetSearchHistoryUseCase
import woowacourse.shopping.domain.usecase.IncreaseCartProductQuantityUseCase
+import woowacourse.shopping.ui.model.CatalogUiModel
class CatalogViewModel(
private val getCatalogProductsUseCase: GetCatalogProductsUseCase,
private val getCatalogProductUseCase: GetCatalogProductUseCase,
- private val getCatalogProductsByIdsUseCase: GetCatalogProductsByIdsUseCase,
+ private val getCatalogProductsByProductIdsUseCase: GetCatalogProductsByProductIdsUseCase,
private val getSearchHistoryUseCase: GetSearchHistoryUseCase,
private val increaseCartProductQuantityUseCase: IncreaseCartProductQuantityUseCase,
private val decreaseCartProductQuantityUseCase: DecreaseCartProductQuantityUseCase,
private val getCartProductsQuantityUseCase: GetCartProductsQuantityUseCase,
) : ViewModel() {
- private val _products: MutableLiveData = MutableLiveData(EMPTY_PRODUCTS)
- val products: LiveData get() = _products
-
- private val _historyProducts: MutableLiveData> =
- MutableLiveData(emptyList())
- val historyProducts: LiveData> get() = _historyProducts
-
- private val _cartProductsQuantity: MutableLiveData =
- MutableLiveData(INITIAL_PRODUCT_QUANTITY)
- val cartProductsQuantity: LiveData get() = _cartProductsQuantity
-
- private val _isLoading: MutableLiveData = MutableLiveData(true)
- val isLoading: LiveData get() = _isLoading
-
- private val _isError: MutableLiveData = MutableLiveData()
- val isError: LiveData get() = _isError
+ private val _uiModel: MutableLiveData = MutableLiveData(CatalogUiModel())
+ val uiModel: LiveData get() = _uiModel
init {
- loadCartProducts()
+ loadCatalogProducts()
}
- private fun loadCartProducts(
- page: Int = products.value?.page?.current ?: UNINITIALIZED_PAGE,
+ private fun loadCatalogProducts(
+ page: Int =
+ uiModel.value
+ ?.catalogProducts
+ ?.page
+ ?.current ?: UNINITIALIZED_PAGE,
count: Int = SHOWN_PRODUCTS_COUNT,
) {
- _isLoading.value = true
- getCatalogProductsUseCase(
- page = page,
- size = count,
- ) { result ->
- result
- .onSuccess { newProducts ->
- _products.postValue(products.value?.plus(newProducts))
- _isLoading.value = false
- }.onFailure {
- _isError.postValue(it.message)
- }
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val products = getCatalogProductsUseCase(page, count)
+
+ updateUiModel { current ->
+ current.copy(
+ catalogProducts = current.catalogProducts.plus(products),
+ isProductsLoading = false,
+ )
+ }
}
}
- fun loadMoreCartProducts() {
+ fun loadMoreCatalogProducts() {
val currentPage =
- products.value
+ uiModel.value
+ ?.catalogProducts
?.page
?.current
?.plus(DEFAULT_PAGE_STEP) ?: UNINITIALIZED_PAGE
- loadCartProducts(page = currentPage)
+
+ loadCatalogProducts(page = currentPage)
}
fun loadHistoryProducts() {
- getSearchHistoryUseCase { historyProducts ->
- _historyProducts.postValue(historyProducts)
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val products = getSearchHistoryUseCase()
+
+ updateUiModel { current ->
+ current.copy(
+ historyProducts = products,
+ )
+ }
}
}
fun increaseCartProduct(productId: Long) {
- runCatching {
- increaseCartProductQuantityUseCase(
- product = products.value?.getProductByProductId(productId) ?: return,
- )
- }.onSuccess {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = uiModel.value?.catalogProducts?.getProductByProductId(productId) ?: return@launch
+ increaseCartProductQuantityUseCase(product)
loadCartProduct(productId)
}
}
fun decreaseCartProduct(productId: Long) {
- runCatching {
- decreaseCartProductQuantityUseCase(
- product = products.value?.getProductByProductId(productId) ?: return,
- )
- }.onSuccess {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = uiModel.value?.catalogProducts?.getProductByProductId(productId) ?: return@launch
+ decreaseCartProductQuantityUseCase(product)
loadCartProduct(productId)
}
}
fun loadCartProduct(productId: Long) {
- getCatalogProductUseCase(productId) { result ->
- result
- .onSuccess { cartProduct ->
- _products.postValue(
- products.value?.updateProduct(
- cartProduct ?: return@getCatalogProductUseCase,
- ),
- )
- }.onFailure {
- _isError.postValue(it.message)
- }
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = getCatalogProductUseCase(productId)
+
+ updateUiModel { current ->
+ current.copy(
+ catalogProducts = current.catalogProducts.updateProduct(product),
+ )
+ }
}
}
- fun loadCartProductsByIds(ids: List) {
- getCatalogProductsByIdsUseCase(ids) { result ->
- result
- .onSuccess { cartProducts ->
- _products.postValue(products.value?.updateProducts(cartProducts))
- }.onFailure {
- _isError.postValue(it.message)
- }
+ fun loadCartProductsByProductIds(productIds: List) {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val products = getCatalogProductsByProductIdsUseCase(productIds)
+
+ updateUiModel { current ->
+ current.copy(
+ catalogProducts = current.catalogProducts.updateProducts(products),
+ )
+ }
}
}
fun loadCartProductsQuantity() {
- getCartProductsQuantityUseCase { result ->
- result
- .onSuccess { quantity ->
- _cartProductsQuantity.postValue(quantity)
- }.onFailure {
- Log.e("CatalogViewModel", it.message.toString())
- }
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val quantity = getCartProductsQuantityUseCase()
+ updateUiModel { current -> current.copy(cartProductsQuantity = quantity) }
}
}
+ private fun updateUiModel(update: (CatalogUiModel) -> CatalogUiModel) {
+ val current = _uiModel.value ?: return
+ _uiModel.value = update(current)
+ }
+
companion object {
+ private const val TAG: String = "CatalogViewModel"
private const val DEFAULT_PAGE_STEP: Int = 1
private const val SHOWN_PRODUCTS_COUNT: Int = 20
- private const val INITIAL_PRODUCT_QUANTITY: Int = 0
val Factory: ViewModelProvider.Factory =
object : ViewModelProvider.Factory {
@@ -161,7 +181,7 @@ class CatalogViewModel(
CatalogViewModel(
getCatalogProductsUseCase = getCatalogProductsUseCase,
getCatalogProductUseCase = getCatalogProductUseCase,
- getCatalogProductsByIdsUseCase = getCatalogProductsByIdsUseCase,
+ getCatalogProductsByProductIdsUseCase = getCatalogProductsByProductIdsUseCase,
getSearchHistoryUseCase = getSearchHistoryUseCase,
increaseCartProductQuantityUseCase = increaseCartProductQuantityUseCase,
decreaseCartProductQuantityUseCase = decreaseCartProductQuantityUseCase,
diff --git a/app/src/main/java/woowacourse/shopping/ui/model/CartProductUiModel.kt b/app/src/main/java/woowacourse/shopping/ui/model/CartProductUiModel.kt
new file mode 100644
index 0000000000..d37974dc79
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/model/CartProductUiModel.kt
@@ -0,0 +1,12 @@
+package woowacourse.shopping.ui.model
+
+import woowacourse.shopping.domain.model.Products
+import woowacourse.shopping.domain.model.Products.Companion.EMPTY_PRODUCTS
+
+data class CartProductUiModel(
+ val cartProducts: Products = EMPTY_PRODUCTS,
+ val recommendedProducts: Products = EMPTY_PRODUCTS,
+ val editedProductIds: Set = emptySet(),
+ val isProductsLoading: Boolean = true,
+ val connectionErrorMessage: String? = null,
+)
diff --git a/app/src/main/java/woowacourse/shopping/ui/model/CatalogUiModel.kt b/app/src/main/java/woowacourse/shopping/ui/model/CatalogUiModel.kt
new file mode 100644
index 0000000000..af4bfe0db4
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/model/CatalogUiModel.kt
@@ -0,0 +1,17 @@
+package woowacourse.shopping.ui.model
+
+import woowacourse.shopping.domain.model.HistoryProduct
+import woowacourse.shopping.domain.model.Products
+import woowacourse.shopping.domain.model.Products.Companion.EMPTY_PRODUCTS
+
+data class CatalogUiModel(
+ val catalogProducts: Products = EMPTY_PRODUCTS,
+ val historyProducts: List = emptyList(),
+ val cartProductsQuantity: Int = INITIAL_PRODUCT_QUANTITY,
+ val isProductsLoading: Boolean = true,
+ val connectionErrorMessage: String? = null,
+) {
+ companion object {
+ private const val INITIAL_PRODUCT_QUANTITY: Int = 0
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/ui/model/PaymentUiModel.kt b/app/src/main/java/woowacourse/shopping/ui/model/PaymentUiModel.kt
new file mode 100644
index 0000000000..d46f101dbd
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/model/PaymentUiModel.kt
@@ -0,0 +1,16 @@
+package woowacourse.shopping.ui.model
+
+import woowacourse.shopping.domain.model.Coupons
+import woowacourse.shopping.domain.model.Coupons.Companion.EMPTY_COUPONS
+import woowacourse.shopping.domain.model.Price
+import woowacourse.shopping.domain.model.Price.Companion.EMPTY_PRICE
+import woowacourse.shopping.domain.model.Products
+import woowacourse.shopping.domain.model.Products.Companion.EMPTY_PRODUCTS
+
+data class PaymentUiModel(
+ val products: Products = EMPTY_PRODUCTS,
+ val coupons: Coupons = EMPTY_COUPONS,
+ val price: Price = EMPTY_PRICE,
+ val isOrderSuccess: Boolean? = null,
+ val connectionErrorMessage: String? = null,
+)
diff --git a/app/src/main/java/woowacourse/shopping/ui/model/ProductDetailUiModel.kt b/app/src/main/java/woowacourse/shopping/ui/model/ProductDetailUiModel.kt
new file mode 100644
index 0000000000..60446b0935
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/model/ProductDetailUiModel.kt
@@ -0,0 +1,12 @@
+package woowacourse.shopping.ui.model
+
+import woowacourse.shopping.domain.model.HistoryProduct
+import woowacourse.shopping.domain.model.Product
+import woowacourse.shopping.domain.model.Product.Companion.EMPTY_PRODUCT
+
+data class ProductDetailUiModel(
+ val product: Product = EMPTY_PRODUCT,
+ val lastHistoryProduct: HistoryProduct? = null,
+ val isCartProductUpdateSuccess: Boolean? = null,
+ val connectionErrorMessage: String? = null,
+)
diff --git a/app/src/main/java/woowacourse/shopping/ui/payment/PaymentActivity.kt b/app/src/main/java/woowacourse/shopping/ui/payment/PaymentActivity.kt
new file mode 100644
index 0000000000..a8170e989a
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/payment/PaymentActivity.kt
@@ -0,0 +1,97 @@
+package woowacourse.shopping.ui.payment
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.MenuItem
+import android.widget.Toast
+import androidx.activity.viewModels
+import com.google.android.material.snackbar.Snackbar
+import woowacourse.shopping.R
+import woowacourse.shopping.databinding.ActivityPaymentBinding
+import woowacourse.shopping.ui.common.DataBindingActivity
+import woowacourse.shopping.ui.model.PaymentUiModel
+import woowacourse.shopping.ui.payment.adapter.PaymentCouponAdapter
+
+class PaymentActivity : DataBindingActivity(R.layout.activity_payment) {
+ private val viewModel: PaymentViewModel by viewModels { PaymentViewModel.Factory }
+ private val paymentCouponAdapter: PaymentCouponAdapter by lazy {
+ PaymentCouponAdapter { couponId -> viewModel.selectCoupon(couponId) }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ initViewBinding()
+ initPaymentProductsInfo()
+ initCouponsView()
+ initObservers()
+ initSupportActionBar()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) finish()
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun initViewBinding() {
+ binding.lifecycleOwner = this
+ binding.viewModel = viewModel
+ }
+
+ private fun initPaymentProductsInfo() {
+ val productIds: List = intent.getLongArrayExtra(KEY_PRODUCT_IDS)?.toList() ?: emptyList()
+ viewModel.loadProducts(productIds)
+ }
+
+ private fun initCouponsView() {
+ binding.paymentCouponsContainer.adapter = paymentCouponAdapter
+ binding.paymentCouponsContainer.isNestedScrollingEnabled = false
+ binding.paymentCouponsContainer.itemAnimator = null
+ }
+
+ private fun initObservers() {
+ viewModel.uiModel.observe(this) { uiModel ->
+ handleCoupons(uiModel)
+ handleErrorMessage(uiModel)
+ handleOrderResult(uiModel)
+ }
+ }
+
+ private fun handleCoupons(uiModel: PaymentUiModel) {
+ if (uiModel.coupons.value.isEmpty() && uiModel.products.products.isNotEmpty()) {
+ viewModel.loadCoupons(uiModel.products)
+ }
+ paymentCouponAdapter.submitList(uiModel.coupons.value)
+ }
+
+ private fun handleErrorMessage(uiModel: PaymentUiModel) {
+ uiModel.connectionErrorMessage?.let {
+ Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
+ }
+ }
+
+ private fun handleOrderResult(uiModel: PaymentUiModel) {
+ if (uiModel.isOrderSuccess == true) {
+ Toast.makeText(this, getString(R.string.payment_order_success), Toast.LENGTH_SHORT).show()
+ finish()
+ }
+ }
+
+ private fun initSupportActionBar() {
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.title = getString(R.string.cart_title)
+ }
+
+ companion object {
+ private const val KEY_PRODUCT_IDS = "PRODUCT_IDS"
+
+ fun newIntent(
+ context: Context,
+ productIds: LongArray,
+ ): Intent =
+ Intent(context, PaymentActivity::class.java).apply {
+ putExtra(KEY_PRODUCT_IDS, productIds)
+ }
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/ui/payment/PaymentViewModel.kt b/app/src/main/java/woowacourse/shopping/ui/payment/PaymentViewModel.kt
new file mode 100644
index 0000000000..633fb0513e
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/payment/PaymentViewModel.kt
@@ -0,0 +1,131 @@
+package woowacourse.shopping.ui.payment
+
+import android.util.Log
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.CreationExtras
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.launch
+import woowacourse.shopping.di.UseCaseInjection.getCatalogProductsByProductIdsUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCouponsUseCase
+import woowacourse.shopping.di.UseCaseInjection.orderProductsUseCase
+import woowacourse.shopping.domain.model.Products
+import woowacourse.shopping.domain.usecase.GetCatalogProductsByProductIdsUseCase
+import woowacourse.shopping.domain.usecase.GetCouponsUseCase
+import woowacourse.shopping.domain.usecase.OrderProductsUseCase
+import woowacourse.shopping.ui.model.PaymentUiModel
+import java.time.LocalDateTime
+
+class PaymentViewModel(
+ private val getCatalogProductsByProductIdsUseCase: GetCatalogProductsByProductIdsUseCase,
+ private val getCouponsUseCase: GetCouponsUseCase,
+ private val orderProductsUseCase: OrderProductsUseCase,
+) : ViewModel() {
+ private val _uiModel: MutableLiveData = MutableLiveData(PaymentUiModel())
+ val uiModel: LiveData get() = _uiModel
+
+ fun loadProducts(productIds: List) {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val products =
+ Products(
+ getCatalogProductsByProductIdsUseCase(productIds)
+ .filterNot { it.cartId == null }
+ .map { it.copy(isSelected = true) },
+ )
+ updateUiModel { current ->
+ current.copy(
+ products = products,
+ )
+ }
+ }
+ }
+
+ fun loadCoupons(
+ products: Products,
+ nowDateTime: LocalDateTime = LocalDateTime.now(),
+ ) {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val coupons = getCouponsUseCase()
+ updateUiModel { current ->
+ current.copy(
+ coupons = coupons.filterAvailableCoupons(products, nowDateTime),
+ price = current.coupons.applyCoupon(products, nowDateTime),
+ )
+ }
+ }
+ }
+
+ fun selectCoupon(
+ couponId: Int,
+ nowDateTime: LocalDateTime = LocalDateTime.now(),
+ ) {
+ updateUiModel { current ->
+ current.copy(
+ coupons = current.coupons.selectCoupon(couponId),
+ )
+ }
+ updateUiModel { current ->
+ current.copy(
+ price = current.coupons.applyCoupon(current.products, nowDateTime),
+ )
+ }
+ }
+
+ fun orderProducts() {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current ->
+ current.copy(
+ isOrderSuccess = false,
+ connectionErrorMessage = e.message.toString(),
+ )
+ }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val cartIds: Set = uiModel.value?.products?.getSelectedCartIds() ?: return@launch
+ orderProductsUseCase(cartIds)
+
+ updateUiModel { current ->
+ current.copy(isOrderSuccess = true)
+ }
+ }
+ }
+
+ private fun updateUiModel(update: (PaymentUiModel) -> PaymentUiModel) {
+ val current = _uiModel.value ?: return
+ _uiModel.value = update(current)
+ }
+
+ companion object {
+ const val MAX_USABLE_COUPON_COUNT: Int = 1
+ private const val TAG: String = "PaymentViewModel"
+
+ val Factory: ViewModelProvider.Factory =
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(
+ modelClass: Class,
+ extras: CreationExtras,
+ ): T =
+ PaymentViewModel(
+ getCatalogProductsByProductIdsUseCase = getCatalogProductsByProductIdsUseCase,
+ getCouponsUseCase = getCouponsUseCase,
+ orderProductsUseCase = orderProductsUseCase,
+ ) as T
+ }
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponAdapter.kt b/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponAdapter.kt
new file mode 100644
index 0000000000..f7b034e8d3
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponAdapter.kt
@@ -0,0 +1,21 @@
+package woowacourse.shopping.ui.payment.adapter
+
+import android.view.ViewGroup
+import androidx.recyclerview.widget.ListAdapter
+import woowacourse.shopping.domain.model.Coupon
+
+class PaymentCouponAdapter(
+ private val onClickHandler: PaymentCouponViewHolder.OnClickHandler,
+) : ListAdapter(PaymentCouponDiffCallback) {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int,
+ ): PaymentCouponViewHolder = PaymentCouponViewHolder.from(parent, onClickHandler)
+
+ override fun onBindViewHolder(
+ holder: PaymentCouponViewHolder,
+ position: Int,
+ ) {
+ holder.bind(getItem(position))
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponDiffCallback.kt b/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponDiffCallback.kt
new file mode 100644
index 0000000000..c99bdc52b5
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponDiffCallback.kt
@@ -0,0 +1,16 @@
+package woowacourse.shopping.ui.payment.adapter
+
+import androidx.recyclerview.widget.DiffUtil
+import woowacourse.shopping.domain.model.Coupon
+
+object PaymentCouponDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: Coupon,
+ newItem: Coupon,
+ ): Boolean = oldItem.detail.id == newItem.detail.id
+
+ override fun areContentsTheSame(
+ oldItem: Coupon,
+ newItem: Coupon,
+ ): Boolean = oldItem.detail == newItem.detail && oldItem.isSelected == newItem.isSelected
+}
diff --git a/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponViewHolder.kt b/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponViewHolder.kt
new file mode 100644
index 0000000000..6b80288298
--- /dev/null
+++ b/app/src/main/java/woowacourse/shopping/ui/payment/adapter/PaymentCouponViewHolder.kt
@@ -0,0 +1,34 @@
+package woowacourse.shopping.ui.payment.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import woowacourse.shopping.databinding.ItemPaymentCouponBinding
+import woowacourse.shopping.domain.model.Coupon
+
+class PaymentCouponViewHolder private constructor(
+ private val binding: ItemPaymentCouponBinding,
+ onClickHandler: OnClickHandler,
+) : RecyclerView.ViewHolder(binding.root) {
+ init {
+ binding.onClickHandler = onClickHandler
+ }
+
+ fun bind(item: Coupon) {
+ binding.coupon = item
+ }
+
+ fun interface OnClickHandler {
+ fun onCouponSelected(couponId: Int)
+ }
+
+ companion object {
+ fun from(
+ parent: ViewGroup,
+ onClickHandler: OnClickHandler,
+ ): PaymentCouponViewHolder {
+ val inflate = ItemPaymentCouponBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return PaymentCouponViewHolder(inflate, onClickHandler)
+ }
+ }
+}
diff --git a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailActivity.kt b/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailActivity.kt
index 5eae215457..05f21157d4 100644
--- a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailActivity.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailActivity.kt
@@ -13,6 +13,7 @@ import woowacourse.shopping.databinding.ActivityProductDetailBinding
import woowacourse.shopping.ui.common.DataBindingActivity
import woowacourse.shopping.ui.custom.CartCountView
import woowacourse.shopping.ui.model.ActivityResult
+import woowacourse.shopping.ui.model.ProductDetailUiModel
class ProductDetailActivity : DataBindingActivity(R.layout.activity_product_detail) {
private val viewModel: ProductDetailViewModel by viewModels { ProductDetailViewModel.Factory }
@@ -65,33 +66,35 @@ class ProductDetailActivity : DataBindingActivity(
}
private fun initObservers() {
- viewModel.product.observe(this) { product ->
- product?.let {
- viewModel.addHistoryProduct(product.productDetail)
- }
+ viewModel.uiModel.observe(this) { uiModel ->
+ handleHistoryProduct(uiModel)
+ handleCartProductAddResult(uiModel)
+ handleErrorMessage(uiModel)
}
+ }
- viewModel.onCartProductAddSuccess.observe(this) { isSuccess ->
- isSuccess?.let { handleCartProductAddResult(it) }
- }
+ private fun handleHistoryProduct(uiModel: ProductDetailUiModel) {
+ viewModel.addHistoryProduct(uiModel.product.productDetail)
+ }
- viewModel.isError.observe(this) { errorMessage ->
- errorMessage?.let {
- Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
+ private fun handleCartProductAddResult(uiModel: ProductDetailUiModel) {
+ uiModel.isCartProductUpdateSuccess?.let { isSuccess ->
+ if (isSuccess) {
+ setResult(
+ ActivityResult.PRODUCT_DETAIL_CART_UPDATED.code,
+ Intent().apply {
+ putExtra(ActivityResult.PRODUCT_DETAIL_CART_UPDATED.key, productId)
+ },
+ )
+ Toast.makeText(this, getString(R.string.product_detail_cart_add_success), Toast.LENGTH_SHORT).show()
+ finish()
}
}
}
- private fun handleCartProductAddResult(isSuccess: Boolean) {
- if (isSuccess) {
- setResult(
- ActivityResult.PRODUCT_DETAIL_CART_UPDATED.code,
- Intent().apply {
- putExtra(ActivityResult.PRODUCT_DETAIL_CART_UPDATED.key, productId)
- },
- )
- Toast.makeText(this, getString(R.string.product_detail_cart_add_success), Toast.LENGTH_SHORT).show()
- finish()
+ private fun handleErrorMessage(uiModel: ProductDetailUiModel) {
+ uiModel.connectionErrorMessage?.let {
+ Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
}
}
diff --git a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailViewModel.kt b/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailViewModel.kt
index 784d9a5e65..4bc0a85a17 100644
--- a/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailViewModel.kt
+++ b/app/src/main/java/woowacourse/shopping/ui/productdetail/ProductDetailViewModel.kt
@@ -1,24 +1,24 @@
package woowacourse.shopping.ui.productdetail
+import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
-import woowacourse.shopping.di.UseCaseModule.addSearchHistoryUseCase
-import woowacourse.shopping.di.UseCaseModule.getCatalogProductUseCase
-import woowacourse.shopping.di.UseCaseModule.getRecentSearchHistoryUseCase
-import woowacourse.shopping.di.UseCaseModule.updateCartProductUseCase
-import woowacourse.shopping.domain.model.HistoryProduct
-import woowacourse.shopping.domain.model.Product
-import woowacourse.shopping.domain.model.Product.Companion.EMPTY_PRODUCT
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.launch
+import woowacourse.shopping.di.UseCaseInjection.addSearchHistoryUseCase
+import woowacourse.shopping.di.UseCaseInjection.getCatalogProductUseCase
+import woowacourse.shopping.di.UseCaseInjection.getRecentSearchHistoryUseCase
+import woowacourse.shopping.di.UseCaseInjection.updateCartProductUseCase
import woowacourse.shopping.domain.model.ProductDetail
import woowacourse.shopping.domain.usecase.AddSearchHistoryUseCase
import woowacourse.shopping.domain.usecase.GetCatalogProductUseCase
import woowacourse.shopping.domain.usecase.GetRecentSearchHistoryUseCase
import woowacourse.shopping.domain.usecase.UpdateCartProductUseCase
-import woowacourse.shopping.util.MutableSingleLiveData
-import woowacourse.shopping.util.SingleLiveData
+import woowacourse.shopping.ui.model.ProductDetailUiModel
class ProductDetailViewModel(
private val getCatalogProductUseCase: GetCatalogProductUseCase,
@@ -26,66 +26,79 @@ class ProductDetailViewModel(
private val addSearchHistoryUseCase: AddSearchHistoryUseCase,
private val updateCartProductUseCase: UpdateCartProductUseCase,
) : ViewModel() {
- private val _product: MutableLiveData =
- MutableLiveData(EMPTY_PRODUCT)
- val product: LiveData get() = _product
+ private val _uiModel: MutableLiveData = MutableLiveData(ProductDetailUiModel())
+ val uiModel: LiveData get() = _uiModel
- private val _lastHistoryProduct: MutableLiveData = MutableLiveData(null)
- val lastHistoryProduct: LiveData get() = _lastHistoryProduct
-
- private val _onCartProductAddSuccess: MutableSingleLiveData =
- MutableSingleLiveData(null)
- val onCartProductAddSuccess: SingleLiveData get() = _onCartProductAddSuccess
-
- private val _isError: MutableLiveData = MutableLiveData()
- val isError: LiveData get() = _isError
-
- fun loadProductDetail(id: Long) {
- getCatalogProductUseCase(id) { result ->
- result
- .onSuccess { catalogProduct ->
- _product.postValue(catalogProduct ?: EMPTY_PRODUCT)
- }.onFailure {
- _isError.postValue(it.message)
- }
+ fun loadProductDetail(productId: Long) {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = getCatalogProductUseCase(productId)
+ updateUiModel { current -> current.copy(product = product) }
}
}
fun loadLastHistoryProduct() {
- getRecentSearchHistoryUseCase { historyProduct ->
- _lastHistoryProduct.postValue(historyProduct)
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ val product = getRecentSearchHistoryUseCase()
+ updateUiModel { current -> current.copy(lastHistoryProduct = product) }
}
}
fun addHistoryProduct(productDetail: ProductDetail) {
- addSearchHistoryUseCase(productDetail)
+ viewModelScope.launch {
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ addSearchHistoryUseCase(productDetail)
+ }
+ }
}
fun decreaseCartProductQuantity() {
- _product.value = product.value?.decreaseQuantity()
+ val uiModel = uiModel.value ?: return
+ updateUiModel { current -> current.copy(product = uiModel.product.decreaseQuantity()) }
}
fun increaseCartProductQuantity() {
- _product.value = product.value?.increaseQuantity()
+ val uiModel = uiModel.value ?: return
+ updateUiModel { current -> current.copy(product = uiModel.product.increaseQuantity()) }
}
fun updateCartProduct() {
- val product: Product = product.value ?: return
- updateCartProductUseCase(
- productId = product.productDetail.id,
- cartId = product.cartId,
- quantity = product.quantity,
- ) { result ->
- result
- .onSuccess {
- _onCartProductAddSuccess.postValue(true)
- }.onFailure {
- _isError.postValue(it.message)
- }
+ val uiModel = uiModel.value ?: return
+ viewModelScope.launch(
+ CoroutineExceptionHandler { _, e ->
+ updateUiModel { current -> current.copy(connectionErrorMessage = e.message.toString()) }
+ Log.e(TAG, e.message.toString())
+ },
+ ) {
+ updateCartProductUseCase(
+ productId = uiModel.product.productDetail.id,
+ cartId = uiModel.product.cartId,
+ quantity = uiModel.product.quantity,
+ )
+ updateUiModel { current -> current.copy(isCartProductUpdateSuccess = true) }
}
}
+ private fun updateUiModel(update: (ProductDetailUiModel) -> ProductDetailUiModel) {
+ val current = _uiModel.value ?: return
+ _uiModel.value = update(current)
+ }
+
companion object {
+ private const val TAG: String = "ProductDetailViewModel"
val Factory: ViewModelProvider.Factory =
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
diff --git a/app/src/main/java/woowacourse/shopping/util/Event.kt b/app/src/main/java/woowacourse/shopping/util/Event.kt
deleted file mode 100644
index de6defd373..0000000000
--- a/app/src/main/java/woowacourse/shopping/util/Event.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package woowacourse.shopping.util
-
-/**
- * 출처: MVVM 피드백 강의자료
- * Used as a wrapper for data that is exposed via a LiveData that represents an event.
- */
-open class Event(
- private val content: T,
-) {
- var hasBeenHandled = false
- private set // Allow external read but not write
-
- /**
- * Returns the content and prevents its use again.
- */
- fun getContentIfNotHandled(): T? =
- if (hasBeenHandled) {
- null
- } else {
- hasBeenHandled = true
- content
- }
-
- /**
- * Returns the content, even if it's already been handled.
- */
- fun peekContent(): T = content
-}
diff --git a/app/src/main/java/woowacourse/shopping/util/MutableSingleLiveData.kt b/app/src/main/java/woowacourse/shopping/util/MutableSingleLiveData.kt
deleted file mode 100644
index 80a1241f5a..0000000000
--- a/app/src/main/java/woowacourse/shopping/util/MutableSingleLiveData.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package woowacourse.shopping.util
-
-/**
- * 출처: MVVM 피드백 강의자료
- */
-class MutableSingleLiveData : SingleLiveData {
- constructor() : super()
-
- constructor(value: T) : super(value)
-
- public override fun postValue(value: T) {
- super.postValue(value)
- }
-
- public override fun setValue(value: T) {
- super.setValue(value)
- }
-}
diff --git a/app/src/main/java/woowacourse/shopping/util/SingleLiveData.kt b/app/src/main/java/woowacourse/shopping/util/SingleLiveData.kt
deleted file mode 100644
index cf24416365..0000000000
--- a/app/src/main/java/woowacourse/shopping/util/SingleLiveData.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package woowacourse.shopping.util
-
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.MutableLiveData
-
-/**
- * 출처: MVVM 피드백 강의자료
- */
-abstract class SingleLiveData {
- private val liveData = MutableLiveData>()
-
- protected constructor()
-
- protected constructor(value: T) {
- liveData.value = Event(value)
- }
-
- protected open fun setValue(value: T) {
- liveData.value = Event(value)
- }
-
- protected open fun postValue(value: T) {
- liveData.postValue(Event(value))
- }
-
- fun getValue() = liveData.value?.peekContent()
-
- fun observe(
- owner: LifecycleOwner,
- onResult: (T) -> Unit,
- ) {
- liveData.observe(owner) { it.getContentIfNotHandled()?.let(onResult) }
- }
-
- fun observePeek(
- owner: LifecycleOwner,
- onResult: (T) -> Unit,
- ) {
- liveData.observe(owner) { onResult(it.peekContent()) }
- }
-}
diff --git a/app/src/main/res/drawable/bg_payment_white_radius_4dp_stroke_gray_4.xml b/app/src/main/res/drawable/bg_payment_white_radius_4dp_stroke_gray_4.xml
new file mode 100644
index 0000000000..86512d03c8
--- /dev/null
+++ b/app/src/main/res/drawable/bg_payment_white_radius_4dp_stroke_gray_4.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_cart.xml b/app/src/main/res/layout/activity_cart.xml
index a00c5c9ab9..5e21c07fd3 100644
--- a/app/src/main/res/layout/activity_cart.xml
+++ b/app/src/main/res/layout/activity_cart.xml
@@ -45,9 +45,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginBottom="12dp"
- android:checked="@{viewModel.cartProducts.allSelected}"
+ android:checked="@{viewModel.uiModel.cartProducts.allSelected}"
android:onClick="@{() -> viewModel.toggleAllCartProductsSelection()}"
- app:isGone="@{viewModel.cartProducts.products.empty}"
app:layout_constraintBottom_toBottomOf="@id/cartOrderInfoContainer"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/cartOrderInfoContainer"
@@ -61,7 +60,6 @@
android:text="@string/cart_select_all"
android:textColor="@color/white"
android:textStyle="bold"
- app:isGone="@{viewModel.cartProducts.products.empty}"
app:layout_constraintBottom_toBottomOf="@id/cartCheckAllButton"
app:layout_constraintEnd_toEndOf="@id/cartCheckAllButton"
app:layout_constraintStart_toStartOf="@id/cartCheckAllButton" />
@@ -70,7 +68,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
- android:text="@{@string/cart_order_price(viewModel.totalOrderPrice)}"
+ android:text="@{@string/cart_order_price(viewModel.uiModel.cartProducts.selectedProductsPrice + viewModel.uiModel.recommendedProducts.selectedProductsPrice)}"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
@@ -88,7 +86,7 @@
android:maxWidth="156dp"
android:onClick="@{()->onClickHandler.onOrderClick()}"
android:paddingHorizontal="16dp"
- android:text="@{@string/cart_order(viewModel.cartProducts.getSelectedCartProductQuantity() + viewModel.recommendedProducts.getSelectedCartRecommendProductQuantity())}"
+ android:text="@{@string/cart_order(viewModel.uiModel.cartProducts.selectedProductsQuantity + viewModel.uiModel.recommendedProducts.selectedProductsQuantity)}"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
diff --git a/app/src/main/res/layout/activity_catalog.xml b/app/src/main/res/layout/activity_catalog.xml
index 9d2e8269e1..12be2be8df 100644
--- a/app/src/main/res/layout/activity_catalog.xml
+++ b/app/src/main/res/layout/activity_catalog.xml
@@ -25,7 +25,7 @@
android:id="@+id/productsHistoryContainer"
android:layout_width="0dp"
android:layout_height="228dp"
- app:isGone="@{viewModel.historyProducts.empty || viewModel.loading}"
+ app:isGone="@{viewModel.uiModel.historyProducts.empty || viewModel.uiModel.productsLoading}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
@@ -82,7 +82,7 @@
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:background="@color/white"
- app:isGone="@{!viewModel.loading}"
+ app:isGone="@{!viewModel.uiModel.productsLoading}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
diff --git a/app/src/main/res/layout/activity_payment.xml b/app/src/main/res/layout/activity_payment.xml
new file mode 100644
index 0000000000..5c912cf728
--- /dev/null
+++ b/app/src/main/res/layout/activity_payment.xml
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_product_detail.xml b/app/src/main/res/layout/activity_product_detail.xml
index 7c2ed1aa4b..4d5b6434d4 100644
--- a/app/src/main/res/layout/activity_product_detail.xml
+++ b/app/src/main/res/layout/activity_product_detail.xml
@@ -35,7 +35,7 @@
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
- app:imageUrl="@{viewModel.product.productDetail.imageUrl}"
+ app:imageUrl="@{viewModel.uiModel.product.productDetail.imageUrl}"
app:layout_constraintDimensionRatio="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -48,7 +48,7 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="18dp"
android:layout_marginVertical="16dp"
- android:text="@{viewModel.product.productDetail.name}"
+ android:text="@{viewModel.uiModel.product.productDetail.name}"
android:textColor="@color/black"
android:textSize="24sp"
android:textStyle="bold"
@@ -74,7 +74,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginTop="16dp"
- android:text="@{@string/catalog_price(viewModel.product.totalPrice)}"
+ android:text="@{@string/catalog_price(viewModel.uiModel.product.totalPrice)}"
android:textColor="@color/black"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
@@ -86,7 +86,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="18dp"
- app:count="@{viewModel.product.quantity}"
+ app:count="@{viewModel.uiModel.product.quantity}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/productDetailPrice" />
@@ -97,10 +97,10 @@
android:layout_marginHorizontal="18dp"
android:layout_marginVertical="30dp"
android:background="@drawable/bg_products_gray_4_radius_right_4dp"
- android:onClick="@{()->onClickHandler.onLastHistoryProductClick(viewModel.lastHistoryProduct.productId)}"
+ android:onClick="@{()->onClickHandler.onLastHistoryProductClick(viewModel.uiModel.lastHistoryProduct.productId)}"
android:orientation="vertical"
android:paddingHorizontal="18dp"
- app:isGone="@{viewModel.lastHistoryProduct == null}"
+ app:isGone="@{viewModel.uiModel.lastHistoryProduct == null}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -122,7 +122,7 @@
android:layout_marginBottom="16dp"
android:ellipsize="end"
android:maxLines="1"
- android:text="@{viewModel.lastHistoryProduct.name}"
+ android:text="@{viewModel.uiModel.lastHistoryProduct.name}"
android:textColor="@color/gray_5"
android:textSize="18sp"
tools:text="마지막으로 본 상품 대충 나올 수 있지" />
@@ -135,7 +135,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_cart_add"
- android:enabled="@{viewModel.product.quantity > 0}"
+ android:enabled="@{viewModel.uiModel.product.quantity > 0}"
android:onClick="@{()->onClickHandler.onAddCartProductClick()}"
android:paddingVertical="12dp"
android:text="@string/product_detail_cart_add"
diff --git a/app/src/main/res/layout/fragment_cart_product.xml b/app/src/main/res/layout/fragment_cart_product.xml
index 9bca9546ef..03b47f0512 100644
--- a/app/src/main/res/layout/fragment_cart_product.xml
+++ b/app/src/main/res/layout/fragment_cart_product.xml
@@ -30,7 +30,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:gravity="center"
- app:isGone="@{viewModel.cartProducts.page.single}"
+ app:isGone="@{viewModel.uiModel.cartProducts.page.single}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
@@ -41,7 +41,7 @@
android:layout_marginEnd="16dp"
android:background="@drawable/selector_cart_previous"
android:clickable="true"
- android:enabled="@{!viewModel.cartProducts.page.first}"
+ android:enabled="@{!viewModel.uiModel.cartProducts.page.first}"
android:onClick="@{()->viewModel.decreasePage(viewModel.DEFAULT_PAGE_STEP)}"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
@@ -54,7 +54,7 @@
android:id="@+id/cartPageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@{String.valueOf(viewModel.cartProducts.page.current + viewModel.PAGE_INDEX_OFFSET)}"
+ android:text="@{String.valueOf(viewModel.uiModel.cartProducts.page.current + viewModel.PAGE_INDEX_OFFSET)}"
android:textColor="@color/gray_5"
android:textSize="22sp"
android:textStyle="bold"
@@ -66,7 +66,7 @@
android:layout_marginStart="16dp"
android:background="@drawable/selector_cart_next"
android:clickable="true"
- android:enabled="@{!viewModel.cartProducts.page.last}"
+ android:enabled="@{!viewModel.uiModel.cartProducts.page.last}"
android:onClick="@{()->viewModel.increasePage(viewModel.DEFAULT_PAGE_STEP)}"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
@@ -80,7 +80,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:src="@drawable/img_cart_empty"
- app:isGone="@{!viewModel.cartProducts.products.empty || viewModel.loading}"
+ app:isGone="@{!viewModel.uiModel.cartProducts.products.empty || viewModel.uiModel.productsLoading}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -92,7 +92,7 @@
android:layout_height="0dp"
android:layout_marginTop="20dp"
android:background="@color/white"
- app:isGone="@{!viewModel.loading}"
+ app:isGone="@{!viewModel.uiModel.productsLoading}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@@ -122,4 +122,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/item_payment_coupon.xml b/app/src/main/res/layout/item_payment_coupon.xml
new file mode 100644
index 0000000000..47fe1fc83a
--- /dev/null
+++ b/app/src/main/res/layout/item_payment_coupon.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/layout_catalog_cart_quantity.xml b/app/src/main/res/layout/layout_catalog_cart_quantity.xml
index 13f89cc38d..2412063a5d 100644
--- a/app/src/main/res/layout/layout_catalog_cart_quantity.xml
+++ b/app/src/main/res/layout/layout_catalog_cart_quantity.xml
@@ -33,14 +33,14 @@
android:layout_marginBottom="14dp"
android:background="@drawable/bg_products_mint_1_circle"
android:gravity="center"
- android:text="@{String.valueOf(viewModel.cartProductsQuantity)}"
+ android:text="@{String.valueOf(viewModel.uiModel.cartProductsQuantity)}"
android:textColor="@color/white"
android:textSize="11sp"
android:textStyle="bold"
- app:isGone="@{viewModel.cartProductsQuantity <= 0}"
+ app:isGone="@{viewModel.uiModel.cartProductsQuantity <= 0}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="1"
tools:visibility="visible" />
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6090a2cb2f..3464933401 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -26,4 +26,18 @@
주문하기(%d)
이런 상품은 어떠세요?
* 최근 본 상품 기반으로 좋아하실 것 같은 상품들을 추천해드려요.
+
+
+ 성공적으로 주문이 완료되었습니다!
+ 만료일: %d년 %d월 %d일
+ 최소 주문 금액: %,d원
+ 적용 가능한 쿠폰
+ * 쿠폰은 %d개만 적용 가능합니다.
+ 주문 금액
+ 쿠폰 할인 금액
+ 배송비
+ 총 결제 금액
+ %,d원
+ -%,d원
+ 결제하기
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/CartProductDetailTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/CartProductDetailTest.kt
deleted file mode 100644
index 9c4669a2d8..0000000000
--- a/app/src/test/java/woowacourse/shopping/domain/model/CartProductDetailTest.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package woowacourse.shopping.domain.model
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.jupiter.api.Test
-import woowacourse.shopping.model.DUMMY_CART_PRODUCT_1
-
-class CartProductDetailTest {
- @Test
- fun `총 가격은 단가와 수량의 곱으로 계산된다`() {
- // given
- val cartProduct = DUMMY_CART_PRODUCT_1
-
- // when
- val total = cartProduct.totalPrice
-
- // then
- assertThat(total).isEqualTo(cartProduct.productDetail.price * cartProduct.quantity)
- }
-}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/CartProductsTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/CartProductsTest.kt
deleted file mode 100644
index 02998201cb..0000000000
--- a/app/src/test/java/woowacourse/shopping/domain/model/CartProductsTest.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package woowacourse.shopping.domain.model
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.jupiter.api.Test
-import woowacourse.shopping.model.DUMMY_CART_PRODUCTS_1
-import woowacourse.shopping.model.DUMMY_CART_PRODUCT_2
-
-class CartProductsTest {
- @Test
- fun `상품 수량을 수정하면 해당 상품을 반영한다`() {
- // given
- val original = DUMMY_CART_PRODUCTS_1
- val targetId = DUMMY_CART_PRODUCT_2.productDetail.id
- val newQuantity = 100
-
- // when
- val updated = original.updateCartProductQuantity(targetId, newQuantity)
-
- // then
- val modified = updated.products.first { it.productDetail.id == targetId }
- assertThat(modified.quantity).isEqualTo(newQuantity)
-
- val originalTarget = original.products.first { it.productDetail.id == targetId }
- assertThat(originalTarget.quantity).isNotEqualTo(newQuantity)
-
- val otherOriginal = original.products.filter { it.productDetail.id != targetId }
- val otherUpdated = updated.products.filter { it.productDetail.id != targetId }
- assertThat(otherUpdated).containsExactlyElementsIn(otherOriginal)
- }
-}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/CouponsTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/CouponsTest.kt
new file mode 100644
index 0000000000..41b00e8ed3
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/domain/model/CouponsTest.kt
@@ -0,0 +1,48 @@
+package woowacourse.shopping.domain.model
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.jupiter.api.Test
+import woowacourse.shopping.model.DUMMY_COUPONS_1
+
+class CouponsTest {
+ @Test
+ fun `쿠폰 ID를 입력하면 쿠폰이 선택된다`() {
+ // given
+ val coupons = DUMMY_COUPONS_1
+
+ // when
+ val selected = coupons.selectCoupon(2)
+
+ // then
+ assertThat(selected.value.count { it.isSelected }).isEqualTo(1)
+ assertThat(selected.value.find { it.detail.id == 2 }?.isSelected).isTrue()
+ assertThat(selected.value.filter { it.detail.id != 2 }.all { !it.isSelected }).isTrue()
+ }
+
+ @Test
+ fun `같은 쿠폰 ID를 두 번 선택하면 선택이 해제된다`() {
+ // given
+ val coupons = DUMMY_COUPONS_1
+
+ // when
+ val onceSelected = coupons.selectCoupon(1)
+ val twiceSelected = onceSelected.selectCoupon(1)
+
+ // then
+ assertThat(twiceSelected.value.all { !it.isSelected }).isTrue()
+ }
+
+ @Test
+ fun `다른 쿠폰을 선택하면 이전 쿠폰은 해제되고 새 쿠폰만 선택된다`() {
+ // given
+ val selected1 = DUMMY_COUPONS_1.selectCoupon(1)
+
+ // when
+ val selected3 = selected1.selectCoupon(3)
+
+ // then
+ assertThat(selected3.value.count { it.isSelected }).isEqualTo(1)
+ assertThat(selected3.value.find { it.detail.id == 3 }?.isSelected).isTrue()
+ assertThat(selected3.value.filter { it.detail.id != 3 }.all { !it.isSelected }).isTrue()
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/FixedDiscountCouponTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/FixedDiscountCouponTest.kt
new file mode 100644
index 0000000000..e424392a0b
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/domain/model/FixedDiscountCouponTest.kt
@@ -0,0 +1,88 @@
+package woowacourse.shopping.domain.model
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.jupiter.api.Test
+import woowacourse.shopping.model.DUMMY_COUPON_1
+import woowacourse.shopping.model.DUMMY_LOCAL_DATE_TIME_1
+import woowacourse.shopping.model.DUMMY_PRODUCTS_2
+import woowacourse.shopping.model.DUMMY_PRODUCTS_3
+import woowacourse.shopping.model.DUMMY_PRODUCTS_4
+
+class FixedDiscountCouponTest {
+ @Test
+ fun `조건을 만족하면 쿠폰 할인이 적용된다`() {
+ // given
+ val coupon = DUMMY_COUPON_1
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.original).isEqualTo(1119200)
+ assertThat(price.discount).isEqualTo(5000)
+ assertThat(price.shipping).isEqualTo(3000)
+ assertThat(price.result).isEqualTo(1117200)
+ }
+
+ @Test
+ fun `조건을 만족하지 않으면 할인이 적용되지 않는다`() {
+ // given
+ val coupon = DUMMY_COUPON_1
+ val products = DUMMY_PRODUCTS_4
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.original).isEqualTo(11900)
+ assertThat(price.discount).isEqualTo(0)
+ assertThat(price.result).isEqualTo(14900)
+ }
+
+ @Test
+ fun `유효기간이 지나면 할인은 적용되지 않는다`() {
+ // given
+ val expiredCoupon =
+ DUMMY_COUPON_1.copy(
+ detail =
+ DUMMY_COUPON_1.detail.copy(
+ expirationDate = DUMMY_LOCAL_DATE_TIME_1.toLocalDate().minusDays(1),
+ ),
+ )
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val price = expiredCoupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.discount).isEqualTo(0)
+ assertThat(price.result).isEqualTo(products.selectedProductsPrice + 3000)
+ }
+
+ @Test
+ fun `최소 금액과 날짜 조건을 만족하면 쿠폰을 사용할 수 있다`() {
+ // given
+ val coupon = DUMMY_COUPON_1
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val result = coupon.getIsAvailable(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun `최소 금액을 만족하지 않으면 쿠폰을 사용할 수 없다`() {
+ // given
+ val coupon = DUMMY_COUPON_1
+ val products = DUMMY_PRODUCTS_2
+
+ // when
+ val result = coupon.getIsAvailable(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(result).isFalse()
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/FreeShippingCouponTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/FreeShippingCouponTest.kt
new file mode 100644
index 0000000000..12281ebb7b
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/domain/model/FreeShippingCouponTest.kt
@@ -0,0 +1,88 @@
+package woowacourse.shopping.domain.model
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.jupiter.api.Test
+import woowacourse.shopping.model.DUMMY_COUPON_3
+import woowacourse.shopping.model.DUMMY_LOCAL_DATE_TIME_1
+import woowacourse.shopping.model.DUMMY_PRODUCTS_2
+import woowacourse.shopping.model.DUMMY_PRODUCTS_3
+import woowacourse.shopping.model.DUMMY_PRODUCTS_4
+
+class FreeShippingCouponTest {
+ @Test
+ fun `조건을 만족하면 배송비 금액만큼 할인된다`() {
+ // given
+ val coupon = DUMMY_COUPON_3
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.original).isEqualTo(1119200)
+ assertThat(price.discount).isEqualTo(3000)
+ assertThat(price.shipping).isEqualTo(3000)
+ assertThat(price.result).isEqualTo(1119200)
+ }
+
+ @Test
+ fun `조건을 만족하지 않으면 할인은 적용되지 않는다`() {
+ // given
+ val coupon = DUMMY_COUPON_3
+ val products = DUMMY_PRODUCTS_4
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.original).isEqualTo(11900)
+ assertThat(price.discount).isEqualTo(0)
+ assertThat(price.result).isEqualTo(14900)
+ }
+
+ @Test
+ fun `유효기간이 지나면 할인은 적용되지 않는다`() {
+ // given
+ val expiredCoupon =
+ DUMMY_COUPON_3.copy(
+ detail =
+ DUMMY_COUPON_3.detail.copy(
+ expirationDate = DUMMY_LOCAL_DATE_TIME_1.toLocalDate().minusDays(1),
+ ),
+ )
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val price = expiredCoupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.discount).isEqualTo(0)
+ assertThat(price.result).isEqualTo(products.selectedProductsPrice + 3000)
+ }
+
+ @Test
+ fun `최소 금액과 날짜 조건을 만족하면 쿠폰을 사용할 수 있다`() {
+ // given
+ val coupon = DUMMY_COUPON_3
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val result = coupon.getIsAvailable(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun `최소 금액을 만족하지 않으면 쿠폰을 사용할 수 없다`() {
+ // given
+ val coupon = DUMMY_COUPON_3
+ val products = DUMMY_PRODUCTS_2
+
+ // when
+ val result = coupon.getIsAvailable(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(result).isFalse()
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/PageTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/PageTest.kt
new file mode 100644
index 0000000000..ad1b156f24
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/domain/model/PageTest.kt
@@ -0,0 +1,30 @@
+package woowacourse.shopping.domain.model
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+class PageTest {
+ @Test
+ fun `시작 페이지와 종료 페이지가 같으면 한 페이지만 존재함을 알려준다`() {
+ // given
+ val singlePage = Page(current = 0, isFirst = true, isLast = true)
+
+ // when
+ val result = singlePage.isSingle
+
+ // then
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun `isFirst와 isLast가 다르면 단일 페이지가 아님을 알려준다`() {
+ // given
+ val middlePage = Page(current = 2, isFirst = false, isLast = false)
+
+ // when
+ val result = middlePage.isSingle
+
+ // then
+ assertThat(result).isFalse()
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/PercentageDiscountCouponTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/PercentageDiscountCouponTest.kt
new file mode 100644
index 0000000000..22a94d1d7b
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/domain/model/PercentageDiscountCouponTest.kt
@@ -0,0 +1,87 @@
+package woowacourse.shopping.domain.model
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.jupiter.api.Test
+import woowacourse.shopping.model.DUMMY_COUPON_4
+import woowacourse.shopping.model.DUMMY_LOCAL_DATE_TIME_1
+import woowacourse.shopping.model.DUMMY_LOCAL_DATE_TIME_2
+import woowacourse.shopping.model.DUMMY_PRODUCTS_3
+import woowacourse.shopping.model.DUMMY_PRODUCTS_4
+
+class PercentageDiscountCouponTest {
+ @Test
+ fun `조건을 만족하면 퍼센트 할인 쿠폰이 적용된다`() {
+ // given
+ val coupon = DUMMY_COUPON_4
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.original).isEqualTo(1119200)
+ assertThat(price.discount).isEqualTo((1119200 * 0.3).toInt())
+ assertThat(price.shipping).isEqualTo(3000)
+ assertThat(price.result).isEqualTo(1119200 - (1119200 * 0.3).toInt() + 3000)
+ }
+
+ @Test
+ fun `조건을 만족하지 않으면 할인은 적용되지 않는다`() {
+ // given
+ val coupon = DUMMY_COUPON_4
+ val products = DUMMY_PRODUCTS_4
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_2)
+
+ // then
+ assertThat(price.original).isEqualTo(11900)
+ assertThat(price.discount).isEqualTo(0)
+ assertThat(price.result).isEqualTo(14900)
+ }
+
+ @Test
+ fun `할인 시간이 아닐 경우 쿠폰을 사용할 수 없다`() {
+ // given
+ val products = DUMMY_PRODUCTS_3
+ val invalidTime = DUMMY_LOCAL_DATE_TIME_2
+
+ // when
+ val result = DUMMY_COUPON_4.getIsAvailable(products, invalidTime)
+
+ // then
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun `유효기간이 지나면 쿠폰을 사용할 수 없다`() {
+ // given
+ val expiredCoupon =
+ DUMMY_COUPON_4.copy(
+ detail =
+ DUMMY_COUPON_4.detail.copy(
+ expirationDate = DUMMY_LOCAL_DATE_TIME_1.toLocalDate().minusDays(1),
+ ),
+ )
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val result = expiredCoupon.getIsAvailable(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun `할인 시간과 유효기간이 유효하면 쿠폰을 사용할 수 있다`() {
+ // given
+ val coupon = DUMMY_COUPON_4
+ val products = DUMMY_PRODUCTS_3
+
+ // when
+ val result = coupon.getIsAvailable(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(result).isTrue()
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/PriceTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/PriceTest.kt
new file mode 100644
index 0000000000..12c3980ad1
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/domain/model/PriceTest.kt
@@ -0,0 +1,23 @@
+package woowacourse.shopping.domain.model
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+
+class PriceTest {
+ @Test
+ fun `할인 가격과 배송비에 따라 최종 금액을 반환한다`() {
+ // given
+ val price =
+ Price(
+ original = 10_000,
+ discount = 2_000,
+ shipping = 3_000,
+ )
+
+ // when
+ val result = price.result
+
+ // then
+ assertThat(result).isEqualTo(11_000)
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/ProductDetailTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/ProductDetailTest.kt
deleted file mode 100644
index 1ba9d15cc0..0000000000
--- a/app/src/test/java/woowacourse/shopping/domain/model/ProductDetailTest.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package woowacourse.shopping.domain.model
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.jupiter.api.Test
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCT_1
-
-class ProductDetailTest {
- @Test
- fun `수량을 증가시키면 해당 수량만큼 증가된 객체를 반환한다`() {
- // given
- val original = DUMMY_CATALOG_PRODUCT_1
-
- // when
- val increased = original.increaseQuantity()
-
- // then
- assertThat(increased.quantity).isEqualTo(6)
- assertThat(increased.productDetail).isEqualTo(original.productDetail)
- }
-
- @Test
- fun `수량을 감소시키면 해당 수량만큼 감소된 객체를 반환한다`() {
- // given
- val original = DUMMY_CATALOG_PRODUCT_1
-
- // when
- val decreased = original.decreaseQuantity()
-
- // then
- assertThat(decreased.quantity).isEqualTo(4)
- assertThat(decreased.productDetail).isEqualTo(original.productDetail)
- }
-
- @Test
- fun `수량을 감소시킬 때 0 미만으로 떨어지지 않도록 한다`() {
- // given
- val original = DUMMY_CATALOG_PRODUCT_1
-
- // when
- val decreased = original.decreaseQuantity(10)
-
- // then
- assertThat(decreased.quantity).isEqualTo(0)
- }
-
- @Test
- fun `총 가격은 단가와 수량의 곱으로 계산된다`() {
- // given
- val catalogProduct = DUMMY_CATALOG_PRODUCT_1
-
- // when
- val total = catalogProduct.totalPrice
-
- // then
- assertThat(total).isEqualTo(catalogProduct.productDetail.price * catalogProduct.quantity)
- }
-}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/ProductTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/ProductTest.kt
new file mode 100644
index 0000000000..a8d095f7ff
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/domain/model/ProductTest.kt
@@ -0,0 +1,55 @@
+package woowacourse.shopping.domain.model
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Test
+import woowacourse.shopping.model.DUMMY_PRODUCT_1
+
+class ProductTest {
+ @Test
+ fun `금액에 따른 총 가격을 반환한다`() {
+ // given
+ val product = DUMMY_PRODUCT_1
+
+ // when
+ val price = product.totalPrice
+
+ // then
+ assertThat(price).isEqualTo(11900 * 5)
+ }
+
+ @Test
+ fun `수량을 증가시키면 수량이 증가된 객체를 반환한다`() {
+ // given
+ val product = DUMMY_PRODUCT_1.copy(quantity = 3)
+
+ // when
+ val updated = product.increaseQuantity()
+
+ // then
+ assertThat(updated.quantity).isEqualTo(4)
+ }
+
+ @Test
+ fun `수량을 감소시키면 수량이 감소된 객체를 반환한다`() {
+ // given
+ val product = DUMMY_PRODUCT_1.copy(quantity = 3)
+
+ // when
+ val updated = product.decreaseQuantity()
+
+ // then
+ assertThat(updated.quantity).isEqualTo(2)
+ }
+
+ @Test
+ fun `수량은 0개 이하로 감소되지 않는다`() {
+ // given
+ val product = DUMMY_PRODUCT_1.copy(quantity = 0)
+
+ // when
+ val updated = product.decreaseQuantity()
+
+ // then
+ assertThat(updated.quantity).isEqualTo(0)
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/ProductsTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/ProductsTest.kt
index 2ac5a56cbf..aef6c2efce 100644
--- a/app/src/test/java/woowacourse/shopping/domain/model/ProductsTest.kt
+++ b/app/src/test/java/woowacourse/shopping/domain/model/ProductsTest.kt
@@ -1,112 +1,183 @@
package woowacourse.shopping.domain.model
-import com.google.common.truth.Truth.assertThat
+import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCTS_1
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCT_1
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCT_2
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCT_3
+import woowacourse.shopping.model.DUMMY_PRODUCTS_1
+import woowacourse.shopping.model.DUMMY_PRODUCTS_2
+import woowacourse.shopping.model.DUMMY_PRODUCT_1
+import woowacourse.shopping.model.DUMMY_PRODUCT_2
+import woowacourse.shopping.model.DUMMY_PRODUCT_3
class ProductsTest {
@Test
- fun `두 상품 목록을 병합하면 기존 순서를 유지하며 결합되고 hasMore는 우측 값을 따른다`() {
+ fun `모든 상품이 선택되어 있으면 모든 상품이 선택되어 있음을 반환한다`() {
// given
- val left = Products(listOf(DUMMY_CATALOG_PRODUCT_1), hasMore = true)
- val right = Products(listOf(DUMMY_CATALOG_PRODUCT_2), hasMore = false)
+ val selectedProducts =
+ DUMMY_PRODUCTS_1.copy(
+ products = DUMMY_PRODUCTS_1.products.map { it.copy(isSelected = true) },
+ )
// when
- val result = left + right
+ val result = selectedProducts.isAllSelected
// then
- assertThat(result.products)
- .containsExactly(
- DUMMY_CATALOG_PRODUCT_1,
- DUMMY_CATALOG_PRODUCT_2,
- ).inOrder()
- assertThat(result.hasMore).isFalse()
+ assertThat(result).isTrue()
}
@Test
- fun `상품 수량을 변경하면 해당 상품만 반영되고 나머지는 유지된다`() {
+ fun `선택된 상품들의 총 수량을 반환한다`() {
// given
- val original = DUMMY_CATALOG_PRODUCTS_1
- val newQuantity = 100
+ val selected =
+ DUMMY_PRODUCTS_1.copy(
+ products = DUMMY_PRODUCTS_1.products.map { it.copy(isSelected = true) },
+ )
// when
- val updated =
- original.updateProductQuantity(
- DUMMY_CATALOG_PRODUCT_2.productDetail.id,
- newQuantity,
+ val quantity = selected.selectedProductsQuantity
+
+ // then
+ assertThat(quantity).isEqualTo(5 + 6 + 7)
+ }
+
+ @Test
+ fun `선택된 상품들의 총 가격을 반환한다`() {
+ // given
+ val selected =
+ DUMMY_PRODUCTS_1.copy(
+ products = DUMMY_PRODUCTS_1.products.map { it.copy(isSelected = true) },
)
+ // when
+ val price = selected.selectedProductsPrice
+
+ // then
+ val expected =
+ DUMMY_PRODUCT_1.totalPrice +
+ DUMMY_PRODUCT_2.totalPrice +
+ DUMMY_PRODUCT_3.totalPrice
+
+ assertThat(price).isEqualTo(expected)
+ }
+
+ @Test
+ fun `상품 선택 상태를 반전한다`() {
+ // given
+ val products = DUMMY_PRODUCTS_1
+ val target = products.products.first()
+
+ // when
+ val updated = products.updateSelection(target)
+
// then
- val modified =
- updated.products.first { it.productDetail.id == DUMMY_CATALOG_PRODUCT_2.productDetail.id }
- assertThat(modified.quantity).isEqualTo(newQuantity)
+ assertThat(updated.getProductByProductId(target.productDetail.id)?.isSelected).isEqualTo(!target.isSelected)
+ }
- val unmodified =
- updated.products.first { it.productDetail.id == DUMMY_CATALOG_PRODUCT_1.productDetail.id }
- assertThat(unmodified.quantity).isEqualTo(DUMMY_CATALOG_PRODUCT_1.quantity)
+ @Test
+ fun `특정 상품의 수량을 업데이트한다`() {
+ // given
+ val products = DUMMY_PRODUCTS_1
+ val target = products.products.first()
+
+ // when
+ val updated = products.updateQuantity(target, 10)
+
+ // then
+ assertThat(updated.getProductByProductId(target.productDetail.id)?.quantity).isEqualTo(10)
}
@Test
- fun `단일 상품 정보를 갱신하면 해당 상품만 변경된다`() {
+ fun `전체 선택 상태를 반전한다`() {
// given
- val original = DUMMY_CATALOG_PRODUCTS_1
- val updatedProduct = DUMMY_CATALOG_PRODUCT_2.copy(quantity = 100)
+ val products = DUMMY_PRODUCTS_1
// when
- val result = original.updateProduct(updatedProduct)
+ val toggled = products.toggleAllSelection()
// then
- val modified =
- result.products.first { it.productDetail.id == updatedProduct.productDetail.id }
- assertThat(modified.quantity).isEqualTo(100)
+ assertThat(toggled.isAllSelected).isTrue()
+ }
+
+ @Test
+ fun `상품의 ID로 상품을 조회할 수 있다`() {
+ // given
+ val products = DUMMY_PRODUCTS_1
+ val id = DUMMY_PRODUCT_3.productDetail.id
- val unmodified =
- result.products.first { it.productDetail.id == DUMMY_CATALOG_PRODUCT_1.productDetail.id }
- assertThat(unmodified.quantity).isEqualTo(DUMMY_CATALOG_PRODUCT_1.quantity)
+ // when
+ val found = products.getProductByProductId(id)
+
+ // then
+ assertThat(found?.productDetail?.id).isEqualTo(id)
}
@Test
- fun `복수 상품 정보를 갱신하면 매칭되는 항목만 반영된다`() {
+ fun `장바구니 ID로 상품을 조회할 수 있다`() {
// given
- val original = DUMMY_CATALOG_PRODUCTS_1
+ val products = DUMMY_PRODUCTS_1
+ val cartId = DUMMY_PRODUCT_2.cartId
- val updatedList =
- listOf(
- DUMMY_CATALOG_PRODUCT_1.copy(quantity = 1),
- DUMMY_CATALOG_PRODUCT_3.copy(quantity = 2),
+ // when
+ val found = products.getProductByCartId(cartId!!)
+
+ // then
+ assertThat(found?.cartId).isEqualTo(cartId)
+ }
+
+ @Test
+ fun `선택된 상품들의 상품 ID 목록을 반환한다`() {
+ // given
+ val selected =
+ DUMMY_PRODUCTS_1.copy(
+ products =
+ DUMMY_PRODUCTS_1.products.mapIndexed { index, product ->
+ if (index < 3) product.copy(isSelected = true) else product
+ },
)
// when
- val result = original.updateProducts(updatedList)
+ val ids = selected.getSelectedProductIds()
// then
- assertThat(result.products.first { it.productDetail.id == DUMMY_CATALOG_PRODUCT_1.productDetail.id }.quantity).isEqualTo(
- 1,
- )
- assertThat(result.products.first { it.productDetail.id == DUMMY_CATALOG_PRODUCT_2.productDetail.id }.quantity)
- .isEqualTo(DUMMY_CATALOG_PRODUCT_2.quantity)
- assertThat(result.products.first { it.productDetail.id == DUMMY_CATALOG_PRODUCT_3.productDetail.id }.quantity).isEqualTo(
- 2,
+ assertThat(ids).containsExactly(
+ DUMMY_PRODUCT_1.productDetail.id,
+ DUMMY_PRODUCT_2.productDetail.id,
+ DUMMY_PRODUCT_3.productDetail.id,
)
}
@Test
- fun `총 수량은 모든 상품 수량의 합과 같다`() {
+ fun `선택된 상품들의 장바구니 ID 목록을 반환한다`() {
// given
- val products =
- Products(
- listOf(
- DUMMY_CATALOG_PRODUCT_1.copy(quantity = 1),
- DUMMY_CATALOG_PRODUCT_2.copy(quantity = 2),
- DUMMY_CATALOG_PRODUCT_3.copy(quantity = 3),
- ),
- hasMore = false,
+ val selected =
+ DUMMY_PRODUCTS_1.copy(
+ products =
+ DUMMY_PRODUCTS_1.products.mapIndexed { index, product ->
+ if (index < 3) product.copy(isSelected = true) else product
+ },
)
+ // when
+ val cartIds = selected.getSelectedCartIds()
+
+ // then
+ assertThat(cartIds).containsExactly(
+ DUMMY_PRODUCT_1.cartId,
+ DUMMY_PRODUCT_2.cartId,
+ DUMMY_PRODUCT_3.cartId,
+ )
+ }
+
+ @Test
+ fun `두 Products를 병합하면 상품 목록이 추가된다`() {
+ // given
+ val left = DUMMY_PRODUCTS_1
+ val right = DUMMY_PRODUCTS_2.copy(page = Page(3, isFirst = false, isLast = true))
+
+ // when
+ val merged = left + right
+
// then
- assertThat(products.catalogProductsQuantity).isEqualTo(6)
+ assertThat(merged.products).hasSize(left.products.size + right.products.size)
+ assertThat(merged.page).isEqualTo(right.page)
}
}
diff --git a/app/src/test/java/woowacourse/shopping/domain/model/QuantityBonusCouponTest.kt b/app/src/test/java/woowacourse/shopping/domain/model/QuantityBonusCouponTest.kt
new file mode 100644
index 0000000000..3bf8c7497a
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/domain/model/QuantityBonusCouponTest.kt
@@ -0,0 +1,92 @@
+package woowacourse.shopping.domain.model
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.jupiter.api.Test
+import woowacourse.shopping.model.DUMMY_COUPON_2
+import woowacourse.shopping.model.DUMMY_LOCAL_DATE_TIME_1
+import woowacourse.shopping.model.DUMMY_PRODUCT_DETAIL_2
+import woowacourse.shopping.model.DUMMY_PRODUCT_DETAIL_3
+import java.time.LocalDate
+
+class QuantityBonusCouponTest {
+ @Test
+ fun `조건을 만족하면 가장 비싼 상품이 할인된다`() {
+ // given
+ val coupon = DUMMY_COUPON_2
+ val product1 = Product(DUMMY_PRODUCT_DETAIL_2.copy(price = 5000), cartId = 1, quantity = 3, isSelected = true)
+ val product2 = Product(DUMMY_PRODUCT_DETAIL_3.copy(price = 2500), cartId = 1, quantity = 3, isSelected = true)
+ val products = Products(listOf(product1, product2))
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.original).isEqualTo(22500)
+ assertThat(price.discount).isEqualTo(5000)
+ assertThat(price.shipping).isEqualTo(3000)
+ assertThat(price.result).isEqualTo(20500)
+ }
+
+ @Test
+ fun `수량이 6개이면 2세트가 적용되어 2개 할인된다`() {
+ // given
+ val coupon = DUMMY_COUPON_2
+ val product = Product(DUMMY_PRODUCT_DETAIL_2.copy(price = 5000), cartId = 1, quantity = 6, isSelected = true)
+ val products = Products(listOf(product))
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.original).isEqualTo(30000)
+ assertThat(price.discount).isEqualTo(5000 * 2)
+ assertThat(price.result).isEqualTo(23000)
+ }
+
+ @Test
+ fun `조건을 만족하지 않으면 쿠폰은 적용되지 않는다`() {
+ // given
+ val coupon = DUMMY_COUPON_2
+ val product = Product(DUMMY_PRODUCT_DETAIL_2.copy(price = 5000), cartId = 1, quantity = 2, isSelected = true)
+ val products = Products(listOf(product))
+
+ // when
+ val price = coupon.apply(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(price.original).isEqualTo(10000)
+ assertThat(price.discount).isEqualTo(0)
+ assertThat(price.result).isEqualTo(13000)
+ }
+
+ @Test
+ fun `유효기간이 지나면 쿠폰은 사용할 수 없다`() {
+ // given
+ val expiredCoupon =
+ DUMMY_COUPON_2.copy(
+ detail = DUMMY_COUPON_2.detail.copy(expirationDate = LocalDate.of(2020, 1, 1)),
+ )
+ val product = Product(DUMMY_PRODUCT_DETAIL_2, cartId = 1, quantity = 3, isSelected = true)
+ val products = Products(listOf(product))
+
+ // when
+ val result = expiredCoupon.getIsAvailable(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun `수량 조건과 유효기간을 만족하면 쿠폰을 사용할 수 있다`() {
+ // given
+ val coupon = DUMMY_COUPON_2
+ val product = Product(DUMMY_PRODUCT_DETAIL_2, cartId = 1, quantity = 3, isSelected = true)
+ val products = Products(listOf(product))
+
+ // when
+ val result = coupon.getIsAvailable(products, DUMMY_LOCAL_DATE_TIME_1)
+
+ // then
+ assertThat(result).isTrue()
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/model/Fixture.kt b/app/src/test/java/woowacourse/shopping/model/Fixture.kt
index c1d0b6afe0..f519e0bb33 100644
--- a/app/src/test/java/woowacourse/shopping/model/Fixture.kt
+++ b/app/src/test/java/woowacourse/shopping/model/Fixture.kt
@@ -1,10 +1,24 @@
package woowacourse.shopping.model
+import woowacourse.shopping.domain.model.Coupon
+import woowacourse.shopping.domain.model.CouponDetail
+import woowacourse.shopping.domain.model.CouponDiscountType.BUY_X_GET_Y
+import woowacourse.shopping.domain.model.CouponDiscountType.FIXED
+import woowacourse.shopping.domain.model.CouponDiscountType.FREE_SHIPPING
+import woowacourse.shopping.domain.model.CouponDiscountType.PERCENTAGE
+import woowacourse.shopping.domain.model.Coupons
+import woowacourse.shopping.domain.model.FixedDiscountCoupon
+import woowacourse.shopping.domain.model.FreeShippingCoupon
import woowacourse.shopping.domain.model.HistoryProduct
import woowacourse.shopping.domain.model.Page
+import woowacourse.shopping.domain.model.PercentageDiscountCoupon
import woowacourse.shopping.domain.model.Product
import woowacourse.shopping.domain.model.ProductDetail
import woowacourse.shopping.domain.model.Products
+import woowacourse.shopping.domain.model.QuantityBonusCoupon
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
val DUMMY_PRODUCT_DETAIL_1 =
ProductDetail(
@@ -38,24 +52,15 @@ val DUMMY_PRODUCT_DETAIL_4 =
id = 4,
name = "치밥하기 좋은 순살 바베큐치킨",
imageUrl = "https://product-image.kurly.com/hdims/resize/%5E%3E360x%3E468/cropcenter/360x468/quality/85/src/product/image/f864b361-85da-4482-aec8-909397caac4e.jpg",
- price = 13990,
+ price = 139900,
category = "공백제이",
)
-val DUMMY_PRODUCT_DETAIL_5 =
- ProductDetail(
- id = 5,
- name = "[이연복의 목란] 짜장면 2인분",
- imageUrl = "https://product-image.kurly.com/hdims/resize/%5E%3E360x%3E468/cropcenter/360x468/quality/85/src/product/image/90256eb2-b02f-493a-ab7a-29a8724254e4.jpeg",
- price = 9980,
- category = "공백제이",
- )
-
-val DUMMY_CART_PRODUCT_1 = Product(DUMMY_PRODUCT_DETAIL_1, 1)
-val DUMMY_CART_PRODUCT_2 = Product(DUMMY_PRODUCT_DETAIL_2, 1)
-val DUMMY_CART_PRODUCT_3 = Product(DUMMY_PRODUCT_DETAIL_3, 1)
-val DUMMY_CART_PRODUCT_4 = Product(DUMMY_PRODUCT_DETAIL_4, 1)
-val DUMMY_CART_PRODUCT_5 = Product(DUMMY_PRODUCT_DETAIL_5, 1)
+val DUMMY_PRODUCT_1 = Product(DUMMY_PRODUCT_DETAIL_1, 1, 5)
+val DUMMY_PRODUCT_2 = Product(DUMMY_PRODUCT_DETAIL_2, 2, 6)
+val DUMMY_PRODUCT_3 = Product(DUMMY_PRODUCT_DETAIL_3, 3, 7)
+val DUMMY_PRODUCT_4 = Product(DUMMY_PRODUCT_DETAIL_4, 4, 8, true)
+val DUMMY_PRODUCT_5 = Product(DUMMY_PRODUCT_DETAIL_1, 1, 1, true)
val DUMMY_HISTORY_PRODUCT_1 =
HistoryProduct(
@@ -65,33 +70,110 @@ val DUMMY_HISTORY_PRODUCT_1 =
category = "공백제이",
)
-val DUMMY_CATALOG_PRODUCT_1 = Product(DUMMY_PRODUCT_DETAIL_1, quantity = 5)
-val DUMMY_CATALOG_PRODUCT_2 = Product(DUMMY_PRODUCT_DETAIL_2, quantity = 6)
-val DUMMY_CATALOG_PRODUCT_3 = Product(DUMMY_PRODUCT_DETAIL_3, quantity = 7)
-
-val DUMMY_CART_PRODUCTS_1 =
+val DUMMY_PRODUCTS_1 =
Products(
products =
listOf(
- DUMMY_CART_PRODUCT_1,
- DUMMY_CART_PRODUCT_2,
- DUMMY_CART_PRODUCT_3,
- DUMMY_CART_PRODUCT_4,
- DUMMY_CART_PRODUCT_5,
+ DUMMY_PRODUCT_1,
+ DUMMY_PRODUCT_2,
+ DUMMY_PRODUCT_3,
),
page = Page(2, isFirst = false, isLast = false),
)
-val DUMMY_CATALOG_PRODUCTS_1 =
+val DUMMY_PRODUCTS_2 =
Products(
- products =
- listOf(
- DUMMY_CATALOG_PRODUCT_1,
- DUMMY_CATALOG_PRODUCT_2,
- DUMMY_CATALOG_PRODUCT_3,
- ),
+ products = listOf(DUMMY_PRODUCT_1),
)
-val DUMMY_CATALOG_PRODUCTS_2 =
+
+val DUMMY_PRODUCTS_3 =
Products(
- products = listOf(DUMMY_CATALOG_PRODUCT_1),
+ products = listOf(DUMMY_PRODUCT_4),
)
+
+val DUMMY_PRODUCTS_4 =
+ Products(
+ products = listOf(DUMMY_PRODUCT_5),
+ )
+
+val DUMMY_COUPON_1: Coupon =
+ FixedDiscountCoupon(
+ detail =
+ CouponDetail(
+ id = 1,
+ code = "FIXED5000",
+ name = "5,000원 할인 쿠폰",
+ expirationDate = LocalDate.of(2025, 11, 30),
+ discount = 5000,
+ minimumPurchase = 100000,
+ discountType = FIXED,
+ buyQuantity = null,
+ getQuantity = null,
+ availableTime = null,
+ ),
+ isSelected = false,
+ )
+
+val DUMMY_COUPON_2: Coupon =
+ QuantityBonusCoupon(
+ detail =
+ CouponDetail(
+ id = 2,
+ code = "BOGO",
+ name = "2개 구매 시 1개 무료 쿠폰",
+ expirationDate = LocalDate.of(2025, 6, 30),
+ discount = null,
+ minimumPurchase = null,
+ discountType = BUY_X_GET_Y,
+ buyQuantity = 2,
+ getQuantity = 1,
+ availableTime = null,
+ ),
+ isSelected = false,
+ )
+
+val DUMMY_COUPON_3: Coupon =
+ FreeShippingCoupon(
+ detail =
+ CouponDetail(
+ id = 3,
+ code = "FREESHIPPING",
+ name = "5만원 이상 구매 시 무료 배송 쿠폰",
+ expirationDate = LocalDate.of(2025, 8, 31),
+ discount = null,
+ minimumPurchase = 50000,
+ discountType = FREE_SHIPPING,
+ buyQuantity = null,
+ getQuantity = null,
+ availableTime = null,
+ ),
+ isSelected = false,
+ )
+
+val DUMMY_COUPON_4: Coupon =
+ PercentageDiscountCoupon(
+ detail =
+ CouponDetail(
+ id = 4,
+ code = "MIRACLESALE",
+ name = "미라클모닝 30% 할인 쿠폰",
+ expirationDate = LocalDate.of(2025, 7, 31),
+ discount = 30,
+ minimumPurchase = null,
+ discountType = PERCENTAGE,
+ buyQuantity = null,
+ getQuantity = null,
+ availableTime =
+ CouponDetail.AvailableTime(
+ start = LocalTime.of(4, 0, 0),
+ end = LocalTime.of(7, 0, 0),
+ ),
+ ),
+ isSelected = false,
+ )
+
+val DUMMY_COUPONS_1: Coupons = Coupons(listOf(DUMMY_COUPON_1, DUMMY_COUPON_2, DUMMY_COUPON_3, DUMMY_COUPON_4))
+
+val DUMMY_LOCAL_DATE_TIME_1: LocalDateTime = LocalDateTime.of(2025, 6, 15, 6, 30)
+
+val DUMMY_LOCAL_DATE_TIME_2: LocalDateTime = LocalDateTime.of(2025, 6, 15, 18, 30)
diff --git a/app/src/test/java/woowacourse/shopping/ui/cart/CartViewModelTest.kt b/app/src/test/java/woowacourse/shopping/ui/cart/CartViewModelTest.kt
index a784f2abae..8ecef8e9a4 100644
--- a/app/src/test/java/woowacourse/shopping/ui/cart/CartViewModelTest.kt
+++ b/app/src/test/java/woowacourse/shopping/ui/cart/CartViewModelTest.kt
@@ -1,47 +1,53 @@
package woowacourse.shopping.ui.cart
import com.google.common.truth.Truth.assertThat
-import io.mockk.Runs
-import io.mockk.clearMocks
-import io.mockk.every
-import io.mockk.just
+import io.mockk.coEvery
import io.mockk.mockk
import io.mockk.unmockkAll
-import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
-import woowacourse.shopping.domain.model.CartProducts
import woowacourse.shopping.domain.usecase.DecreaseCartProductQuantityUseCase
import woowacourse.shopping.domain.usecase.GetCartProductsUseCase
+import woowacourse.shopping.domain.usecase.GetCartRecommendProductsUseCase
+import woowacourse.shopping.domain.usecase.GetCatalogProductUseCase
import woowacourse.shopping.domain.usecase.IncreaseCartProductQuantityUseCase
import woowacourse.shopping.domain.usecase.RemoveCartProductUseCase
-import woowacourse.shopping.model.DUMMY_CART_PRODUCTS_1
-import woowacourse.shopping.model.DUMMY_PRODUCT_Detail_1
+import woowacourse.shopping.model.DUMMY_PRODUCTS_1
+import woowacourse.shopping.model.DUMMY_PRODUCT_1
+import woowacourse.shopping.ui.model.CartProductUiModel
+import woowacourse.shopping.util.CoroutinesTestExtension
import woowacourse.shopping.util.InstantTaskExecutorExtension
import woowacourse.shopping.util.getOrAwaitValue
+import woowacourse.shopping.util.setUpTestLiveData
+@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(InstantTaskExecutorExtension::class)
+@ExtendWith(CoroutinesTestExtension::class)
class CartViewModelTest {
private lateinit var getCartProductsUseCase: GetCartProductsUseCase
private lateinit var removeCartProductUseCase: RemoveCartProductUseCase
private lateinit var increaseCartProductQuantityUseCase: IncreaseCartProductQuantityUseCase
private lateinit var decreaseCartProductQuantityUseCase: DecreaseCartProductQuantityUseCase
+ private lateinit var getCartRecommendProductsUseCase: GetCartRecommendProductsUseCase
+ private lateinit var getCatalogProductUseCase: GetCatalogProductUseCase
+
private lateinit var viewModel: CartViewModel
@BeforeEach
- fun setup() {
+ fun setUp() {
getCartProductsUseCase = mockk()
removeCartProductUseCase = mockk()
increaseCartProductQuantityUseCase = mockk()
decreaseCartProductQuantityUseCase = mockk()
+ getCartRecommendProductsUseCase = mockk()
+ getCatalogProductUseCase = mockk()
- every {
- getCartProductsUseCase.invoke(any(), any(), any())
- } answers {
- thirdArg<(CartProducts) -> Unit>().invoke(DUMMY_CART_PRODUCTS_1)
- }
+ coEvery { getCartProductsUseCase(any(), any()) } returns DUMMY_PRODUCTS_1
viewModel =
CartViewModel(
@@ -49,108 +55,141 @@ class CartViewModelTest {
removeCartProductUseCase,
increaseCartProductQuantityUseCase,
decreaseCartProductQuantityUseCase,
+ getCartRecommendProductsUseCase,
+ getCatalogProductUseCase,
)
}
@Test
- fun `장바구니 상품 목록을 불러온다`() {
- // given & when & then
- assertThat(viewModel.cartProducts.getOrAwaitValue().products).hasSize(5)
- assertThat(viewModel.cartProducts.getOrAwaitValue().products).containsExactlyElementsIn(DUMMY_CART_PRODUCTS_1.products)
+ fun `초기 로딩 시 장바구니 상품 목록을 불러온다`() {
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.cartProducts.products).containsExactlyElementsIn(DUMMY_PRODUCTS_1.products)
}
@Test
- fun `장바구니 상품 수량을 증가시키고 수정된 상품 목록에 추가한다`() {
- // given
- val productId = DUMMY_PRODUCT_Detail_1.id
- val newQuantity = 10
-
- every {
- increaseCartProductQuantityUseCase.invoke(eq(productId), any(), any())
- } answers {
- thirdArg<(Int) -> Unit>().invoke(newQuantity)
+ fun `장바구니 상품을 제거하면 UI 상태에서 제거되고 상품 목록을 다시 불러온다`() =
+ runTest {
+ coEvery { removeCartProductUseCase(DUMMY_PRODUCT_1.cartId!!) } returns Unit
+ coEvery { getCartProductsUseCase(any(), any()) } returns DUMMY_PRODUCTS_1
+
+ setUpTestLiveData(CartProductUiModel(cartProducts = DUMMY_PRODUCTS_1), "_uiModel", viewModel)
+
+ viewModel.removeCartProduct(DUMMY_PRODUCT_1.cartId!!, DUMMY_PRODUCT_1.productDetail.id)
+ advanceUntilIdle()
+
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.editedProductIds).contains(DUMMY_PRODUCT_1.productDetail.id)
}
- // when
- viewModel.increaseCartProductQuantity(productId)
+ @Test
+ fun `장바구니 상품 수량을 증가시키면 변경된 수량이 UI에 반영된다`() =
+ runTest {
+ coEvery { increaseCartProductQuantityUseCase(any()) } returns 10
- // then
- val updatedProduct =
- viewModel.cartProducts
- .getOrAwaitValue()
- .products
- .find { it.productDetail.id == productId }
- assertThat(updatedProduct?.quantity).isEqualTo(newQuantity)
- assertThat(viewModel.editedProductIds.getOrAwaitValue()).contains(productId)
- }
+ setUpTestLiveData(CartProductUiModel(cartProducts = DUMMY_PRODUCTS_1), "_uiModel", viewModel)
+
+ viewModel.increaseCartProductQuantity(DUMMY_PRODUCT_1.productDetail.id)
+ advanceUntilIdle()
+
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.cartProducts.getProductByProductId(DUMMY_PRODUCT_1.productDetail.id)?.quantity).isEqualTo(10)
+ }
@Test
- fun `장바구니 상품 수량을 감소시키고 장바구니 상품 목록을 불러온 뒤 수정된 상품 목록에 추가한다`() {
- // given
- val productId = DUMMY_PRODUCT_Detail_1.id
-
- every {
- decreaseCartProductQuantityUseCase.invoke(eq(productId), any(), any())
- } answers {
- thirdArg<(Int) -> Unit>().invoke(0)
+ fun `장바구니 상품 수량을 감소시키면 변경된 수량이 UI에 반영된다`() =
+ runTest {
+ coEvery { decreaseCartProductQuantityUseCase(any()) } returns 3
+
+ setUpTestLiveData(CartProductUiModel(cartProducts = DUMMY_PRODUCTS_1), "_uiModel", viewModel)
+
+ viewModel.decreaseCartProductQuantity(DUMMY_PRODUCT_1.productDetail.id)
+ advanceUntilIdle()
+
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.cartProducts.getProductByProductId(DUMMY_PRODUCT_1.productDetail.id)?.quantity).isEqualTo(3)
}
- // when
- viewModel.decreaseCartProductQuantity(productId)
+ @Test
+ fun `장바구니 상품의 선택 상태를 토글하면 상태가 반전된다`() {
+ setUpTestLiveData(CartProductUiModel(cartProducts = DUMMY_PRODUCTS_1), "_uiModel", viewModel)
+
+ val before =
+ viewModel.uiModel
+ .getOrAwaitValue()
+ .cartProducts
+ .getProductByCartId(DUMMY_PRODUCT_1.cartId!!)
+ ?.isSelected
+
+ viewModel.toggleCartProductSelection(DUMMY_PRODUCT_1.cartId!!)
+
+ val after =
+ viewModel.uiModel
+ .getOrAwaitValue()
+ .cartProducts
+ .getProductByCartId(DUMMY_PRODUCT_1.cartId!!)
+ ?.isSelected
- // then
- verify { getCartProductsUseCase.invoke(any(), any(), any()) }
- assertThat(viewModel.editedProductIds.getOrAwaitValue()).contains(productId)
+ assertThat(before).isNotEqualTo(after)
}
@Test
- fun `장바구니 상품을 제거하고 상품 목록을 불러온다`() {
- // given
- val productId = DUMMY_PRODUCT_Detail_1.id
-
- every { removeCartProductUseCase.invoke(productId) } just Runs
- every { getCartProductsUseCase.invoke(any(), any(), any()) } answers {
- thirdArg<(CartProducts) -> Unit>().invoke(DUMMY_CART_PRODUCTS_1)
- }
+ fun `전체 장바구니 상품의 선택 상태를 토글하면 전체 상태가 반전된다`() {
+ setUpTestLiveData(CartProductUiModel(cartProducts = DUMMY_PRODUCTS_1), "_uiModel", viewModel)
- // when
- viewModel.removeCartProduct(productId)
+ viewModel.toggleAllCartProductsSelection()
- // then
- verify { removeCartProductUseCase.invoke(productId) }
- verify { getCartProductsUseCase.invoke(any(), any(), any()) }
- assertThat(viewModel.editedProductIds.getOrAwaitValue()).contains(productId)
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.cartProducts.products.all { it.isSelected }).isTrue()
}
@Test
- fun `장바구니 상품 페이지를 증가시키고 상품 목록을 불러온다`() {
- // given
- clearMocks(getCartProductsUseCase)
-
- every {
- getCartProductsUseCase.invoke(any(), any(), any())
- } answers {
- thirdArg<(CartProducts) -> Unit>().invoke(DUMMY_CART_PRODUCTS_1)
+ fun `추천 상품 수량을 증가시키면 반영된다`() =
+ runTest {
+ val updatedQuantity = 2
+ val updated = DUMMY_PRODUCT_1.copy(quantity = updatedQuantity)
+
+ coEvery { increaseCartProductQuantityUseCase(any()) } returns updatedQuantity
+
+ setUpTestLiveData(CartProductUiModel(recommendedProducts = DUMMY_PRODUCTS_1), "_uiModel", viewModel)
+
+ viewModel.increaseRecommendedProductQuantity(DUMMY_PRODUCT_1.productDetail.id)
+ advanceUntilIdle()
+
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.recommendedProducts.getProductByProductId(updated.productDetail.id)?.quantity).isEqualTo(updatedQuantity)
}
- // when
- viewModel.increasePage()
+ @Test
+ fun `추천 상품 수량을 감소시키면 반영된다`() =
+ runTest {
+ val updatedQuantity = 1
+ val updated = DUMMY_PRODUCT_1.copy(quantity = updatedQuantity)
- // then
- verify { getCartProductsUseCase.invoke(2, 5, any()) }
- assertThat(viewModel.pageState.getOrAwaitValue().current).isEqualTo(2)
- }
+ coEvery { decreaseCartProductQuantityUseCase(any()) } returns updatedQuantity
+
+ setUpTestLiveData(CartProductUiModel(recommendedProducts = DUMMY_PRODUCTS_1), "_uiModel", viewModel)
+
+ viewModel.decreaseRecommendedProductQuantity(DUMMY_PRODUCT_1.productDetail.id)
+ advanceUntilIdle()
+
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.recommendedProducts.getProductByProductId(updated.productDetail.id)?.quantity).isEqualTo(updatedQuantity)
+ }
@Test
- fun `장바구니 최대 상품 페이지 수를 불러온다`() {
- // given
- val expected = 100
+ fun `선택된 장바구니 및 추천 상품 ID를 반환한다`() {
+ val modifiedState =
+ CartProductUiModel(
+ cartProducts = DUMMY_PRODUCTS_1.toggleAllSelection(),
+ recommendedProducts = DUMMY_PRODUCTS_1.toggleAllSelection(),
+ )
+
+ setUpTestLiveData(modifiedState, "_uiModel", viewModel)
- // when
- viewModel.updateTotalPage(expected)
+ val selectedIds = viewModel.getSelectedProductIds()
+ val expectedIds = (DUMMY_PRODUCTS_1.products + DUMMY_PRODUCTS_1.products).map { it.productDetail.id }.toSet()
- // then
- assertThat(viewModel.pageState.getOrAwaitValue().total).isEqualTo(expected)
+ assertThat(selectedIds).isEqualTo(expectedIds)
}
@AfterEach
diff --git a/app/src/test/java/woowacourse/shopping/ui/catalog/CatalogViewModelTest.kt b/app/src/test/java/woowacourse/shopping/ui/catalog/CatalogViewModelTest.kt
index 40bbf9ccd1..072996eba6 100644
--- a/app/src/test/java/woowacourse/shopping/ui/catalog/CatalogViewModelTest.kt
+++ b/app/src/test/java/woowacourse/shopping/ui/catalog/CatalogViewModelTest.kt
@@ -1,196 +1,210 @@
package woowacourse.shopping.ui.catalog
+import android.util.Log
import com.google.common.truth.Truth.assertThat
+import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
+import io.mockk.mockkStatic
import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
-import woowacourse.shopping.domain.model.HistoryProduct
-import woowacourse.shopping.domain.model.Product
-import woowacourse.shopping.domain.model.Products
import woowacourse.shopping.domain.usecase.DecreaseCartProductQuantityUseCase
+import woowacourse.shopping.domain.usecase.GetCartProductsQuantityUseCase
import woowacourse.shopping.domain.usecase.GetCatalogProductUseCase
-import woowacourse.shopping.domain.usecase.GetCatalogProductsByIdsUseCase
+import woowacourse.shopping.domain.usecase.GetCatalogProductsByProductIdsUseCase
import woowacourse.shopping.domain.usecase.GetCatalogProductsUseCase
import woowacourse.shopping.domain.usecase.GetSearchHistoryUseCase
import woowacourse.shopping.domain.usecase.IncreaseCartProductQuantityUseCase
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCTS_1
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCTS_2
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCT_1
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCT_2
import woowacourse.shopping.model.DUMMY_HISTORY_PRODUCT_1
-import woowacourse.shopping.model.DUMMY_PRODUCT_Detail_1
+import woowacourse.shopping.model.DUMMY_PRODUCTS_1
+import woowacourse.shopping.model.DUMMY_PRODUCTS_2
+import woowacourse.shopping.model.DUMMY_PRODUCTS_3
+import woowacourse.shopping.model.DUMMY_PRODUCT_1
+import woowacourse.shopping.ui.model.CatalogUiModel
+import woowacourse.shopping.util.CoroutinesTestExtension
import woowacourse.shopping.util.InstantTaskExecutorExtension
import woowacourse.shopping.util.getOrAwaitValue
import woowacourse.shopping.util.setUpTestLiveData
+@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(InstantTaskExecutorExtension::class)
+@ExtendWith(CoroutinesTestExtension::class)
class CatalogViewModelTest {
private lateinit var getCatalogProductsUseCase: GetCatalogProductsUseCase
private lateinit var getCatalogProductUseCase: GetCatalogProductUseCase
- private lateinit var getCatalogProductsByIdsUseCase: GetCatalogProductsByIdsUseCase
+ private lateinit var getCatalogProductsByProductIdsUseCase: GetCatalogProductsByProductIdsUseCase
private lateinit var getSearchHistoryUseCase: GetSearchHistoryUseCase
private lateinit var increaseCartProductQuantityUseCase: IncreaseCartProductQuantityUseCase
private lateinit var decreaseCartProductQuantityUseCase: DecreaseCartProductQuantityUseCase
+ private lateinit var getCartProductsQuantityUseCase: GetCartProductsQuantityUseCase
+
private lateinit var viewModel: CatalogViewModel
@BeforeEach
fun setup() {
getCatalogProductsUseCase = mockk()
getCatalogProductUseCase = mockk()
- getCatalogProductsByIdsUseCase = mockk()
+ getCatalogProductsByProductIdsUseCase = mockk()
getSearchHistoryUseCase = mockk()
increaseCartProductQuantityUseCase = mockk()
decreaseCartProductQuantityUseCase = mockk()
+ getCartProductsQuantityUseCase = mockk()
- every {
- getCatalogProductsUseCase.invoke(any(), any(), any())
- } answers {
- thirdArg<(Products) -> Unit>().invoke(DUMMY_CATALOG_PRODUCTS_1)
- }
+ coEvery { getCatalogProductsUseCase(any(), any()) } returns DUMMY_PRODUCTS_1
viewModel =
CatalogViewModel(
getCatalogProductsUseCase,
getCatalogProductUseCase,
- getCatalogProductsByIdsUseCase,
+ getCatalogProductsByProductIdsUseCase,
getSearchHistoryUseCase,
increaseCartProductQuantityUseCase,
decreaseCartProductQuantityUseCase,
+ getCartProductsQuantityUseCase,
)
+
+ mockkStatic(Log::class)
+ every { Log.e(any(), any()) } returns 0
}
@Test
- fun `전체 상품 목록과 다른 상품 존재 여부를 불러온다`() {
- // given
- setUpTestLiveData(DUMMY_CATALOG_PRODUCTS_1, "_catalogProducts", viewModel)
+ fun `ViewModel이 초기화되면 상품 목록을 불러온다`() {
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.catalogProducts.products).containsExactlyElementsIn(DUMMY_PRODUCTS_1.products)
+ assertThat(state.isProductsLoading).isFalse()
+ }
- val newProduct = DUMMY_CATALOG_PRODUCT_2
- val newData = Products(products = listOf(newProduct), hasMore = false)
+ @Test
+ fun `상품을 더 불러오면 페이지가 증가하고 기존 목록에 추가된다`() =
+ runTest {
+ coEvery { getCatalogProductsUseCase(any(), any()) } returns DUMMY_PRODUCTS_3
+
+ setUpTestLiveData(
+ CatalogUiModel(catalogProducts = DUMMY_PRODUCTS_1),
+ "_uiModel",
+ viewModel,
+ )
+
+ viewModel.loadMoreCatalogProducts()
+ advanceUntilIdle()
- every {
- getCatalogProductsUseCase.invoke(any(), any(), any())
- } answers {
- thirdArg<(Products) -> Unit>().invoke(newData)
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.catalogProducts.products).containsExactlyElementsIn(
+ DUMMY_PRODUCTS_1.products + DUMMY_PRODUCTS_3.products,
+ )
}
- // when
- viewModel.loadCartProducts()
+ @Test
+ fun `장바구니 수량을 증가시키면 상품 수량에 반영된다`() =
+ runTest {
+ val updatedProduct = DUMMY_PRODUCT_1.copy(quantity = 10)
- // then
- val result = viewModel.products.getOrAwaitValue()
- assertThat(result.products).containsExactlyElementsIn(DUMMY_CATALOG_PRODUCTS_1.products + newProduct)
- assertThat(result.hasMore).isFalse()
- }
+ coEvery { increaseCartProductQuantityUseCase(any()) } returns 10
+ coEvery { getCatalogProductUseCase(DUMMY_PRODUCT_1.productDetail.id) } returns updatedProduct
- @Test
- fun `최근에 탐색한 상품 목록을 불러온다`() {
- // given
- val expected = listOf(DUMMY_HISTORY_PRODUCT_1)
-
- every {
- getSearchHistoryUseCase.invoke(any())
- } answers {
- firstArg<(List) -> Unit>().invoke(expected)
- }
+ setUpTestLiveData(
+ CatalogUiModel(catalogProducts = DUMMY_PRODUCTS_2),
+ "_uiModel",
+ viewModel,
+ )
- // when
- viewModel.loadHistoryProducts()
+ viewModel.increaseCartProduct(DUMMY_PRODUCT_1.productDetail.id)
+ advanceUntilIdle()
- // then
- val result = viewModel.historyProducts.getOrAwaitValue()
- assertThat(result).containsExactlyElementsIn(expected)
- }
+ val result = viewModel.uiModel.getOrAwaitValue()
+ assertThat(result.catalogProducts.getProductByProductId(updatedProduct.productDetail.id)?.quantity).isEqualTo(10)
+ }
@Test
- fun `장바구니 상품 갯수를 증가시킨다`() {
- // given
- val productId = DUMMY_CATALOG_PRODUCT_1.productDetail.id
- setUpTestLiveData(DUMMY_CATALOG_PRODUCTS_1, "_catalogProducts", viewModel)
-
- every {
- increaseCartProductQuantityUseCase.invoke(eq(productId), any(), any())
- } answers {
- thirdArg<(Int) -> Unit>().invoke(10)
- }
+ fun `장바구니 수량을 감소시키면 상품 수량에 반영된다`() =
+ runTest {
+ val updatedProduct = DUMMY_PRODUCT_1.copy(quantity = 3)
- // when
- viewModel.increaseCartProduct(productId)
+ coEvery { decreaseCartProductQuantityUseCase(any()) } returns 3
+ coEvery { getCatalogProductUseCase(DUMMY_PRODUCT_1.productDetail.id) } returns updatedProduct
- // then
- val updated = viewModel.products.getOrAwaitValue()
- assertThat(updated.products.first { it.productDetail.id == productId }.quantity).isEqualTo(
- 10,
- )
- }
+ setUpTestLiveData(
+ CatalogUiModel(catalogProducts = DUMMY_PRODUCTS_2),
+ "_uiModel",
+ viewModel,
+ )
- @Test
- fun `장바구니 상품 갯수를 감소시킨다`() {
- // given
- val productId = DUMMY_CATALOG_PRODUCT_1.productDetail.id
- setUpTestLiveData(DUMMY_CATALOG_PRODUCTS_1, "_catalogProducts", viewModel)
-
- every {
- decreaseCartProductQuantityUseCase.invoke(eq(productId), any(), any())
- } answers {
- thirdArg<(Int) -> Unit>().invoke(1)
+ viewModel.decreaseCartProduct(DUMMY_PRODUCT_1.productDetail.id)
+ advanceUntilIdle()
+
+ val result = viewModel.uiModel.getOrAwaitValue()
+ assertThat(result.catalogProducts.getProductByProductId(updatedProduct.productDetail.id)?.quantity).isEqualTo(3)
}
- // when
- viewModel.decreaseCartProduct(productId)
+ @Test
+ fun `특정 상품 정보를 불러오면 상품 목록에 반영된다`() =
+ runTest {
+ val updatedProduct = DUMMY_PRODUCT_1.copy(quantity = 1234)
- // then
- val updated = viewModel.products.getOrAwaitValue()
- assertThat(updated.products.first { it.productDetail.id == productId }.quantity).isEqualTo(1)
- }
+ coEvery { getCatalogProductUseCase(DUMMY_PRODUCT_1.productDetail.id) } returns updatedProduct
- @Test
- fun `특정 상품의 정보를 불러와 상품 목록에 반영한다`() {
- // given
- val productId = DUMMY_CATALOG_PRODUCT_1.productDetail.id
- setUpTestLiveData(DUMMY_CATALOG_PRODUCTS_1, "_catalogProducts", viewModel)
+ setUpTestLiveData(
+ CatalogUiModel(catalogProducts = DUMMY_PRODUCTS_1),
+ "_uiModel",
+ viewModel,
+ )
- val updatedProduct = DUMMY_CATALOG_PRODUCT_1.copy(quantity = 100)
+ viewModel.loadCartProduct(DUMMY_PRODUCT_1.productDetail.id)
+ advanceUntilIdle()
- every {
- getCatalogProductUseCase.invoke(eq(productId), any())
- } answers {
- secondArg<(Product?) -> Unit>().invoke(updatedProduct)
+ val result = viewModel.uiModel.getOrAwaitValue()
+ assertThat(result.catalogProducts.getProductByProductId(updatedProduct.productDetail.id)?.quantity).isEqualTo(1234)
}
- // when
- viewModel.loadCartProduct(productId)
+ @Test
+ fun `여러 상품 정보를 불러오면 상품 목록에 반영된다`() =
+ runTest {
+ val updated = listOf(DUMMY_PRODUCT_1.copy(quantity = 1234))
- // then
- val result = viewModel.products.getOrAwaitValue()
- assertThat(result.products.first { it.productDetail.id == productId }.quantity).isEqualTo(
- 100,
- )
- }
+ coEvery { getCatalogProductsByProductIdsUseCase(any()) } returns updated
+
+ setUpTestLiveData(
+ CatalogUiModel(catalogProducts = DUMMY_PRODUCTS_2),
+ "_uiModel",
+ viewModel,
+ )
+
+ viewModel.loadCartProductsByProductIds(listOf(DUMMY_PRODUCT_1.productDetail.id))
+ advanceUntilIdle()
+
+ val result = viewModel.uiModel.getOrAwaitValue()
+ assertThat(result.catalogProducts.products).containsExactlyElementsIn(updated)
+ }
@Test
- fun `특정 상품들의 정보를 불러와 상품 목록에 반영한다`() {
- // given
- setUpTestLiveData(DUMMY_CATALOG_PRODUCTS_2, "_catalogProducts", viewModel)
+ fun `최근 검색 상품 목록을 불러온다`() =
+ runTest {
+ coEvery { getSearchHistoryUseCase() } returns listOf(DUMMY_HISTORY_PRODUCT_1)
- val updatedProducts = listOf(DUMMY_CATALOG_PRODUCT_1.copy(quantity = 6))
+ viewModel.loadHistoryProducts()
+ advanceUntilIdle()
- every {
- getCatalogProductsByIdsUseCase.invoke(any(), any())
- } answers {
- secondArg<(List) -> Unit>().invoke(updatedProducts)
+ val result = viewModel.uiModel.getOrAwaitValue()
+ assertThat(result.historyProducts).containsExactly(DUMMY_HISTORY_PRODUCT_1)
}
- // when
- viewModel.loadCartProductsByIds(listOf(DUMMY_PRODUCT_Detail_1.id))
+ @Test
+ fun `장바구니 상품 수량을 불러와서 상품 목록에 반영한다`() =
+ runTest {
+ coEvery { getCartProductsQuantityUseCase() } returns 10
- // then
- val result = viewModel.products.getOrAwaitValue()
- assertThat(result.products).containsExactlyElementsIn(updatedProducts)
- }
+ viewModel.loadCartProductsQuantity()
+ advanceUntilIdle()
+
+ val result = viewModel.uiModel.getOrAwaitValue()
+ assertThat(result.cartProductsQuantity).isEqualTo(10)
+ }
@AfterEach
fun tearDown() {
diff --git a/app/src/test/java/woowacourse/shopping/ui/payment/PaymentViewModelTest.kt b/app/src/test/java/woowacourse/shopping/ui/payment/PaymentViewModelTest.kt
new file mode 100644
index 0000000000..8d93e0e26c
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/ui/payment/PaymentViewModelTest.kt
@@ -0,0 +1,122 @@
+package woowacourse.shopping.ui.payment
+
+import android.util.Log
+import com.google.common.truth.Truth.assertThat
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import woowacourse.shopping.domain.usecase.GetCatalogProductsByProductIdsUseCase
+import woowacourse.shopping.domain.usecase.GetCouponsUseCase
+import woowacourse.shopping.domain.usecase.OrderProductsUseCase
+import woowacourse.shopping.model.DUMMY_COUPONS_1
+import woowacourse.shopping.model.DUMMY_LOCAL_DATE_TIME_1
+import woowacourse.shopping.model.DUMMY_PRODUCTS_1
+import woowacourse.shopping.model.DUMMY_PRODUCTS_3
+import woowacourse.shopping.util.CoroutinesTestExtension
+import woowacourse.shopping.util.InstantTaskExecutorExtension
+import woowacourse.shopping.util.getOrAwaitValue
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(InstantTaskExecutorExtension::class)
+@ExtendWith(CoroutinesTestExtension::class)
+class PaymentViewModelTest {
+ private lateinit var getCatalogProductsByProductIdsUseCase: GetCatalogProductsByProductIdsUseCase
+ private lateinit var getCouponsUseCase: GetCouponsUseCase
+ private lateinit var orderProductsUseCase: OrderProductsUseCase
+
+ private lateinit var viewModel: PaymentViewModel
+
+ @BeforeEach
+ fun setUp() {
+ getCatalogProductsByProductIdsUseCase = mockk()
+ getCouponsUseCase = mockk()
+ orderProductsUseCase = mockk()
+
+ viewModel =
+ PaymentViewModel(
+ getCatalogProductsByProductIdsUseCase,
+ getCouponsUseCase,
+ orderProductsUseCase,
+ )
+
+ mockkStatic(Log::class)
+ every { Log.e(any(), any()) } returns 0
+ }
+
+ @Test
+ fun `상품 ID 목록을 전달하면 해당 상품들과 쿠폰을 불러온다`() =
+ runTest {
+ coEvery { getCatalogProductsByProductIdsUseCase(any()) } returns DUMMY_PRODUCTS_3.products
+ coEvery { getCouponsUseCase() } returns DUMMY_COUPONS_1
+
+ viewModel.loadProducts(DUMMY_PRODUCTS_3.products.map { it.productDetail.id })
+ advanceUntilIdle()
+
+ viewModel.loadCoupons(DUMMY_PRODUCTS_3, DUMMY_LOCAL_DATE_TIME_1)
+ advanceUntilIdle()
+
+ val state = viewModel.uiModel.getOrAwaitValue()
+
+ val expectedProducts = DUMMY_PRODUCTS_3.products.map { it.copy(isSelected = true) }
+
+ assertThat(state.products.products).containsExactlyElementsIn(expectedProducts)
+ assertThat(state.coupons.value).containsExactlyElementsIn(DUMMY_COUPONS_1.value)
+ assertThat(state.connectionErrorMessage).isNull()
+ }
+
+ @Test
+ fun `쿠폰을 선택하면 선택된 쿠폰만 참이 된다`() =
+ runTest {
+ coEvery { getCatalogProductsByProductIdsUseCase(any()) } returns DUMMY_PRODUCTS_3.products
+ coEvery { getCouponsUseCase() } returns DUMMY_COUPONS_1
+
+ viewModel.loadProducts(DUMMY_PRODUCTS_3.products.map { it.productDetail.id })
+ advanceUntilIdle()
+
+ viewModel.loadCoupons(DUMMY_PRODUCTS_3, DUMMY_LOCAL_DATE_TIME_1)
+ advanceUntilIdle()
+
+ viewModel.selectCoupon(
+ DUMMY_COUPONS_1.value
+ .first()
+ .detail.id,
+ DUMMY_LOCAL_DATE_TIME_1,
+ )
+
+ val state = viewModel.uiModel.getOrAwaitValue()
+ val selected = state.coupons.value.filter { it.isSelected }
+ assertThat(selected).hasSize(1)
+ assertThat(state.price.result).isGreaterThan(0)
+ }
+
+ @Test
+ fun `주문 성공 시 isOrderSuccess가 참이다`() =
+ runTest {
+ coEvery { getCatalogProductsByProductIdsUseCase(any()) } returns DUMMY_PRODUCTS_1.products
+ coEvery { getCouponsUseCase() } returns DUMMY_COUPONS_1
+ coEvery { orderProductsUseCase(any()) } returns Unit
+
+ viewModel.loadProducts(DUMMY_PRODUCTS_1.products.map { it.productDetail.id })
+ advanceUntilIdle()
+
+ viewModel.orderProducts()
+ advanceUntilIdle()
+
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.isOrderSuccess).isTrue()
+ }
+
+ @AfterEach
+ fun tearDown() {
+ unmockkAll()
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailDetailViewModelTest.kt b/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailDetailViewModelTest.kt
deleted file mode 100644
index 3c947fc443..0000000000
--- a/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailDetailViewModelTest.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-package woowacourse.shopping.ui.productdetail
-
-import com.google.common.truth.Truth.assertThat
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.unmockkAll
-import io.mockk.verify
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.extension.ExtendWith
-import woowacourse.shopping.domain.model.CartProduct
-import woowacourse.shopping.domain.model.HistoryProduct
-import woowacourse.shopping.domain.model.Product
-import woowacourse.shopping.domain.usecase.AddSearchHistoryUseCase
-import woowacourse.shopping.domain.usecase.GetRecentSearchHistoryUseCase
-import woowacourse.shopping.domain.usecase.UpdateCartProductUseCase
-import woowacourse.shopping.model.DUMMY_CATALOG_PRODUCT_1
-import woowacourse.shopping.model.DUMMY_HISTORY_PRODUCT_1
-import woowacourse.shopping.model.DUMMY_PRODUCT_Detail_1
-import woowacourse.shopping.util.InstantTaskExecutorExtension
-import woowacourse.shopping.util.getOrAwaitValue
-import woowacourse.shopping.util.setUpTestLiveData
-
-@ExtendWith(InstantTaskExecutorExtension::class)
-class ProductDetailDetailViewModelTest {
- private lateinit var getProductDetailUseCase: GetProductDetailUseCase
- private lateinit var getRecentSearchHistoryUseCase: GetRecentSearchHistoryUseCase
- private lateinit var addSearchHistoryUseCase: AddSearchHistoryUseCase
- private lateinit var updateCartProductUseCase: UpdateCartProductUseCase
- private lateinit var viewModel: ProductDetailViewModel
-
- @BeforeEach
- fun setup() {
- getProductDetailUseCase = mockk()
- getRecentSearchHistoryUseCase = mockk()
- addSearchHistoryUseCase = mockk(relaxed = true)
- updateCartProductUseCase = mockk(relaxed = true)
-
- viewModel =
- ProductDetailViewModel(
- getProductDetailUseCase,
- getRecentSearchHistoryUseCase,
- addSearchHistoryUseCase,
- updateCartProductUseCase,
- )
- }
-
- @Test
- fun `상품 상세 정보를 불러온다`() {
- // given
- val expected = DUMMY_CATALOG_PRODUCT_1
-
- every {
- getProductDetailUseCase.invoke(DUMMY_PRODUCT_Detail_1.id, any())
- } answers {
- secondArg<(Product?) -> Unit>().invoke(expected)
- }
-
- // when
- viewModel.loadProductDetail(DUMMY_PRODUCT_Detail_1.id)
-
- // then
- val actual = viewModel.product.getOrAwaitValue()
- assertThat(actual).isEqualTo(expected)
- }
-
- @Test
- fun `가장 최근에 탐색한 상품을 불러온다`() {
- // given
- every {
- getRecentSearchHistoryUseCase.invoke(any())
- } answers {
- firstArg<(HistoryProduct?) -> Unit>().invoke(DUMMY_HISTORY_PRODUCT_1)
- }
-
- // when
- viewModel.loadLastHistoryProduct()
-
- // then
- val actual = viewModel.lastHistoryProduct.getOrAwaitValue()
- assertThat(actual).isEqualTo(DUMMY_HISTORY_PRODUCT_1)
- }
-
- @Test
- fun `최근 탐색한 상품 목록에 현재 상품을 추가한다`() {
- // given
- val productId = DUMMY_PRODUCT_Detail_1.id
-
- // when
- viewModel.addHistoryProduct(productId)
-
- // then
- verify { addSearchHistoryUseCase.invoke(productId) }
- }
-
- @Test
- fun `상품 수량을 증가시킨다`() {
- // given
- val initial = DUMMY_CATALOG_PRODUCT_1
- setUpTestLiveData(initial, "_catalogProduct", viewModel)
-
- // when
- viewModel.increaseCartProductQuantity()
-
- // then
- val updated = viewModel.product.getOrAwaitValue()
- assertThat(updated.quantity).isEqualTo(6)
- }
-
- @Test
- fun `상품 수량을 감소시킨다`() {
- // given
- val initial = DUMMY_CATALOG_PRODUCT_1
- setUpTestLiveData(initial, "_catalogProduct", viewModel)
-
- // when
- viewModel.decreaseCartProductQuantity()
-
- // then
- val updated = viewModel.product.getOrAwaitValue()
- assertThat(updated.quantity).isEqualTo(4)
- }
-
- @Test
- fun `장바구니에 변경된 상품 수량을 반영한다`() {
- // given
- val catalogProduct = DUMMY_CATALOG_PRODUCT_1
- setUpTestLiveData(catalogProduct, "_catalogProduct", viewModel)
-
- // when
- viewModel.updateCartProduct()
-
- // then
- verify { updateCartProductUseCase.invoke(CartProduct(DUMMY_PRODUCT_Detail_1, 5)) }
- assertThat(viewModel.onCartProductAddSuccess.getValue()).isTrue()
- }
-
- @AfterEach
- fun tearDown() {
- unmockkAll()
- }
-}
diff --git a/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailViewModelTest.kt b/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailViewModelTest.kt
new file mode 100644
index 0000000000..5716f8e1df
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/ui/productdetail/ProductDetailViewModelTest.kt
@@ -0,0 +1,138 @@
+package woowacourse.shopping.ui.productdetail
+
+import com.google.common.truth.Truth.assertThat
+import io.mockk.coEvery
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import woowacourse.shopping.domain.usecase.AddSearchHistoryUseCase
+import woowacourse.shopping.domain.usecase.GetCatalogProductUseCase
+import woowacourse.shopping.domain.usecase.GetRecentSearchHistoryUseCase
+import woowacourse.shopping.domain.usecase.UpdateCartProductUseCase
+import woowacourse.shopping.model.DUMMY_HISTORY_PRODUCT_1
+import woowacourse.shopping.model.DUMMY_PRODUCT_1
+import woowacourse.shopping.ui.model.ProductDetailUiModel
+import woowacourse.shopping.util.CoroutinesTestExtension
+import woowacourse.shopping.util.InstantTaskExecutorExtension
+import woowacourse.shopping.util.getOrAwaitValue
+import woowacourse.shopping.util.setUpTestLiveData
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@ExtendWith(InstantTaskExecutorExtension::class)
+@ExtendWith(CoroutinesTestExtension::class)
+class ProductDetailViewModelTest {
+ private lateinit var getCatalogProductUseCase: GetCatalogProductUseCase
+ private lateinit var getRecentSearchHistoryUseCase: GetRecentSearchHistoryUseCase
+ private lateinit var addSearchHistoryUseCase: AddSearchHistoryUseCase
+ private lateinit var updateCartProductUseCase: UpdateCartProductUseCase
+
+ private lateinit var viewModel: ProductDetailViewModel
+
+ @BeforeEach
+ fun setup() {
+ getCatalogProductUseCase = mockk()
+ getRecentSearchHistoryUseCase = mockk()
+ addSearchHistoryUseCase = mockk(relaxed = true)
+ updateCartProductUseCase = mockk()
+
+ viewModel =
+ ProductDetailViewModel(
+ getCatalogProductUseCase,
+ getRecentSearchHistoryUseCase,
+ addSearchHistoryUseCase,
+ updateCartProductUseCase,
+ )
+ }
+
+ @Test
+ fun `상품 상세 정보를 불러온다`() =
+ runTest {
+ // given
+ val expected = DUMMY_PRODUCT_1
+ coEvery { getCatalogProductUseCase(expected.productDetail.id) } returns expected
+
+ // when
+ viewModel.loadProductDetail(expected.productDetail.id)
+ advanceUntilIdle()
+
+ // then
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.product).isEqualTo(expected)
+ assertThat(state.connectionErrorMessage).isNull()
+ }
+
+ @Test
+ fun `최근 탐색한 상품을 불러온다`() =
+ runTest {
+ // given
+ val expected = DUMMY_HISTORY_PRODUCT_1
+ coEvery { getRecentSearchHistoryUseCase() } returns expected
+
+ // when
+ viewModel.loadLastHistoryProduct()
+ advanceUntilIdle()
+
+ // then
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.lastHistoryProduct).isEqualTo(expected)
+ }
+
+ @Test
+ fun `장바구니 수량을 증가시키면 상품 수량이 1 증가한다`() {
+ // given
+ val original = DUMMY_PRODUCT_1
+ setUpTestLiveData(ProductDetailUiModel(product = original), "_uiModel", viewModel)
+
+ // when
+ viewModel.increaseCartProductQuantity()
+
+ // then
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.product.quantity).isEqualTo(original.quantity + 1)
+ }
+
+ @Test
+ fun `장바구니 수량을 감소시키면 상품 수량이 1 감소한다`() {
+ // given
+ val original = DUMMY_PRODUCT_1
+ setUpTestLiveData(ProductDetailUiModel(product = original), "_uiModel", viewModel)
+
+ // when
+ viewModel.decreaseCartProductQuantity()
+
+ // then
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.product.quantity).isEqualTo(original.quantity - 1)
+ }
+
+ @Test
+ fun `장바구니 수량 업데이트 성공 여부를 State에 반영한다`() =
+ runTest {
+ // given
+ val product = DUMMY_PRODUCT_1
+ setUpTestLiveData(ProductDetailUiModel(product = product), "_uiModel", viewModel)
+
+ coEvery {
+ updateCartProductUseCase(productId = product.productDetail.id, cartId = product.cartId, quantity = product.quantity)
+ } returns Unit
+
+ // when
+ viewModel.updateCartProduct()
+ advanceUntilIdle()
+
+ // then
+ val state = viewModel.uiModel.getOrAwaitValue()
+ assertThat(state.isCartProductUpdateSuccess).isTrue()
+ }
+
+ @AfterEach
+ fun tearDown() {
+ unmockkAll()
+ }
+}
diff --git a/app/src/test/java/woowacourse/shopping/util/CoroutinesTestExtension.kt b/app/src/test/java/woowacourse/shopping/util/CoroutinesTestExtension.kt
new file mode 100644
index 0000000000..298c34ef4d
--- /dev/null
+++ b/app/src/test/java/woowacourse/shopping/util/CoroutinesTestExtension.kt
@@ -0,0 +1,25 @@
+package woowacourse.shopping.util
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.jupiter.api.extension.AfterEachCallback
+import org.junit.jupiter.api.extension.BeforeEachCallback
+import org.junit.jupiter.api.extension.ExtensionContext
+
+@ExperimentalCoroutinesApi
+class CoroutinesTestExtension(
+ private val dispatcher: TestDispatcher = UnconfinedTestDispatcher(),
+) : BeforeEachCallback,
+ AfterEachCallback {
+ override fun beforeEach(context: ExtensionContext?) {
+ Dispatchers.setMain(dispatcher)
+ }
+
+ override fun afterEach(context: ExtensionContext?) {
+ Dispatchers.resetMain()
+ }
+}