From 89945d5011800f8946fa96a3f956d958633cfe45 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EA=B6=8C=EC=84=B1=EC=B0=AC?=
<54511614+ksc1008@users.noreply.github.com>
Date: Thu, 31 Oct 2024 23:15:34 +0900
Subject: [PATCH 1/4] =?UTF-8?q?Refactor:=20BuildConfig=20=EB=AA=A8?=
=?UTF-8?q?=EB=93=88=20=EC=83=9D=EC=84=B1,=20common.gradle=20=EB=B6=84?=
=?UTF-8?q?=EB=A6=AC=20(#77)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Fix: BuildConfig 모듈 분리
* Refactor: common.gradle 분리
* Chore: Lint 수행
---
app/build.gradle.kts | 7 +--
.../kappzzang/jeongsan/JeongsanApplication.kt | 1 +
build-config/.gitignore | 1 +
build-config/build.gradle.kts | 43 ++++++++++++++++++
build.gradle.kts | 4 ++
common/datastore/build.gradle.kts | 27 ------------
common/navigation/build.gradle.kts | 22 ----------
common/resource/build.gradle.kts | 15 -------
common/retrofit/build.gradle.kts | 28 +-----------
.../jeongsan/retrofit/RetrofitModule.kt | 2 +-
data/build.gradle.kts | 44 +------------------
.../KakaoAuthenticationDataSource.kt | 2 +-
domain/build.gradle.kts | 33 --------------
gradle/common.gradle | 24 ++++++++++
settings.gradle.kts | 1 +
ui/build.gradle.kts | 39 +---------------
16 files changed, 81 insertions(+), 212 deletions(-)
create mode 100644 build-config/.gitignore
create mode 100644 build-config/build.gradle.kts
create mode 100644 gradle/common.gradle
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e7b0f0fb..f6b20e1d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,10 +22,6 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
-
- buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY"))
- buildConfigField("String", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY"))
- resValue("string", "KAKAO_API_KEY_MANIFEST", getApiKey("KAKAO_API_KEY_MANIFEST"))
}
buildTypes {
@@ -48,7 +44,6 @@ android {
buildFeatures {
dataBinding = true
viewBinding = true
- buildConfig = true
}
}
@@ -88,4 +83,6 @@ dependencies {
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.dagger:hilt-android:2.48.1")
kapt("com.google.dagger:hilt-compiler:2.48.1")
+
+ implementation(project(":build-config"))
}
diff --git a/app/src/main/java/com/kappzzang/jeongsan/JeongsanApplication.kt b/app/src/main/java/com/kappzzang/jeongsan/JeongsanApplication.kt
index 2480cb6a..0260cbcc 100644
--- a/app/src/main/java/com/kappzzang/jeongsan/JeongsanApplication.kt
+++ b/app/src/main/java/com/kappzzang/jeongsan/JeongsanApplication.kt
@@ -3,6 +3,7 @@ package com.kappzzang.jeongsan
import android.app.Application
import android.util.Log
import com.kakao.sdk.common.KakaoSdk
+import com.kappzzang.jeongsan.build_config.BuildConfig
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
diff --git a/build-config/.gitignore b/build-config/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/build-config/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/build-config/build.gradle.kts b/build-config/build.gradle.kts
new file mode 100644
index 00000000..fd9c0274
--- /dev/null
+++ b/build-config/build.gradle.kts
@@ -0,0 +1,43 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("org.jlleitschuh.gradle.ktlint")
+}
+
+fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
+
+android {
+ compileSdk = 34
+ namespace = "com.kappzzang.jeongsan.build_config"
+
+ buildTypes {
+ debug {
+ buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY"))
+ buildConfigField("String", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY"))
+ buildConfigField("String", "KEYSTORE_NAME", getApiKey("KEYSTORE_NAME"))
+ resValue("string", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY"))
+ resValue("string", "KAKAO_API_KEY_MANIFEST", getApiKey("KAKAO_API_KEY_MANIFEST"))
+
+ buildConfigField("String", "KAKAO_API_URL", getApiKey("KAKAO_API_URL"))
+ buildConfigField("String", "SERVICE_URL", getApiKey("SERVICE_URL"))
+ buildConfigField("String", "KAKAO_AUTH_URL", getApiKey("KAKAO_AUTH_URL"))
+ }
+
+ release {
+ buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY"))
+ buildConfigField("String", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY"))
+ buildConfigField("String", "KEYSTORE_NAME", getApiKey("KEYSTORE_NAME"))
+ resValue("string", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY"))
+ resValue("string", "KAKAO_API_KEY_MANIFEST", getApiKey("KAKAO_API_KEY_MANIFEST"))
+
+ buildConfigField("String", "KAKAO_API_URL", getApiKey("KAKAO_API_URL"))
+ buildConfigField("String", "SERVICE_URL", getApiKey("SERVICE_URL"))
+ buildConfigField("String", "KAKAO_AUTH_URL", getApiKey("KAKAO_AUTH_URL"))
+ }
+ }
+ buildFeatures {
+ buildConfig = true
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index ef217c7f..4491c2a6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -23,4 +23,8 @@ allprojects {
}
group = "com.kappzzang.jeongsan"
+
+ afterEvaluate {
+ project.apply("$rootDir/gradle/common.gradle")
+ }
}
diff --git a/common/datastore/build.gradle.kts b/common/datastore/build.gradle.kts
index 0f65868f..866c8f5e 100644
--- a/common/datastore/build.gradle.kts
+++ b/common/datastore/build.gradle.kts
@@ -8,40 +8,13 @@ plugins {
android {
namespace = "com.kappzzang.datastore"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
}
dependencies {
-
implementation("androidx.datastore:datastore-preferences:1.1.1")
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}
diff --git a/common/navigation/build.gradle.kts b/common/navigation/build.gradle.kts
index 59a80239..5f595917 100644
--- a/common/navigation/build.gradle.kts
+++ b/common/navigation/build.gradle.kts
@@ -5,30 +5,11 @@ plugins {
android {
namespace = "com.kappzzang.jeongsan.navigation"
- compileSdk = 34
defaultConfig {
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- }
-
- buildTypes {
- release {
- isMinifyEnabled = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
}
}
@@ -37,7 +18,4 @@ dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
- testImplementation("junit:junit:4.13.2")
- androidTestImplementation("androidx.test.ext:junit:1.2.1")
- androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
}
diff --git a/common/resource/build.gradle.kts b/common/resource/build.gradle.kts
index e4be1a53..487d0b19 100644
--- a/common/resource/build.gradle.kts
+++ b/common/resource/build.gradle.kts
@@ -6,21 +6,6 @@ plugins {
android {
namespace = "com.kappzzang.jeongsan"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "1.8"
- }
}
dependencies {
diff --git a/common/retrofit/build.gradle.kts b/common/retrofit/build.gradle.kts
index 2c8abab9..d6550c4d 100644
--- a/common/retrofit/build.gradle.kts
+++ b/common/retrofit/build.gradle.kts
@@ -1,5 +1,3 @@
-import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
-
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
@@ -8,33 +6,8 @@ plugins {
id("com.google.dagger.hilt.android")
}
-fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
-
android {
namespace = "com.kappzzang.jeongsan"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
-
- buildConfigField("String", "KAKAO_API_URL", getApiKey("KAKAO_API_URL"))
- buildConfigField("String", "SERVICE_URL", getApiKey("SERVICE_URL"))
- buildConfigField("String", "KAKAO_AUTH_URL", getApiKey("KAKAO_AUTH_URL"))
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
-
- buildFeatures {
- buildConfig = true
- }
}
dependencies {
@@ -44,4 +17,5 @@ dependencies {
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
+ implementation(project(":build-config"))
}
diff --git a/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/RetrofitModule.kt b/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/RetrofitModule.kt
index 15e62ca3..fb0e133e 100644
--- a/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/RetrofitModule.kt
+++ b/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/RetrofitModule.kt
@@ -1,6 +1,6 @@
package com.kappzzang.jeongsan.retrofit
-import com.kappzzang.jeongsan.BuildConfig
+import com.kappzzang.jeongsan.build_config.BuildConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index c2ec4b9e..a17dc1b4 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -1,5 +1,3 @@
-import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
-
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
@@ -10,25 +8,9 @@ plugins {
android {
namespace = "com.kappzzang.jeongsan"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
}
subprojects {
- fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
apply {
plugin("com.android.library")
plugin("org.jetbrains.kotlin.android")
@@ -47,6 +29,7 @@ subprojects {
implementation(project(":common:util"))
implementation(project(":domain"))
implementation(project(":domain:group"))
+ implementation(project(":build-config"))
// Test Dependencies
testImplementation("org.assertj:assertj-core:3.25.3")
@@ -59,29 +42,4 @@ subprojects {
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1")
}
-
- android {
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
-
- buildConfigField("String", "KAKAO_REST_API_KEY", getApiKey("KAKAO_REST_API_KEY"))
- buildConfigField("String", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY"))
- buildConfigField("String", "KEYSTORE_NAME", getApiKey("KEYSTORE_NAME"))
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
- buildFeatures {
- buildConfig = true
- }
- }
}
diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthenticationDataSource.kt b/data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthenticationDataSource.kt
index 9f842c67..c05787ba 100644
--- a/data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthenticationDataSource.kt
+++ b/data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthenticationDataSource.kt
@@ -1,9 +1,9 @@
package com.kappzzang.jeongsan.datasource
import com.kappzzang.jeongsan.api.KakaoAuthRetrofitService
+import com.kappzzang.jeongsan.build_config.BuildConfig
import com.kappzzang.jeongsan.entity.KakaoRefreshTokenPayloadDTO
import com.kappzzang.jeongsan.entity.KakaoRefreshTokenResponseDTO
-import com.kappzzang.jeongsan.user.BuildConfig
import javax.inject.Inject
import retrofit2.Response
diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts
index 167ce4ea..545d13aa 100644
--- a/domain/build.gradle.kts
+++ b/domain/build.gradle.kts
@@ -8,21 +8,6 @@ plugins {
android {
namespace = "com.kappzzang.jeongsan"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
}
subprojects {
@@ -52,22 +37,4 @@ subprojects {
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1")
}
-
- android {
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
- }
}
diff --git a/gradle/common.gradle b/gradle/common.gradle
new file mode 100644
index 00000000..53bef314
--- /dev/null
+++ b/gradle/common.gradle
@@ -0,0 +1,24 @@
+def hasLibraryPlugin = pluginManager.hasPlugin("com.android.library")
+def hasApplicationPlugin = pluginManager.hasPlugin("com.android.application")
+
+if (hasLibraryPlugin || hasApplicationPlugin) {
+ android {
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 26
+ lint.targetSdk = 34
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 17de2ae3..fd6dd1bf 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -45,3 +45,4 @@ include(":ui:data")
project(":data").children.forEach { module -> module.name = "data-${module.name}" }
include(":common:navigation")
include(":common:retrofit")
+include(":build-config")
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
index eb16ad41..15c1e0cd 100644
--- a/ui/build.gradle.kts
+++ b/ui/build.gradle.kts
@@ -1,5 +1,3 @@
-import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
-
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
@@ -9,26 +7,7 @@ plugins {
}
android {
- fun getApiKey(key: String): String = gradleLocalProperties(rootDir, providers).getProperty(key)
-
namespace = "com.kappzzang.jeongsan"
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles("consumer-rules.pro")
- resValue("string", "KAKAO_API_KEY", getApiKey("KAKAO_API_KEY"))
- }
-
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
}
subprojects {
@@ -84,18 +63,10 @@ subprojects {
androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("androidx.test.espresso:espresso-intents:3.6.1")
androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1")
+ implementation(project(":build-config"))
}
android {
- compileSdk = 34
-
- defaultConfig {
- minSdk = 26
- targetSdk = 34
-
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- }
-
buildTypes {
release {
isMinifyEnabled = false
@@ -105,18 +76,10 @@ subprojects {
)
}
}
- compileOptions {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- }
- kotlinOptions {
- jvmTarget = "17"
- }
buildFeatures {
dataBinding = true
viewBinding = true
- buildConfig = true
resValues = true
}
}
From 84069d13b52d3744826f1c495db777e008695097 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EA=B6=8C=EC=84=B1=EC=B0=AC?=
<54511614+ksc1008@users.noreply.github.com>
Date: Thu, 31 Oct 2024 23:15:51 +0900
Subject: [PATCH 2/4] =?UTF-8?q?Design:=20ExpenseList=20=EB=94=94=EC=9E=90?=
=?UTF-8?q?=EC=9D=B8=20=EC=88=98=EC=A0=95=20(#76)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Refactor: 레이아웃 파일 모듈 이동
* Feat: 테두리 처리 코드 추가
* Chore: OutlineProvider 파일 명 수정
* Design: ExpenseList 그림자 추가
* Design: 프로토타입에 맞춰 디자인 수정
* Design: UI 깨지는 현상 수정, 날짜순 정렬, 통화 표시 개선
* Feat: 분리할 세 뷰모델에 대한 클래스 생성
* Refactor: ExpenseList내 각 페이지마다 개별적인 뷰모델을 갖게 수정
* Chore: 사용하지 않는 Dependency 제거
* Chore: Lint 수행
* Fix: 불필요한 Log 제거
* Chore: Lint 수행
---
common/resource/src/main/res/values/dimen.xml | 2 +-
ui/data/build.gradle.kts | 1 -
.../jeongsan/data/ExpenseListViewUIData.kt | 15 +-
.../kappzzang/jeongsan/data/ExpenseUiItem.kt | 15 ++
.../jeongsan/data/ListViewItemPositionInfo.kt | 3 +
ui/expenselist/build.gradle.kts | 1 +
.../CompleteExpenseListFragment.kt | 10 +-
.../expenselist/ExpenseListActivity.kt | 2 +-
.../expenselist/ExpenseListAdapter.kt | 21 ++-
.../expenselist/ExpenseListBindingAdapter.kt | 30 ++++
.../expenselist/ExpenseListFragment.kt | 13 +-
.../expenselist/ExpenseListViewModel.kt | 152 ------------------
.../expenselist/PendingExpenseListFragment.kt | 10 +-
.../customview/CustomOutlineProvider.kt | 63 ++++++++
.../inviteinfo/InviteInfoDialogFragment.kt | 2 +-
.../expenselist/util/ExpenseUiItemMapper.kt | 28 ++++
.../CompleteExpenseListPageViewModel.kt | 15 ++
.../ExpenseListOnCalculationPageViewModel.kt | 53 ++++++
.../viewmodel/ExpenseListPageViewModel.kt | 86 ++++++++++
.../viewmodel/ExpenseListViewModel.kt | 47 ++++++
.../PendingExpenseListPageViewModel.kt | 15 ++
.../res/drawable/expense_list_bottom_nav.xml | 0
.../expense_list_bottom_nav_color.xml | 0
.../res/drawable/expense_list_chip_color.xml | 0
.../drawable/expense_list_item_background.xml | 0
.../main/res/layout/activity_expense_list.xml | 29 +---
.../layout/fragment_complete_expense_list.xml | 31 +++-
.../main/res/layout/fragment_expense_list.xml | 31 +++-
.../layout/fragment_pending_expense_list.xml | 34 +++-
.../src/main/res/layout/item_expense.xml | 14 +-
ui/expenselist/src/main/res/values/dimens.xml | 5 +
31 files changed, 512 insertions(+), 216 deletions(-)
create mode 100644 ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseUiItem.kt
create mode 100644 ui/data/src/main/java/com/kappzzang/jeongsan/data/ListViewItemPositionInfo.kt
delete mode 100644 ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListViewModel.kt
create mode 100644 ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/customview/CustomOutlineProvider.kt
create mode 100644 ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/util/ExpenseUiItemMapper.kt
create mode 100644 ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/CompleteExpenseListPageViewModel.kt
create mode 100644 ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListOnCalculationPageViewModel.kt
create mode 100644 ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListPageViewModel.kt
create mode 100644 ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListViewModel.kt
create mode 100644 ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/PendingExpenseListPageViewModel.kt
rename {common/resource => ui/expenselist}/src/main/res/drawable/expense_list_bottom_nav.xml (100%)
rename {common/resource => ui/expenselist}/src/main/res/drawable/expense_list_bottom_nav_color.xml (100%)
rename {common/resource => ui/expenselist}/src/main/res/drawable/expense_list_chip_color.xml (100%)
rename {common/resource => ui/expenselist}/src/main/res/drawable/expense_list_item_background.xml (100%)
create mode 100644 ui/expenselist/src/main/res/values/dimens.xml
diff --git a/common/resource/src/main/res/values/dimen.xml b/common/resource/src/main/res/values/dimen.xml
index ae04628b..5af2e8f5 100644
--- a/common/resource/src/main/res/values/dimen.xml
+++ b/common/resource/src/main/res/values/dimen.xml
@@ -63,4 +63,4 @@
3dp
20sp
-
\ No newline at end of file
+
diff --git a/ui/data/build.gradle.kts b/ui/data/build.gradle.kts
index d4256d34..ccb054d5 100644
--- a/ui/data/build.gradle.kts
+++ b/ui/data/build.gradle.kts
@@ -37,7 +37,6 @@ dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.android.material:material:1.12.0")
- implementation(project(":domain:expense"))
implementation(project(":domain:group"))
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
diff --git a/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseListViewUIData.kt b/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseListViewUIData.kt
index 6980a733..22749129 100644
--- a/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseListViewUIData.kt
+++ b/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseListViewUIData.kt
@@ -1,10 +1,15 @@
package com.kappzzang.jeongsan.data
-import com.kappzzang.jeongsan.model.ExpenseItem
-
data class ExpenseListViewUIData(
val totalPriceText: String,
val priceToSendText: String,
- val groupNameText: String,
- val expenseItems: List
-)
+ val expenseItems: List
+) {
+ companion object {
+ val emptyData = ExpenseListViewUIData(
+ "",
+ "",
+ emptyList()
+ )
+ }
+}
diff --git a/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseUiItem.kt b/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseUiItem.kt
new file mode 100644
index 00000000..ea4df29d
--- /dev/null
+++ b/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseUiItem.kt
@@ -0,0 +1,15 @@
+package com.kappzzang.jeongsan.data
+
+import java.time.LocalDateTime
+
+data class ExpenseUiItem(
+ val id: String,
+ val name: String,
+ val isFirstItem: Boolean,
+ val isLastItem: Boolean,
+ val payerName: String,
+ val payerMemberId: String,
+ val price: String,
+ val date: LocalDateTime,
+ val categoryColor: String
+)
diff --git a/ui/data/src/main/java/com/kappzzang/jeongsan/data/ListViewItemPositionInfo.kt b/ui/data/src/main/java/com/kappzzang/jeongsan/data/ListViewItemPositionInfo.kt
new file mode 100644
index 00000000..453b4dc0
--- /dev/null
+++ b/ui/data/src/main/java/com/kappzzang/jeongsan/data/ListViewItemPositionInfo.kt
@@ -0,0 +1,3 @@
+package com.kappzzang.jeongsan.data
+
+data class ListViewItemPositionInfo(val isFirstItem: Boolean, val isLastItem: Boolean)
diff --git a/ui/expenselist/build.gradle.kts b/ui/expenselist/build.gradle.kts
index 29134755..6d64b523 100644
--- a/ui/expenselist/build.gradle.kts
+++ b/ui/expenselist/build.gradle.kts
@@ -6,6 +6,7 @@ dependencies {
implementation("androidx.navigation:navigation-fragment-ktx:2.8.1")
implementation("androidx.navigation:navigation-ui-ktx:2.8.1")
implementation("nl.dionsegijn:konfetti-xml:2.0.4")
+ implementation("androidx.hilt:hilt-navigation-fragment:1.1.0")
implementation(project(":domain:group"))
implementation(project(":domain:expense"))
implementation(project(":domain:ocr"))
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/CompleteExpenseListFragment.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/CompleteExpenseListFragment.kt
index 83a338cd..4a7552d6 100644
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/CompleteExpenseListFragment.kt
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/CompleteExpenseListFragment.kt
@@ -6,13 +6,17 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.kappzzang.jeongsan.expenselist.databinding.FragmentCompleteExpenseListBinding
+import com.kappzzang.jeongsan.expenselist.viewmodel.CompleteExpenseListPageViewModel
+import com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class CompleteExpenseListFragment : Fragment() {
- private val viewModel: ExpenseListViewModel by activityViewModels()
+ private val activityViewModel: ExpenseListViewModel by activityViewModels()
+ private val viewModel: CompleteExpenseListPageViewModel by viewModels()
private lateinit var binding: FragmentCompleteExpenseListBinding
override fun onCreateView(
@@ -31,9 +35,9 @@ class CompleteExpenseListFragment : Fragment() {
// UI 확인을 위한 임시 코드
binding.completeExpenseListRecyclerview.adapter = ExpenseListAdapter {
- viewModel.clickExpenseItem(it)
+ activityViewModel.clickExpenseItem(it)
}
binding.completeExpenseListRecyclerview.layoutManager = LinearLayoutManager(this.context)
- viewModel.clickSentCompleteMenuButton()
+ viewModel.onFragmentStart(activityViewModel.groupId.value)
}
}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListActivity.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListActivity.kt
index 048e7050..fdae7b7c 100644
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListActivity.kt
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListActivity.kt
@@ -23,6 +23,7 @@ import androidx.navigation.ui.setupWithNavController
import com.kappzzang.jeongsan.expenselist.databinding.ActivityExpenseListBinding
import com.kappzzang.jeongsan.expenselist.inviteinfo.InviteInfoDialogFragment
import com.kappzzang.jeongsan.expenselist.sendmessage.SendMessageActivity
+import com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListViewModel
import com.kappzzang.jeongsan.intentcontract.AddExpenseContract
import com.kappzzang.jeongsan.intentcontract.ExpenseListContract
import com.kappzzang.jeongsan.intentcontract.ReceiptCameraContract
@@ -84,7 +85,6 @@ class ExpenseListActivity : AppCompatActivity() {
activityReceiptCameraLauncher = createReceiptCameraLauncher()
- // TODO: 임시 연결용 코드
binding.requestExpenseFab.setOnClickListener {
startActivity(Intent(this, SendMessageActivity::class.java))
}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListAdapter.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListAdapter.kt
index de84749e..da77e1b1 100644
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListAdapter.kt
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListAdapter.kt
@@ -5,19 +5,22 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
+import com.kappzzang.jeongsan.data.ExpenseUiItem
+import com.kappzzang.jeongsan.data.ListViewItemPositionInfo
import com.kappzzang.jeongsan.expenselist.databinding.ItemExpenseBinding
-import com.kappzzang.jeongsan.model.ExpenseItem
import com.kappzzang.jeongsan.util.DateConverter.formatToExpenseDate
class ExpenseListAdapter(private val onExpenseItemClickListener: (expenseId: String) -> Unit) :
- ListAdapter(
+ ListAdapter(
object :
- DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldItem: ExpenseItem, newItem: ExpenseItem): Boolean =
+ DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ExpenseUiItem, newItem: ExpenseUiItem): Boolean =
oldItem.id == newItem.id
- override fun areContentsTheSame(oldItem: ExpenseItem, newItem: ExpenseItem): Boolean =
- oldItem == newItem
+ override fun areContentsTheSame(
+ oldItem: ExpenseUiItem,
+ newItem: ExpenseUiItem
+ ): Boolean = oldItem == newItem
}
) {
@@ -31,7 +34,7 @@ class ExpenseListAdapter(private val onExpenseItemClickListener: (expenseId: Str
}
}
- fun bind(expenseItem: ExpenseItem) {
+ fun bind(expenseItem: ExpenseUiItem) {
binding.categoryColorView.setBackgroundColor(
android.graphics.Color.parseColor(
expenseItem.categoryColor
@@ -39,6 +42,10 @@ class ExpenseListAdapter(private val onExpenseItemClickListener: (expenseId: Str
)
binding.expenseItem = expenseItem
binding.expenseDate = expenseItem.date.formatToExpenseDate()
+ binding.positionInfo = ListViewItemPositionInfo(
+ isFirstItem = this.bindingAdapterPosition == 0,
+ isLastItem = this.bindingAdapter?.itemCount?.minus(1) == this.bindingAdapterPosition
+ )
}
}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListBindingAdapter.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListBindingAdapter.kt
index 67ba78e7..f5b82e8d 100644
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListBindingAdapter.kt
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListBindingAdapter.kt
@@ -1,7 +1,10 @@
package com.kappzzang.jeongsan.expenselist
+import android.view.View
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
+import com.kappzzang.jeongsan.expenselist.customview.CustomOutlineProvider
+import com.kappzzang.jeongsan.expenselist.customview.ExpenseListItemBoxType
import kotlinx.coroutines.flow.StateFlow
object ExpenseListBindingAdapter {
@@ -18,4 +21,31 @@ object ExpenseListBindingAdapter {
)
}
}
+
+ @BindingAdapter(
+ value = ["clipRadius", "clipUpperCorner", "clipBottomCorner"],
+ requireAll = false
+ )
+ @JvmStatic
+ fun bindCorners(view: View, radius: Float?, upperCorner: Boolean?, bottomCorner: Boolean?) {
+ val boxType = if (upperCorner == true) {
+ if (bottomCorner == true) {
+ ExpenseListItemBoxType.ALL_CORNERS
+ } else {
+ ExpenseListItemBoxType.TOP_CORNER
+ }
+ } else {
+ if (bottomCorner == true) {
+ ExpenseListItemBoxType.BOTTOM_CORNER
+ } else {
+ ExpenseListItemBoxType.NO_CORNERS
+ }
+ }
+
+ view.outlineProvider = CustomOutlineProvider(
+ radius ?: view.resources.getDimension(R.dimen.expense_list_item_corner_radius),
+ boxType
+ )
+ view.clipToOutline = true
+ }
}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListFragment.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListFragment.kt
index f720f0d3..85dbefa7 100644
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListFragment.kt
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListFragment.kt
@@ -6,13 +6,17 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.kappzzang.jeongsan.expenselist.databinding.FragmentExpenseListBinding
+import com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListOnCalculationPageViewModel
+import com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ExpenseListFragment : Fragment() {
- private val viewModel: ExpenseListViewModel by activityViewModels()
+ private val activityViewModel: ExpenseListViewModel by activityViewModels()
+ private val viewModel: ExpenseListOnCalculationPageViewModel by viewModels()
private lateinit var binding: FragmentExpenseListBinding
override fun onCreateView(
@@ -29,11 +33,12 @@ class ExpenseListFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- // UI 확인을 위한 임시 코드
binding.expenseListRecyclerview.adapter = ExpenseListAdapter {
- viewModel.clickExpenseItem(it)
+ activityViewModel.clickExpenseItem(it)
}
+
binding.expenseListRecyclerview.layoutManager = LinearLayoutManager(this.context)
- viewModel.clickOnlyNotConfirmedExpensesChipButton()
+
+ viewModel.onFragmentStart(activityViewModel.groupId.value)
}
}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListViewModel.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListViewModel.kt
deleted file mode 100644
index 5d567f51..00000000
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/ExpenseListViewModel.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-package com.kappzzang.jeongsan.expenselist
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import com.kappzzang.jeongsan.data.ExpenseListViewUIData
-import com.kappzzang.jeongsan.model.ExpenseListResponse
-import com.kappzzang.jeongsan.model.ExpenseState
-import com.kappzzang.jeongsan.usecase.GetCurrentGroupInfoUseCase
-import com.kappzzang.jeongsan.usecase.GetExpenseListUseCase
-import com.kappzzang.jeongsan.util.IntegerFormatter.formatDecimalSeparator
-import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.zip
-import kotlinx.coroutines.launch
-
-@HiltViewModel
-class ExpenseListViewModel @Inject constructor(
- private val getCurrentGroupInfoUseCase: GetCurrentGroupInfoUseCase,
- private val getExpenseListUseCase: GetExpenseListUseCase
-) : ViewModel() {
-
- private var expenseListFetchingJob: Job? = null
- private var _groupId = MutableStateFlow("")
-
- private val expenseList =
- MutableStateFlow(ExpenseListResponse.emptyList())
- private val _groupName = MutableStateFlow("")
-
- private val _selectedExpense = MutableStateFlow("")
-
- private val _uiData by lazy {
- combine(
- expenseList,
- _groupName
- ) { expenseList, groupName ->
- val totalPrice = expenseList.totalPrice
- val priceToSend = expenseList.totalExpenseToSend
- val items = expenseList.expenseList
-
- ExpenseListViewUIData(
- totalPriceText = "${totalPrice.formatDecimalSeparator()}원",
- priceToSendText = "${priceToSend.formatDecimalSeparator()}원",
- groupNameText = groupName,
- expenseItems = items
- )
- }.stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(5000L),
- ExpenseListViewUIData("", "", "", listOf())
- )
- }
-
- val groupName = _groupName.asStateFlow()
-
- val groupId = _groupId.asStateFlow()
-
- val uiData by lazy {
- _uiData
- }
-
- val selectedExpense = _selectedExpense.asStateFlow()
-
- private fun cancelPreviousJob() {
- if (expenseListFetchingJob?.isCompleted != false) {
- return
- }
- expenseListFetchingJob?.cancel()
- }
-
- private fun fetchExpenseList(expenseState: ExpenseState) {
- cancelPreviousJob()
- expenseListFetchingJob = viewModelScope.launch(Dispatchers.IO) {
- getExpenseListUseCase(_groupId.value, expenseState)
- .collect {
- expenseList.emit(it)
- }
- }
- }
-
- // 미확인 + 확인 지출 모두 불러오기
- private fun fetchCalculatingExpenseList() {
- cancelPreviousJob()
- expenseListFetchingJob = viewModelScope.launch(Dispatchers.IO) {
- getExpenseListUseCase(_groupId.value, ExpenseState.CONFIRMED).zip(
- getExpenseListUseCase(
- _groupId.value,
- ExpenseState.NOT_CONFIRMED
- )
- ) { confirmed, notConfirmed ->
- ExpenseListResponse(
- expenseList = confirmed.expenseList.toMutableList() + notConfirmed.expenseList,
- totalPrice = confirmed.totalPrice + notConfirmed.totalPrice,
- totalExpenseToSend = 0
- )
- }.collect {
- expenseList.emit(it)
- }
- }
- }
-
- private fun fetchGroupInfo() {
- viewModelScope.launch(Dispatchers.IO) {
- getCurrentGroupInfoUseCase(_groupId.value).map {
- it.name
- }.collect {
- _groupName.emit(it)
- }
- }
- }
-
- fun clickPendSendingMenuButton() {
- fetchExpenseList(ExpenseState.TRANSFER_PENDING)
- }
-
- fun clickOnCalculatingMenuButton() {
- fetchExpenseList(ExpenseState.NOT_CONFIRMED)
- }
-
- fun clickSentCompleteMenuButton() {
- fetchExpenseList(ExpenseState.TRANSFERED)
- }
-
- fun clickAllExpensesChipButton() {
- fetchCalculatingExpenseList()
- }
-
- fun clickOnlyNotConfirmedExpensesChipButton() {
- fetchExpenseList(ExpenseState.NOT_CONFIRMED)
- }
-
- fun clickOnlyConfirmedExpensesChipButton() {
- fetchExpenseList(ExpenseState.CONFIRMED)
- }
-
- fun updateGroupId(groupId: String) {
- this._groupId.value = groupId
-
- fetchGroupInfo()
- }
-
- fun clickExpenseItem(expenseId: String) {
- _selectedExpense.value = expenseId
- }
-}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/PendingExpenseListFragment.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/PendingExpenseListFragment.kt
index a0c49e8b..fe55a902 100644
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/PendingExpenseListFragment.kt
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/PendingExpenseListFragment.kt
@@ -6,13 +6,17 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
+import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.kappzzang.jeongsan.expenselist.databinding.FragmentPendingExpenseListBinding
+import com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListViewModel
+import com.kappzzang.jeongsan.expenselist.viewmodel.PendingExpenseListPageViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class PendingExpenseListFragment : Fragment() {
- private val viewModel: ExpenseListViewModel by activityViewModels()
+ private val activityViewModel: ExpenseListViewModel by activityViewModels()
+ private val viewModel: PendingExpenseListPageViewModel by viewModels()
private lateinit var binding: FragmentPendingExpenseListBinding
override fun onCreateView(
@@ -30,10 +34,10 @@ class PendingExpenseListFragment : Fragment() {
binding.viewModel = viewModel
binding.lifecycleOwner = activity
binding.pendingExpenseListRecyclerview.adapter = ExpenseListAdapter {
- viewModel.clickExpenseItem(it)
+ activityViewModel.clickExpenseItem(it)
}
binding.pendingExpenseListRecyclerview.layoutManager = LinearLayoutManager(this.context)
- viewModel.clickPendSendingMenuButton()
+ viewModel.onFragmentStart(activityViewModel.groupId.value)
}
}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/customview/CustomOutlineProvider.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/customview/CustomOutlineProvider.kt
new file mode 100644
index 00000000..29edfb72
--- /dev/null
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/customview/CustomOutlineProvider.kt
@@ -0,0 +1,63 @@
+package com.kappzzang.jeongsan.expenselist.customview
+
+import android.graphics.Outline
+import android.util.TypedValue
+import android.view.View
+import android.view.ViewOutlineProvider
+
+enum class ExpenseListItemBoxType { TOP_CORNER, BOTTOM_CORNER, ALL_CORNERS, NO_CORNERS }
+
+class CustomOutlineProvider(
+ private val cornerRadiusDP: Float,
+ private val outlineType: ExpenseListItemBoxType
+) : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ val left = 0
+ val top = 0
+ val right = view.width
+ val bottom = view.height
+
+ val cornerRadius = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ cornerRadiusDP,
+ view.resources.displayMetrics
+ )
+ when (outlineType) {
+ ExpenseListItemBoxType.TOP_CORNER ->
+ outline.setRoundRect(
+ left,
+ top,
+ right,
+ bottom + cornerRadius.toInt(),
+ cornerRadius
+ )
+
+ ExpenseListItemBoxType.BOTTOM_CORNER ->
+ outline.setRoundRect(
+ left,
+ top - cornerRadius.toInt(),
+ right,
+ bottom,
+ cornerRadius
+ )
+
+ ExpenseListItemBoxType.NO_CORNERS ->
+ outline.setRoundRect(
+ left,
+ top,
+ right,
+ bottom,
+ 0f
+ )
+
+ ExpenseListItemBoxType.ALL_CORNERS ->
+ outline.setRoundRect(
+ left,
+ top,
+ right,
+ bottom,
+ cornerRadius
+ )
+ }
+ }
+}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/inviteinfo/InviteInfoDialogFragment.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/inviteinfo/InviteInfoDialogFragment.kt
index 72867765..65755f3e 100644
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/inviteinfo/InviteInfoDialogFragment.kt
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/inviteinfo/InviteInfoDialogFragment.kt
@@ -13,8 +13,8 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
-import com.kappzzang.jeongsan.expenselist.ExpenseListViewModel
import com.kappzzang.jeongsan.expenselist.databinding.FragmentInviteInfoDialogBinding
+import com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/util/ExpenseUiItemMapper.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/util/ExpenseUiItemMapper.kt
new file mode 100644
index 00000000..38b08d84
--- /dev/null
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/util/ExpenseUiItemMapper.kt
@@ -0,0 +1,28 @@
+package com.kappzzang.jeongsan.expenselist.util
+
+import com.kappzzang.jeongsan.data.ExpenseUiItem
+import com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListPageViewModel.Companion.CURRENCY_POSTFIX
+import com.kappzzang.jeongsan.model.ExpenseItem
+import com.kappzzang.jeongsan.util.IntegerFormatter.formatDecimalSeparator
+
+object ExpenseUiItemMapper {
+ internal fun mapToExpenseUiItem(expenseItemList: List): List =
+ expenseItemList.mapIndexed { index, item ->
+ ExpenseUiItem(
+ isFirstItem = index == 0,
+ isLastItem = index == expenseItemList.size - 1,
+ id = item.id,
+ name = item.name,
+ date = item.date,
+ payerMemberId = item.payerMemberId,
+ categoryColor = item.categoryColor,
+ payerName = item.payerName,
+ price = "${item.price.formatDecimalSeparator()} $CURRENCY_POSTFIX"
+ )
+ }
+
+ internal fun sortItemsByTime(expenseItemList: List): List =
+ expenseItemList.sortedBy {
+ it.date
+ }.reversed()
+}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/CompleteExpenseListPageViewModel.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/CompleteExpenseListPageViewModel.kt
new file mode 100644
index 00000000..4de618a8
--- /dev/null
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/CompleteExpenseListPageViewModel.kt
@@ -0,0 +1,15 @@
+package com.kappzzang.jeongsan.expenselist.viewmodel
+
+import com.kappzzang.jeongsan.model.ExpenseState
+import com.kappzzang.jeongsan.usecase.GetExpenseListUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class CompleteExpenseListPageViewModel @Inject constructor(
+ getExpenseListUseCase: GetExpenseListUseCase
+) : ExpenseListPageViewModel(getExpenseListUseCase) {
+ override fun fetchDefaultList(groupId: String) {
+ fetchExpenseList(ExpenseState.TRANSFERED, groupId)
+ }
+}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListOnCalculationPageViewModel.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListOnCalculationPageViewModel.kt
new file mode 100644
index 00000000..9f3b1c4b
--- /dev/null
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListOnCalculationPageViewModel.kt
@@ -0,0 +1,53 @@
+package com.kappzzang.jeongsan.expenselist.viewmodel
+
+import androidx.lifecycle.viewModelScope
+import com.kappzzang.jeongsan.model.ExpenseListResponse
+import com.kappzzang.jeongsan.model.ExpenseState
+import com.kappzzang.jeongsan.usecase.GetExpenseListUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.zip
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class ExpenseListOnCalculationPageViewModel @Inject constructor(
+ getExpenseListUseCase: GetExpenseListUseCase
+) : ExpenseListPageViewModel(getExpenseListUseCase) {
+ override fun fetchDefaultList(groupId: String) {
+ fetchExpenseList(ExpenseState.NOT_CONFIRMED, groupId)
+ }
+
+ // 미확인 + 확인 지출 모두 불러오기
+ private fun fetchCalculatingExpenseList(groupId: String) {
+ cancelPreviousJob()
+ expenseListFetchingJob = viewModelScope.launch(Dispatchers.IO) {
+ getExpenseListUseCase(groupId, ExpenseState.CONFIRMED).zip(
+ getExpenseListUseCase(
+ groupId,
+ ExpenseState.NOT_CONFIRMED
+ )
+ ) { confirmed, notConfirmed ->
+ ExpenseListResponse(
+ expenseList = confirmed.expenseList.toMutableList() + notConfirmed.expenseList,
+ totalPrice = confirmed.totalPrice + notConfirmed.totalPrice,
+ totalExpenseToSend = 0
+ )
+ }.collect {
+ expenseList.emit(it)
+ }
+ }
+ }
+
+ fun clickAllExpensesChipButton() {
+ fetchCalculatingExpenseList(groupId.value)
+ }
+
+ fun clickOnlyNotConfirmedExpensesChipButton() {
+ fetchExpenseList(ExpenseState.NOT_CONFIRMED, groupId.value)
+ }
+
+ fun clickOnlyConfirmedExpensesChipButton() {
+ fetchExpenseList(ExpenseState.CONFIRMED, groupId.value)
+ }
+}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListPageViewModel.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListPageViewModel.kt
new file mode 100644
index 00000000..77c3fdbf
--- /dev/null
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListPageViewModel.kt
@@ -0,0 +1,86 @@
+package com.kappzzang.jeongsan.expenselist.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kappzzang.jeongsan.data.ExpenseListViewUIData
+import com.kappzzang.jeongsan.expenselist.util.ExpenseUiItemMapper.mapToExpenseUiItem
+import com.kappzzang.jeongsan.expenselist.util.ExpenseUiItemMapper.sortItemsByTime
+import com.kappzzang.jeongsan.model.ExpenseListResponse
+import com.kappzzang.jeongsan.model.ExpenseState
+import com.kappzzang.jeongsan.usecase.GetExpenseListUseCase
+import com.kappzzang.jeongsan.util.IntegerFormatter.formatDecimalSeparator
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+open class ExpenseListPageViewModel @Inject constructor(
+ protected val getExpenseListUseCase: GetExpenseListUseCase
+) : ViewModel() {
+
+ protected var expenseListFetchingJob: Job? = null
+ protected val expenseList =
+ MutableStateFlow(ExpenseListResponse.emptyList())
+ protected val groupId = MutableStateFlow("")
+
+ private val _uiData by lazy {
+ expenseList.map { expenseList ->
+ val totalPrice = expenseList.totalPrice
+ val priceToSend = expenseList.totalExpenseToSend
+ val items = expenseList.expenseList
+
+ ExpenseListViewUIData(
+ totalPriceText = "${totalPrice.formatDecimalSeparator()}$CURRENCY_POSTFIX",
+ priceToSendText = "${priceToSend.formatDecimalSeparator()}$CURRENCY_POSTFIX",
+ expenseItems = mapToExpenseUiItem(sortItemsByTime(items))
+ )
+ }.stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(5000L),
+ ExpenseListViewUIData.emptyData
+ )
+ }
+
+ val uiData by lazy {
+ _uiData
+ }
+
+ protected fun cancelPreviousJob() {
+ if (expenseListFetchingJob?.isCompleted != false) {
+ return
+ }
+ expenseListFetchingJob?.cancel()
+ }
+
+ protected fun fetchExpenseList(expenseState: ExpenseState, groupId: String) {
+ cancelPreviousJob()
+ expenseListFetchingJob = viewModelScope.launch(Dispatchers.IO) {
+ getExpenseListUseCase(groupId, expenseState)
+ .collect {
+ expenseList.emit(it)
+ }
+ }
+ }
+
+ protected open fun fetchDefaultList(groupId: String) {
+ }
+
+ fun onFragmentStart(groupId: String) {
+ if (this.groupId.value != groupId) {
+ this.groupId.value = groupId
+ fetchDefaultList(this.groupId.value)
+ } else {
+ return
+ }
+ }
+
+ companion object {
+ const val CURRENCY_POSTFIX = "원"
+ }
+}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListViewModel.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListViewModel.kt
new file mode 100644
index 00000000..61b481c3
--- /dev/null
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/ExpenseListViewModel.kt
@@ -0,0 +1,47 @@
+package com.kappzzang.jeongsan.expenselist.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.kappzzang.jeongsan.usecase.GetCurrentGroupInfoUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+@HiltViewModel
+class ExpenseListViewModel @Inject constructor(
+ private val getCurrentGroupInfoUseCase: GetCurrentGroupInfoUseCase
+) : ViewModel() {
+ private var _groupId = MutableStateFlow("")
+ private val _groupName = MutableStateFlow("")
+
+ val groupName = _groupName.asStateFlow()
+
+ val groupId = _groupId.asStateFlow()
+
+ private val _selectedExpense = MutableStateFlow("")
+ val selectedExpense = _selectedExpense.asStateFlow()
+
+ private fun fetchGroupInfo() {
+ viewModelScope.launch(Dispatchers.IO) {
+ getCurrentGroupInfoUseCase(_groupId.value).map {
+ it.name
+ }.collect {
+ _groupName.emit(it)
+ }
+ }
+ }
+
+ fun updateGroupId(groupId: String) {
+ this._groupId.value = groupId
+
+ fetchGroupInfo()
+ }
+
+ fun clickExpenseItem(expenseId: String) {
+ _selectedExpense.value = expenseId
+ }
+}
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/PendingExpenseListPageViewModel.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/PendingExpenseListPageViewModel.kt
new file mode 100644
index 00000000..141c84b6
--- /dev/null
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/viewmodel/PendingExpenseListPageViewModel.kt
@@ -0,0 +1,15 @@
+package com.kappzzang.jeongsan.expenselist.viewmodel
+
+import com.kappzzang.jeongsan.model.ExpenseState
+import com.kappzzang.jeongsan.usecase.GetExpenseListUseCase
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+@HiltViewModel
+class PendingExpenseListPageViewModel @Inject constructor(
+ getExpenseListUseCase: GetExpenseListUseCase
+) : ExpenseListPageViewModel(getExpenseListUseCase) {
+ override fun fetchDefaultList(groupId: String) {
+ fetchExpenseList(ExpenseState.TRANSFER_PENDING, groupId)
+ }
+}
diff --git a/common/resource/src/main/res/drawable/expense_list_bottom_nav.xml b/ui/expenselist/src/main/res/drawable/expense_list_bottom_nav.xml
similarity index 100%
rename from common/resource/src/main/res/drawable/expense_list_bottom_nav.xml
rename to ui/expenselist/src/main/res/drawable/expense_list_bottom_nav.xml
diff --git a/common/resource/src/main/res/drawable/expense_list_bottom_nav_color.xml b/ui/expenselist/src/main/res/drawable/expense_list_bottom_nav_color.xml
similarity index 100%
rename from common/resource/src/main/res/drawable/expense_list_bottom_nav_color.xml
rename to ui/expenselist/src/main/res/drawable/expense_list_bottom_nav_color.xml
diff --git a/common/resource/src/main/res/drawable/expense_list_chip_color.xml b/ui/expenselist/src/main/res/drawable/expense_list_chip_color.xml
similarity index 100%
rename from common/resource/src/main/res/drawable/expense_list_chip_color.xml
rename to ui/expenselist/src/main/res/drawable/expense_list_chip_color.xml
diff --git a/common/resource/src/main/res/drawable/expense_list_item_background.xml b/ui/expenselist/src/main/res/drawable/expense_list_item_background.xml
similarity index 100%
rename from common/resource/src/main/res/drawable/expense_list_item_background.xml
rename to ui/expenselist/src/main/res/drawable/expense_list_item_background.xml
diff --git a/ui/expenselist/src/main/res/layout/activity_expense_list.xml b/ui/expenselist/src/main/res/layout/activity_expense_list.xml
index b9d19c3f..cbb44612 100644
--- a/ui/expenselist/src/main/res/layout/activity_expense_list.xml
+++ b/ui/expenselist/src/main/res/layout/activity_expense_list.xml
@@ -7,7 +7,7 @@
+ type="com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListViewModel" />
+ tools:text="@{viewModel.groupName}" />
-
-
-
+ type="com.kappzzang.jeongsan.expenselist.viewmodel.CompleteExpenseListPageViewModel" />
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/total_expense_explain_textview"
+ app:layout_constraintStart_toStartOf="parent"/>
+ type="com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListOnCalculationPageViewModel" />
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/total_expense_explain_textview">
+ type="com.kappzzang.jeongsan.expenselist.viewmodel.PendingExpenseListPageViewModel" />
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/total_expense_explain_textview" />
+ type="com.kappzzang.jeongsan.data.ExpenseUiItem" />
+
+ android:layout_marginBottom="1dp"
+ android:background="@color/white"
+ clipUpperCorner="@{positionInfo.firstItem}"
+ clipBottomCorner="@{positionInfo.lastItem}">
+
+ 10dp
+ 90dp
+
From d65bca177cf9092198e04ce3349058d280575876 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=A0=95=EC=88=98=ED=98=84?=
Date: Fri, 1 Nov 2024 12:07:31 +0900
Subject: [PATCH 3/4] =?UTF-8?q?Test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?=
=?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#78)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Refactor: Unit Test를 위해 MainPageViewModel 수정
- Test 단계에서 Test Dispatcher를 사용할 수 있도록, IO Dispatcher를 주입하는 것으로 수정
- private 함수를 테스트하기 위해 public으로 수정
* Test: MainPageViewModel을 위한 단위 테스트 작성
* Test: AddExpenseViewModel을 위한 단위 테스트 작성
* Test: CreateGroupViewModel을 위한 단위 테스트 작성
* Test: SendMessageViewModel을 위한 단위 테스트 작성
---
.../kappzzang/jeongsan/di/DispatcherModule.kt | 16 ++
.../addexpense/AddExpenseViewModel.kt | 8 +-
.../addexpense/AddExpenseViewModelTest.kt | 195 ++++++++++++++++++
.../creategroup/CreateGroupViewModelTest.kt | 154 ++++++++++++++
.../sendmessage/SendMessageViewModelTest.kt | 79 +++++++
.../jeongsan/main/MainPageViewModel.kt | 9 +-
.../jeongsan/main/MainPageViewModelTest.kt | 155 ++++++++++++++
7 files changed, 609 insertions(+), 7 deletions(-)
create mode 100644 app/src/main/java/com/kappzzang/jeongsan/di/DispatcherModule.kt
create mode 100644 ui/addexpense/src/test/java/com/kappzzang/jeongsan/addexpense/AddExpenseViewModelTest.kt
create mode 100644 ui/creategroup/src/test/java/com/kappzzang/jeongsan/creategroup/CreateGroupViewModelTest.kt
create mode 100644 ui/expenselist/src/test/java/com/kappzzang/jeongsan/expenselist/sendmessage/SendMessageViewModelTest.kt
create mode 100644 ui/main/src/test/java/com/kappzzang/jeongsan/main/MainPageViewModelTest.kt
diff --git a/app/src/main/java/com/kappzzang/jeongsan/di/DispatcherModule.kt b/app/src/main/java/com/kappzzang/jeongsan/di/DispatcherModule.kt
new file mode 100644
index 00000000..164c10b0
--- /dev/null
+++ b/app/src/main/java/com/kappzzang/jeongsan/di/DispatcherModule.kt
@@ -0,0 +1,16 @@
+package com.kappzzang.jeongsan.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DispatcherModule {
+
+ @Provides
+ fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
+}
diff --git a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseViewModel.kt b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseViewModel.kt
index 23df6e4b..73706713 100644
--- a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseViewModel.kt
+++ b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseViewModel.kt
@@ -12,6 +12,7 @@ import com.kappzzang.jeongsan.usecase.UploadExpenseUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import java.io.ByteArrayOutputStream
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -20,7 +21,8 @@ import kotlinx.coroutines.launch
@HiltViewModel
class AddExpenseViewModel @Inject constructor(
- private val uploadExpenseUseCase: UploadExpenseUseCase
+ private val uploadExpenseUseCase: UploadExpenseUseCase,
+ private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {
private val _expenseItemList by lazy {
MutableStateFlow(
@@ -109,14 +111,14 @@ class AddExpenseViewModel @Inject constructor(
}
)
- viewModelScope.launch(Dispatchers.IO) {
+ viewModelScope.launch(ioDispatcher) {
uploadExpenseUseCase(receiptItem)
}
return true
}
- private fun convertBitmapToBase64(bitmap: Bitmap?): String? {
+ fun convertBitmapToBase64(bitmap: Bitmap?): String? {
if (bitmap == null) {
return null
}
diff --git a/ui/addexpense/src/test/java/com/kappzzang/jeongsan/addexpense/AddExpenseViewModelTest.kt b/ui/addexpense/src/test/java/com/kappzzang/jeongsan/addexpense/AddExpenseViewModelTest.kt
new file mode 100644
index 00000000..f2da5c76
--- /dev/null
+++ b/ui/addexpense/src/test/java/com/kappzzang/jeongsan/addexpense/AddExpenseViewModelTest.kt
@@ -0,0 +1,195 @@
+package com.kappzzang.jeongsan.addexpense
+
+import android.graphics.Bitmap
+import com.kappzzang.jeongsan.model.OcrDetailItem
+import com.kappzzang.jeongsan.model.OcrResultResponse
+import com.kappzzang.jeongsan.model.ReceiptItem
+import com.kappzzang.jeongsan.usecase.UploadExpenseUseCase
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.slot
+import io.mockk.spyk
+import io.mockk.unmockkAll
+import java.time.LocalDateTime
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class AddExpenseViewModelTest {
+
+ private val mockUploadExpenseUseCase = mockk()
+ private lateinit var viewModel: AddExpenseViewModel
+
+ private val testDispatcher = StandardTestDispatcher(TestCoroutineScheduler())
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+ viewModel = spyk(AddExpenseViewModel(mockUploadExpenseUseCase, testDispatcher))
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `영수증 모드인 경우 해당 속성을 반영하는지 확인`() = runTest {
+ // Given
+ val manualMode = AddExpenseViewModel.Companion.ManualMode.RECEIPT
+
+ // When
+ viewModel.setManualMode(manualMode)
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(false, viewModel.manualMode.value)
+ assertEquals(true, viewModel.uploadedImage.value)
+ }
+
+ @Test
+ fun `수기 입력 모드인 경우 해당 속성을 반영하는지 확인`() = runTest {
+ // Given
+ val testManualMode = AddExpenseViewModel.Companion.ManualMode.MANUAL
+
+ // When
+ viewModel.setManualMode(testManualMode)
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(true, viewModel.manualMode.value)
+ assertEquals(false, viewModel.uploadedImage.value)
+ }
+
+ @Test
+ fun `영수증 인식 정보를 잘 반영하는지 확인`() = runTest {
+ // Given
+ val testBitmap = mockk()
+ val testOcrResult = OcrResultResponse.OcrSuccess(
+ name = "Test Receipt",
+ paymentTime = LocalDateTime.now(),
+ detailItems = listOf(
+ OcrDetailItem("Test Item 1", 1000, 1),
+ OcrDetailItem("Test Item 2", 2000, 2)
+ )
+ )
+
+ // When
+ viewModel.setInitialReceiptData(testBitmap, testOcrResult)
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(testBitmap, viewModel.expenseImageBitmap.value)
+ assertEquals(testOcrResult.name, viewModel.expenseName.value)
+ assertEquals(testOcrResult.detailItems.size + 1, viewModel.expenseItemList.value.size)
+ for (i in testOcrResult.detailItems.indices) {
+ assertEquals(
+ testOcrResult.detailItems[i].itemName,
+ viewModel.expenseItemList.value[i].itemName
+ )
+ assertEquals(
+ testOcrResult.detailItems[i].itemPrice,
+ viewModel.expenseItemList.value[i].itemPrice
+ )
+ assertEquals(
+ testOcrResult.detailItems[i].itemQuantity,
+ viewModel.expenseItemList.value[i].itemQuantity
+ )
+ }
+ }
+
+ @Test
+ fun `항목 추가를 진행할 때 정상적으로 늘어나는지 확인`() = runTest {
+ // Given
+ val initialItemSize = viewModel.expenseItemList.value.size
+
+ // When
+ viewModel.addNewExpense()
+ advanceUntilIdle()
+
+ // Then
+ val afterItemSize = viewModel.expenseItemList.value.size
+ assertEquals(initialItemSize + 1, afterItemSize)
+ assertEquals(false, viewModel.expenseItemList.value[afterItemSize - 2].isPlaceholder)
+ assertEquals(true, viewModel.expenseItemList.value[afterItemSize - 1].isPlaceholder)
+ }
+
+ @Test
+ fun `항목 삭제를 진행할 때 정상적으로 삭제되는지 확인`() = runTest {
+ // Given
+ viewModel.addNewExpense()
+ advanceUntilIdle()
+ val initialItemSize = viewModel.expenseItemList.value.size
+
+ // When
+ viewModel.removeExpense(0)
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(initialItemSize - 1, viewModel.expenseItemList.value.size)
+ }
+
+ @Test
+ fun `빈 값들을 업로드 할때, 이를 잘 검사하는지 확인`() = runTest {
+ // Given
+
+ // When
+ val result = viewModel.uploadExpense()
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(false, result)
+ }
+
+ @Test
+ fun `유효한 값들을 업로드 할 때, 잘 실행하는지 확인`() = runTest {
+ // Given
+ val testBitmap = mockk()
+ val testBase64 = "test_base64"
+ every { viewModel.convertBitmapToBase64(any()) } returns testBase64
+ coEvery { mockUploadExpenseUseCase(any()) } returns "test success"
+
+ val testOcrResult = OcrResultResponse.OcrSuccess(
+ name = "Test Receipt",
+ paymentTime = LocalDateTime.now(),
+ detailItems = listOf(
+ OcrDetailItem("Test Item 1", 1000, 1),
+ OcrDetailItem("Test Item 2", 2000, 2)
+ )
+ )
+ viewModel.setInitialReceiptData(testBitmap, testOcrResult)
+ advanceUntilIdle()
+
+ // When
+ val result = viewModel.uploadExpense()
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(true, result)
+ val receiptItemSlot = slot()
+ coVerify { mockUploadExpenseUseCase(capture(receiptItemSlot)) }
+ assertEquals(testOcrResult.name, receiptItemSlot.captured.title)
+ assertEquals(testBase64, receiptItemSlot.captured.imageBase64)
+ assertEquals(
+ testOcrResult.detailItems.size,
+ receiptItemSlot.captured.expenseDetailItemList.size
+ )
+ assertEquals(
+ testOcrResult.detailItems[0].itemName,
+ receiptItemSlot.captured.expenseDetailItemList[0].itemName
+ )
+ }
+}
diff --git a/ui/creategroup/src/test/java/com/kappzzang/jeongsan/creategroup/CreateGroupViewModelTest.kt b/ui/creategroup/src/test/java/com/kappzzang/jeongsan/creategroup/CreateGroupViewModelTest.kt
new file mode 100644
index 00000000..2cd18d86
--- /dev/null
+++ b/ui/creategroup/src/test/java/com/kappzzang/jeongsan/creategroup/CreateGroupViewModelTest.kt
@@ -0,0 +1,154 @@
+package com.kappzzang.jeongsan.creategroup
+
+import com.kappzzang.jeongsan.data.MemberUIData
+import com.kappzzang.jeongsan.usecase.SendInviteMessageUseCase
+import com.kappzzang.jeongsan.usecase.UploadGroupInfoUseCase
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class CreateGroupViewModelTest {
+
+ private val mockUploadGroupInfoUseCase = mockk()
+ private val mockSendInviteMessageUseCase = mockk()
+ private lateinit var viewModel: CreateGroupViewModel
+
+ private val testDispatcher = StandardTestDispatcher(TestCoroutineScheduler())
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+ coEvery { mockUploadGroupInfoUseCase(any()) } returns Unit
+ coEvery { mockSendInviteMessageUseCase(any(), any(), any()) } returns true
+ viewModel = CreateGroupViewModel(mockUploadGroupInfoUseCase, mockSendInviteMessageUseCase)
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `그룹 주제(이모지) 업데이트시 올바르게 반영하는지 확인`() = runTest {
+ // given
+ val testSubject = "😊"
+
+ // when
+ viewModel.updateGroupSubject(testSubject)
+ advanceUntilIdle()
+
+ // then
+ assertEquals(testSubject, viewModel.groupSubject.value)
+ }
+
+ @Test
+ fun `그룹 멤버 리스트 업데이트시 올바르게 반영되는지 확인`() = runTest {
+ // given
+ val testMembers = listOf(
+ MemberUIData("1", "Alice", "test url1"),
+ MemberUIData("2", "Bob", "test url2")
+ )
+
+ // when
+ viewModel.updateGroupMemberList(testMembers)
+ advanceUntilIdle()
+
+ // then
+ assertEquals(testMembers, viewModel.groupMemberList.value)
+ }
+
+ @Test
+ fun `멤버 제거시 해당 위치의 멤버가 삭제되는지 확인`() = runTest {
+ // given
+ val testMembers = listOf(
+ MemberUIData("1", "Alice", "test url1"),
+ MemberUIData("2", "Bob", "test url2"),
+ MemberUIData("3", "Charlie", "test url3")
+ )
+ viewModel.updateGroupMemberList(testMembers)
+ advanceUntilIdle()
+
+ // when
+ viewModel.removeMember(1) // "Bob" 제거
+ advanceUntilIdle()
+
+ // then
+ val updatedList = viewModel.groupMemberList.value
+ assertEquals(testMembers.size - 1, updatedList.size)
+ assertEquals(testMembers[0].name, updatedList[0].name)
+ assertEquals(testMembers[2].name, updatedList[1].name)
+ }
+
+ @Test
+ fun `그룹 정보가 유효하지 않으면 업로드가 진행되지 않는지 확인`() = runTest {
+ // given
+
+ // when
+ val result = viewModel.uploadGroupInfo()
+
+ // then
+ assertEquals(false, result)
+ coVerify(exactly = 0) { mockUploadGroupInfoUseCase(any()) }
+ }
+
+ @Test
+ fun `그룹 정보가 유효하면 업로드가 잘 진행되는지 확인`() = runTest {
+ // given
+ val testGroupName = "Test Group"
+ val testGroupSubject = "✈️"
+ val testMembers = listOf(
+ MemberUIData("1", "Alice", "test url1"),
+ MemberUIData("2", "Bob", "test url2")
+ )
+ viewModel.groupName.emit(testGroupName)
+ viewModel.updateGroupSubject(testGroupSubject)
+ viewModel.updateGroupMemberList(testMembers)
+ advanceUntilIdle()
+
+ // when
+ val result = viewModel.uploadGroupInfo()
+ advanceUntilIdle()
+
+ // then
+ assertEquals(true, result)
+ coVerify { mockUploadGroupInfoUseCase(any()) }
+ }
+
+ @Test
+ fun `모든 멤버에게 초대 메시지를 전송하는지 확인`() = runTest {
+ // given
+ val testGroupId = "test_group_id"
+ val testGroupName = "Test Group"
+ val testMembers = listOf(
+ MemberUIData("1", "Alice", "test url1"),
+ MemberUIData("2", "Bob", "test url2")
+ )
+ viewModel.groupName.emit(testGroupName)
+ viewModel.updateGroupMemberList(testMembers)
+ advanceUntilIdle()
+
+ // when
+ viewModel.sendInviteMessageAll(testGroupId)
+ advanceUntilIdle()
+
+ // then
+ for (member in testMembers) {
+ coVerify { mockSendInviteMessageUseCase(testGroupId, testGroupName, member.uuid) }
+ }
+ }
+}
diff --git a/ui/expenselist/src/test/java/com/kappzzang/jeongsan/expenselist/sendmessage/SendMessageViewModelTest.kt b/ui/expenselist/src/test/java/com/kappzzang/jeongsan/expenselist/sendmessage/SendMessageViewModelTest.kt
new file mode 100644
index 00000000..d7d79a1b
--- /dev/null
+++ b/ui/expenselist/src/test/java/com/kappzzang/jeongsan/expenselist/sendmessage/SendMessageViewModelTest.kt
@@ -0,0 +1,79 @@
+package com.kappzzang.jeongsan.expenselist.sendmessage
+
+import com.kappzzang.jeongsan.model.TransferDetailItem
+import com.kappzzang.jeongsan.usecase.GetTransferInfoUseCase
+import com.kappzzang.jeongsan.usecase.SendTransferMessageUseCase
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class SendMessageViewModelTest {
+
+ private val mockGetTransferInfoUseCase = mockk()
+ private val mockSendTransferMessageUseCase = mockk()
+ private lateinit var viewModel: SendMessageViewModel
+
+ private val testDispatcher = StandardTestDispatcher(TestCoroutineScheduler())
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+ coEvery { mockGetTransferInfoUseCase() } returns emptyList()
+ coEvery { mockSendTransferMessageUseCase(any()) } returns true
+ viewModel = SendMessageViewModel(mockGetTransferInfoUseCase, mockSendTransferMessageUseCase)
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `송금 정보가 올바르게 받아서 합계를 잘 계산하는지 확인`() = runTest {
+ // given
+ val expectedTransferInfo = listOf(
+ TransferDetailItem("1", "test item 1", 2024, "test image url 1"),
+ TransferDetailItem("2", "test item 2", 11, "test image url 2"),
+ TransferDetailItem("3", "test item 3", 1, "test image url 3")
+ )
+ coEvery { mockGetTransferInfoUseCase() } returns expectedTransferInfo
+
+ // when
+ viewModel = SendMessageViewModel(mockGetTransferInfoUseCase, mockSendTransferMessageUseCase)
+ advanceUntilIdle()
+
+ // then
+ val expectedTotalPrice = 2036
+ assertEquals(expectedTransferInfo, viewModel.transferInfo.value)
+ assertEquals(expectedTotalPrice, viewModel.totalPrice.value)
+ }
+
+ @Test
+ fun `sendTransferMessage가 성공적으로 호출되는지 확인`() = runTest {
+ // given
+ val transferInfo = viewModel.transferInfo.value
+
+ // when
+ val result = viewModel.sendTransferMessage()
+ advanceUntilIdle()
+
+ // then
+ assertEquals(true, result)
+ coVerify { mockSendTransferMessageUseCase(transferInfo) }
+ }
+}
diff --git a/ui/main/src/main/java/com/kappzzang/jeongsan/main/MainPageViewModel.kt b/ui/main/src/main/java/com/kappzzang/jeongsan/main/MainPageViewModel.kt
index c712163f..15ec8b90 100644
--- a/ui/main/src/main/java/com/kappzzang/jeongsan/main/MainPageViewModel.kt
+++ b/ui/main/src/main/java/com/kappzzang/jeongsan/main/MainPageViewModel.kt
@@ -9,7 +9,7 @@ import com.kappzzang.jeongsan.usecase.GetProgressingGroupUseCase
import com.kappzzang.jeongsan.usecase.GetUserInfoUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
@@ -19,7 +19,8 @@ import kotlinx.coroutines.withContext
class MainPageViewModel @Inject constructor(
private val getProgressingGroupUseCase: GetProgressingGroupUseCase,
private val getDoneGroupUseCase: GetDoneGroupUseCase,
- private val getUserInfoUseCase: GetUserInfoUseCase
+ private val getUserInfoUseCase: GetUserInfoUseCase,
+ private val ioDispatcher: CoroutineDispatcher
) : ViewModel() {
private val _userName = MutableStateFlow("")
@@ -36,7 +37,7 @@ class MainPageViewModel @Inject constructor(
loadGroupList()
}
- private fun loadUserInfo() {
+ fun loadUserInfo() {
viewModelScope.launch {
val userInfo = getUserInfoUseCase()
_userName.value = userInfo?.name ?: "알 수 없음"
@@ -46,7 +47,7 @@ class MainPageViewModel @Inject constructor(
fun loadGroupList() {
viewModelScope.launch {
- withContext(Dispatchers.IO) {
+ withContext(ioDispatcher) {
val resultGroupList = mutableListOf()
val progressingGroupList = getProgressingGroupUseCase()
diff --git a/ui/main/src/test/java/com/kappzzang/jeongsan/main/MainPageViewModelTest.kt b/ui/main/src/test/java/com/kappzzang/jeongsan/main/MainPageViewModelTest.kt
new file mode 100644
index 00000000..52d9b35b
--- /dev/null
+++ b/ui/main/src/test/java/com/kappzzang/jeongsan/main/MainPageViewModelTest.kt
@@ -0,0 +1,155 @@
+package com.kappzzang.jeongsan.main
+
+import com.kappzzang.jeongsan.data.GroupViewItem
+import com.kappzzang.jeongsan.model.GroupItem
+import com.kappzzang.jeongsan.usecase.GetDoneGroupUseCase
+import com.kappzzang.jeongsan.usecase.GetProgressingGroupUseCase
+import com.kappzzang.jeongsan.usecase.GetUserInfoUseCase
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class MainPageViewModelTest {
+
+ private val mockGetProgressingGroupUseCase = mockk()
+ private val mockGetDoneGroupUseCase = mockk()
+ private val mockGetUserInfoUseCase = mockk()
+ private lateinit var viewModel: MainPageViewModel
+
+ private val testDispatcher = StandardTestDispatcher()
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(testDispatcher)
+
+ coEvery { mockGetProgressingGroupUseCase() } returns emptyList()
+ coEvery { mockGetDoneGroupUseCase() } returns emptyList()
+ coEvery { mockGetUserInfoUseCase() } returns null
+ viewModel = MainPageViewModel(
+ mockGetProgressingGroupUseCase,
+ mockGetDoneGroupUseCase,
+ mockGetUserInfoUseCase,
+ testDispatcher
+ )
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `loadUserInfo가 정상적으로 정보를 가져오는지 확인`() = runTest {
+ // Given
+ val testUserName = "Test User"
+ val testProfileUrl = "http://this.is.test.profile.url"
+ coEvery { mockGetUserInfoUseCase() } returns mockk {
+ every { name } returns testUserName
+ every { profileUrl } returns testProfileUrl
+ }
+
+ // When
+ viewModel.loadUserInfo()
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(testUserName, viewModel.userName.value)
+ assertEquals(testProfileUrl, viewModel.userProfileUrl.value)
+ }
+
+ @Test
+ fun `진행중인 그룹만 존재하는 경우 loadGroupList 함수 테스트`() = runTest {
+ // Given
+ val testProgressingGroupList = listOf(mockk(), mockk())
+ val testDoneGroupList = emptyList()
+ coEvery { mockGetProgressingGroupUseCase() } returns testProgressingGroupList
+ coEvery { mockGetDoneGroupUseCase() } returns testDoneGroupList
+
+ // When
+ viewModel.loadGroupList()
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(testProgressingGroupList.size + 1, viewModel.groupList.value.size)
+ assertEquals(GroupViewItem.ProgressTitle, viewModel.groupList.value[0])
+ for (i in testProgressingGroupList.indices) {
+ assertEquals(
+ GroupViewItem.Group(testProgressingGroupList[i]),
+ viewModel.groupList.value[i + 1]
+ )
+ }
+ }
+
+ @Test
+ fun `완료된 그룹만 존재하는 경우 loadGroupList 함수 테스트`() = runTest {
+ // Given
+ val testProgressingGroupList = emptyList()
+ val testDoneGroupList = listOf(mockk(), mockk(), mockk())
+ coEvery { mockGetProgressingGroupUseCase() } returns testProgressingGroupList
+ coEvery { mockGetDoneGroupUseCase() } returns testDoneGroupList
+
+ // When
+ viewModel.loadGroupList()
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(testDoneGroupList.size + 1, viewModel.groupList.value.size)
+ assertEquals(GroupViewItem.DoneTitle, viewModel.groupList.value[0])
+ for (i in testDoneGroupList.indices) {
+ assertEquals(
+ GroupViewItem.Group(testDoneGroupList[i]),
+ viewModel.groupList.value[i + 1]
+ )
+ }
+ }
+
+ @Test
+ fun `진행중인 그룹과 완료된 그룹이 모두 존재하는 경우 loadGroupList 함수 테스트`() = runTest {
+ // Given
+ val testProgressingGroupList = listOf(mockk(), mockk())
+ val testDoneGroupList = listOf(mockk(), mockk(), mockk())
+ coEvery { mockGetProgressingGroupUseCase() } returns testProgressingGroupList
+ coEvery { mockGetDoneGroupUseCase() } returns testDoneGroupList
+
+ // When
+ viewModel.loadGroupList()
+ advanceUntilIdle()
+
+ // Then
+ assertEquals(
+ testProgressingGroupList.size + testDoneGroupList.size + 2,
+ viewModel.groupList.value.size
+ )
+ assertEquals(GroupViewItem.ProgressTitle, viewModel.groupList.value[0])
+ for (i in testProgressingGroupList.indices) {
+ assertEquals(
+ GroupViewItem.Group(testProgressingGroupList[i]),
+ viewModel.groupList.value[i + 1]
+ )
+ }
+ assertEquals(
+ GroupViewItem.DoneTitle,
+ viewModel.groupList.value[testProgressingGroupList.size + 1]
+ )
+ for (i in testDoneGroupList.indices) {
+ assertEquals(
+ GroupViewItem.Group(testDoneGroupList[i]),
+ viewModel.groupList.value[testProgressingGroupList.size + i + 2]
+ )
+ }
+ }
+}
From a2a87c50e2ad90443aec95bea31c2b99b7a7fa5e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EA=B6=8C=EC=84=B1=EC=B0=AC?=
<54511614+ksc1008@users.noreply.github.com>
Date: Fri, 1 Nov 2024 22:37:48 +0900
Subject: [PATCH 4/4] =?UTF-8?q?Feat:=20Expense=20=EB=AA=A8=EB=93=88=20API?=
=?UTF-8?q?=20=ED=86=B5=ED=95=A9=20(#80)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Feat: SaveExpense 추가
* Feat: SaveExpense Response값 추가
* Style: API Path 내 파라미터 명 수정
* Style: 파일 명 수정
* Feat: GetExpenseDetails 추가
* Refactor: ExpenseList 하위 패키지 추가
* Refactor: ExpenseItem을 포함하는 ExpenseItemWithCategory, ExpenseItemWithDetails 클래스 생성
* Refactor: 합성한 super 클래스를 private로 수정
* Feat: ExpenseListRemoteDatasource 구현
* Refactor: ExpenseDetailEntity 새로운 패키지로 이동
* Feat: ExpenseDetailRemoteDatasource 구현
* Feat: getSavedJwt 인터페이스 작성
* Feat: ExpenseListRepository 구현
* Chore: Lint 수행
* Fix: ReceiptItem의 LocalDateTime 기본값 추가
---
data/build.gradle.kts | 1 +
data/expense/build.gradle.kts | 5 +
.../jeongsan/api/ReceiptRetrofitService.kt | 50 +++++++++
.../ExpenseDetailRemoteDatasource.kt | 16 +++
.../datasource/ExpenseListFakeDatasource.kt | 3 +-
.../datasource/ExpenseListRemoteDatasource.kt | 103 ++++++++++++++++++
.../jeongsan/datasource/expense/ExpenseDao.kt | 14 +--
.../datasource/expense/ExpenseDatabase.kt | 8 +-
.../jeongsan/entity/ExpenseItemEntity.kt | 14 +++
.../kappzzang/jeongsan/entity/ImageEntity.kt | 16 +++
.../entity/ResponseWithExpenseIdDTO.kt | 10 ++
.../jeongsan/entity/SaveExpensePayloadDTO.kt | 18 +++
.../expensedetail/ExpenseDetailEntity.kt | 14 +++
.../expensedetail/ExpenseDetailItemEntity.kt | 18 +++
.../entity/expenselist/CategoryEntity.kt | 12 ++
.../expenselist/ExpenseListResponseDTO.kt | 14 +++
.../entity/expenselist/ExpenseRemoteEntity.kt | 20 ++++
.../ExpenseRoomEntity.kt} | 4 +-
.../jeongsan/mapper/ExpenseEntityMapper.kt | 97 +++++++++++++++--
.../ExpenseFakeRepositoryImpl.kt | 25 +++--
.../ExpenseListRepositoryImpl.kt | 88 +++++++++++++++
...Test.kt => ExpenseRoomEntityMapperTest.kt} | 6 +-
.../ServerAuthenticationRepository.kt | 2 +
.../kappzzang/jeongsan/model/ExpenseItem.kt | 21 +---
.../jeongsan/model/ExpenseItemWithCategory.kt | 26 +++++
.../jeongsan/model/ExpenseItemWithDetails.kt | 25 +++++
.../jeongsan/model/ExpenseListResponse.kt | 2 +-
.../kappzzang/jeongsan/model/ReceiptItem.kt | 5 +-
.../jeongsan/repository/ExpenseRepository.kt | 4 +-
.../kappzzang/jeongsan/data/ExpenseUiItem.kt | 2 -
.../expensedetail/ExpenseDetailViewModel.kt | 6 +-
.../expenselist/util/ExpenseUiItemMapper.kt | 38 +++----
32 files changed, 600 insertions(+), 87 deletions(-)
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/api/ReceiptRetrofitService.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseDetailRemoteDatasource.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListRemoteDatasource.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/ExpenseItemEntity.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/ImageEntity.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/ResponseWithExpenseIdDTO.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/SaveExpensePayloadDTO.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailEntity.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailItemEntity.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/CategoryEntity.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseListResponseDTO.kt
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseRemoteEntity.kt
rename data/expense/src/main/java/com/kappzzang/jeongsan/entity/{ExpenseEntity.kt => expenselist/ExpenseRoomEntity.kt} (93%)
create mode 100644 data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseListRepositoryImpl.kt
rename data/expense/src/test/java/com/kappzzang/jeongsan/{ExpenseEntityMapperTest.kt => ExpenseRoomEntityMapperTest.kt} (86%)
create mode 100644 domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithCategory.kt
create mode 100644 domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithDetails.kt
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
index a17dc1b4..ab496e6d 100644
--- a/data/build.gradle.kts
+++ b/data/build.gradle.kts
@@ -41,5 +41,6 @@ subprojects {
testImplementation("androidx.arch.core:core-testing:2.2.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.48.1")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
}
}
diff --git a/data/expense/build.gradle.kts b/data/expense/build.gradle.kts
index 6a12d57f..e8926c8a 100644
--- a/data/expense/build.gradle.kts
+++ b/data/expense/build.gradle.kts
@@ -1,3 +1,7 @@
+plugins {
+ kotlin("plugin.serialization") version "1.9.0"
+}
+
android {
namespace = "com.kappzzang.jeongsan.expense"
}
@@ -5,5 +9,6 @@ dependencies {
implementation(project(":domain:expense"))
implementation("androidx.room:room-ktx:2.6.1")
implementation("com.kakao.sdk:v2-talk:2.20.6")
+ implementation(project(":domain:common-user"))
testImplementation("androidx.room:room-testing:2.6.1")
}
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/api/ReceiptRetrofitService.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/api/ReceiptRetrofitService.kt
new file mode 100644
index 00000000..47da1b99
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/api/ReceiptRetrofitService.kt
@@ -0,0 +1,50 @@
+package com.kappzzang.jeongsan.api
+
+import com.kappzzang.jeongsan.entity.ResponseWithExpenseIdDTO
+import com.kappzzang.jeongsan.entity.SaveExpensePayloadDTO
+import com.kappzzang.jeongsan.entity.expensedetail.ExpenseDetailEntity
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseListResponseDTO
+import retrofit2.Response
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.Header
+import retrofit2.http.POST
+import retrofit2.http.Path
+import retrofit2.http.Query
+
+interface ReceiptRetrofitService {
+ @POST("/api/receipts/{teamId}")
+ suspend fun saveExpense(
+ @Path(value = "teamId") groupId: String,
+ @Header("accessToken") jwt: String,
+ @Body body: SaveExpensePayloadDTO
+ ): Response
+
+ @GET("/api/receipts/items/{expenseId}")
+ suspend fun getExpenseDetails(
+ @Path(value = "expenseId") expenseId: String,
+ @Header("accessToken") jwt: String
+ ): Response
+
+ @POST("/api/expenses/personal/{teamId}/{expenseId}")
+ suspend fun updateExpenseDetails(
+ @Path(value = "teamId") groupId: String,
+ @Path(value = "expenseId") expenseId: String,
+ @Header("accessToken") jwt: String
+ )
+
+ @GET("/api/expenses/{teamId}")
+ suspend fun getExpenseList(
+ @Path(value = "teamId") groupId: String,
+ @Header("accessToken") jwt: String,
+ @Query("state") state: String,
+ @Query("isChecked") checked: Boolean
+ ): Response
+
+ @GET("/api/expenses/{teamId}")
+ suspend fun getExpenseList(
+ @Path(value = "teamId") groupId: String,
+ @Header("accessToken") jwt: String,
+ @Query("state") state: String
+ ): Response
+}
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseDetailRemoteDatasource.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseDetailRemoteDatasource.kt
new file mode 100644
index 00000000..72095635
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseDetailRemoteDatasource.kt
@@ -0,0 +1,16 @@
+package com.kappzzang.jeongsan.datasource
+
+import com.kappzzang.jeongsan.api.ReceiptRetrofitService
+import com.kappzzang.jeongsan.entity.expensedetail.ExpenseDetailEntity
+import javax.inject.Inject
+import retrofit2.Response
+
+class ExpenseDetailRemoteDatasource @Inject constructor(
+ private val receiptRetrofitService: ReceiptRetrofitService
+) {
+ suspend fun getExpenseDetail(expenseId: String, jwt: String): Response =
+ receiptRetrofitService.getExpenseDetails(
+ expenseId = expenseId,
+ jwt = jwt
+ )
+}
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListFakeDatasource.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListFakeDatasource.kt
index 415138eb..45ced2cf 100644
--- a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListFakeDatasource.kt
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListFakeDatasource.kt
@@ -1,6 +1,7 @@
package com.kappzzang.jeongsan.datasource
import com.kappzzang.jeongsan.datasource.expense.ExpenseDatabase
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseRoomEntity
import com.kappzzang.jeongsan.mapper.ExpenseEntityMapper
import com.kappzzang.jeongsan.model.ExpenseListResponse
import com.kappzzang.jeongsan.model.ExpenseState
@@ -40,7 +41,7 @@ class ExpenseListFakeDatasource @Inject constructor(private val expenseDatabase:
}
fun addExpense(receiptItem: ReceiptItem): String {
- val expenseEntity = com.kappzzang.jeongsan.entity.ExpenseEntity(
+ val expenseEntity = ExpenseRoomEntity(
name = receiptItem.title,
totalPrice = receiptItem.expenseDetailItemList.sumOf { it.itemPrice * it.itemQuantity },
createdTime = Timestamp(System.currentTimeMillis()).toString(),
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListRemoteDatasource.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListRemoteDatasource.kt
new file mode 100644
index 00000000..c018eb97
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListRemoteDatasource.kt
@@ -0,0 +1,103 @@
+package com.kappzzang.jeongsan.datasource
+
+import com.kappzzang.jeongsan.api.ReceiptRetrofitService
+import com.kappzzang.jeongsan.entity.ImageEntity
+import com.kappzzang.jeongsan.entity.ResponseWithExpenseIdDTO
+import com.kappzzang.jeongsan.entity.SaveExpensePayloadDTO
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseListResponseDTO
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseRoomEntity
+import com.kappzzang.jeongsan.mapper.ExpenseEntityMapper
+import com.kappzzang.jeongsan.model.ExpenseState
+import com.kappzzang.jeongsan.model.ReceiptItem
+import com.kappzzang.jeongsan.util.DateConverter.formatToTransferString
+import java.sql.Timestamp
+import javax.inject.Inject
+import retrofit2.Response
+
+class ExpenseListRemoteDatasource @Inject constructor(
+ private val receiptRetrofitService: ReceiptRetrofitService
+) {
+
+ private fun mapExpenseStateToDtoState(state: ExpenseState): String = when (state) {
+ ExpenseState.CONFIRMED -> "ongoing"
+ ExpenseState.NOT_CONFIRMED -> "ongoing"
+ ExpenseState.TRANSFER_PENDING -> "pending"
+ ExpenseState.TRANSFERED -> "completed"
+ }
+
+ private fun checkNeedAdditionalQuery(state: ExpenseState): Boolean =
+ (state == ExpenseState.CONFIRMED || state == ExpenseState.NOT_CONFIRMED)
+
+ private fun checkIsChecked(state: ExpenseState): Boolean = state == ExpenseState.CONFIRMED
+
+ suspend fun getExpenseList(
+ expenseState: ExpenseState,
+ jwt: String,
+ groupId: String
+ ): Response {
+ val needAdditionalQuery = checkNeedAdditionalQuery(expenseState)
+ val result: Response = if (needAdditionalQuery) {
+ receiptRetrofitService.getExpenseList(
+ groupId = groupId,
+ jwt = jwt,
+ state = mapExpenseStateToDtoState(expenseState),
+ checked = checkIsChecked(expenseState)
+ )
+ } else {
+ receiptRetrofitService.getExpenseList(
+ groupId = groupId,
+ jwt = jwt,
+ state = mapExpenseStateToDtoState(expenseState)
+ )
+ }
+
+ return result
+ }
+
+ suspend fun addExpense(
+ receiptItem: ReceiptItem,
+ jwt: String,
+ groupId: String
+ ): Response {
+ val postBody = SaveExpensePayloadDTO(
+ title = receiptItem.title,
+ items = receiptItem.expenseDetailItemList.map {
+ ExpenseEntityMapper.mapReceiptDetailItemToExpenseItemEntity(it)
+ },
+ paymentTime = receiptItem.paymentTime.formatToTransferString(),
+ image = ImageEntity(
+ name = "",
+ data = receiptItem.imageBase64 ?: "",
+ url = "",
+ format = IMAGE_FORMAT
+ ),
+ categoryId = CATEGORY_ID
+ )
+
+ val result = receiptRetrofitService.saveExpense(
+ groupId = groupId,
+ jwt = jwt,
+ body = postBody
+ )
+
+ return result
+ }
+
+ fun addExpense(receiptItem: ReceiptItem): String {
+ val expenseEntity = ExpenseRoomEntity(
+ name = receiptItem.title,
+ totalPrice = receiptItem.expenseDetailItemList.sumOf { it.itemPrice * it.itemQuantity },
+ createdTime = Timestamp(System.currentTimeMillis()).toString(),
+ categoryColor = receiptItem.categoryColor,
+ expenseState = ExpenseState.CONFIRMED.ordinal
+ )
+
+ // expenseDatabase.expenseDao().addExpense(expenseEntity)
+ return expenseEntity.id.toString()
+ }
+
+ companion object {
+ const val IMAGE_FORMAT = "JPG"
+ const val CATEGORY_ID = 0L
+ }
+}
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/expense/ExpenseDao.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/expense/ExpenseDao.kt
index c6f04673..c81ef164 100644
--- a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/expense/ExpenseDao.kt
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/expense/ExpenseDao.kt
@@ -4,25 +4,25 @@ import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
-import com.kappzzang.jeongsan.entity.ExpenseEntity
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseRoomEntity
@Dao
interface ExpenseDao {
@Insert
- fun addExpense(expenseEntity: ExpenseEntity)
+ fun addExpense(expenseEntity: ExpenseRoomEntity)
@Delete
- fun deleteExpense(expenseEntity: ExpenseEntity)
+ fun deleteExpense(expenseEntity: ExpenseRoomEntity)
@Query("SELECT * FROM `${ExpenseContract.ExpenseEntity.TABLE_NAME}` WHERE state = 0")
- fun getConfirmedExpense(): List
+ fun getConfirmedExpense(): List
@Query("SELECT * FROM `${ExpenseContract.ExpenseEntity.TABLE_NAME}` WHERE state = 1")
- fun getNotConfirmedExpense(): List
+ fun getNotConfirmedExpense(): List
@Query("SELECT * FROM `${ExpenseContract.ExpenseEntity.TABLE_NAME}` WHERE state = 2")
- fun getPendingExpense(): List
+ fun getPendingExpense(): List
@Query("SELECT * FROM `${ExpenseContract.ExpenseEntity.TABLE_NAME}` WHERE state = 3")
- fun getTransferredExpense(): List
+ fun getTransferredExpense(): List
}
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/expense/ExpenseDatabase.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/expense/ExpenseDatabase.kt
index 5cef9386..04cdd55c 100644
--- a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/expense/ExpenseDatabase.kt
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/expense/ExpenseDatabase.kt
@@ -5,7 +5,7 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
-import com.kappzzang.jeongsan.entity.ExpenseEntity
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseRoomEntity
import com.kappzzang.jeongsan.model.ExpenseState
import java.sql.Timestamp
import java.util.Date
@@ -17,11 +17,11 @@ private val colorList =
listOf("#87A2FF", "#FFD7C4", "#87A2FF", "#987D9A", "#987D9A", "#BB9AB1", "#BB9AB1")
private val categoryNameList = listOf("커피", "편의점", "커피", "영화관", "영화관", "식당", "식당")
-private fun makeFakeItemWithState(expenseState: ExpenseState, id: Int): ExpenseEntity {
+private fun makeFakeItemWithState(expenseState: ExpenseState, id: Int): ExpenseRoomEntity {
val adjustedIndex =
(id + 1) * (ExpenseState.entries.indexOf(expenseState) + 1)
- return ExpenseEntity(
+ return ExpenseRoomEntity(
name = nameList[adjustedIndex % nameList.size],
totalPrice = 1200 * adjustedIndex,
image = "",
@@ -34,7 +34,7 @@ private fun makeFakeItemWithState(expenseState: ExpenseState, id: Int): ExpenseE
)
}
-@Database(entities = [ExpenseEntity::class], version = 1)
+@Database(entities = [ExpenseRoomEntity::class], version = 1)
abstract class ExpenseDatabase : RoomDatabase() {
abstract fun expenseDao(): ExpenseDao
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ExpenseItemEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ExpenseItemEntity.kt
new file mode 100644
index 00000000..57c87ee7
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ExpenseItemEntity.kt
@@ -0,0 +1,14 @@
+package com.kappzzang.jeongsan.entity
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ExpenseItemEntity(
+ @SerializedName("name")
+ val name: String,
+ @SerializedName("quantity")
+ val quantity: Int,
+ @SerializedName("unit_price")
+ val unitPrice: Int
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ImageEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ImageEntity.kt
new file mode 100644
index 00000000..4996ea85
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ImageEntity.kt
@@ -0,0 +1,16 @@
+package com.kappzzang.jeongsan.entity
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ImageEntity(
+ @SerializedName("format")
+ val format: String,
+ @SerializedName("url")
+ val url: String,
+ @SerializedName("data")
+ val data: String,
+ @SerializedName("name")
+ val name: String
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ResponseWithExpenseIdDTO.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ResponseWithExpenseIdDTO.kt
new file mode 100644
index 00000000..a5db1a09
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ResponseWithExpenseIdDTO.kt
@@ -0,0 +1,10 @@
+package com.kappzzang.jeongsan.entity
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ResponseWithExpenseIdDTO(
+ @SerializedName("expense_id")
+ val expenseId: String
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/SaveExpensePayloadDTO.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/SaveExpensePayloadDTO.kt
new file mode 100644
index 00000000..990d6054
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/SaveExpensePayloadDTO.kt
@@ -0,0 +1,18 @@
+package com.kappzzang.jeongsan.entity
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SaveExpensePayloadDTO(
+ @SerializedName("title")
+ val title: String,
+ @SerializedName("payment_time")
+ val paymentTime: String,
+ @SerializedName("category_id")
+ val categoryId: Long,
+ @SerializedName("image")
+ val image: ImageEntity,
+ @SerializedName("items")
+ val items: List
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailEntity.kt
new file mode 100644
index 00000000..deb13d0d
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailEntity.kt
@@ -0,0 +1,14 @@
+package com.kappzzang.jeongsan.entity.expensedetail
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ExpenseDetailEntity(
+ @SerializedName("title")
+ val title: String,
+ @SerializedName("imageUrl")
+ val imageUrl: String,
+ @SerializedName("items")
+ val detailItems: List
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailItemEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailItemEntity.kt
new file mode 100644
index 00000000..07969837
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailItemEntity.kt
@@ -0,0 +1,18 @@
+package com.kappzzang.jeongsan.entity.expensedetail
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ExpenseDetailItemEntity(
+ @SerializedName("id")
+ val id: Long,
+ @SerializedName("name")
+ val name: String,
+ @SerializedName("quantity")
+ val quantity: Int,
+ @SerializedName("unitPrice")
+ val unitPrice: Int,
+ @SerializedName("consumedQuantity")
+ val quantityConsumed: Int
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/CategoryEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/CategoryEntity.kt
new file mode 100644
index 00000000..059eca53
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/CategoryEntity.kt
@@ -0,0 +1,12 @@
+package com.kappzzang.jeongsan.entity.expenselist
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class CategoryEntity(
+ @SerializedName("name")
+ val name: String,
+ @SerializedName("color")
+ val color: String
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseListResponseDTO.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseListResponseDTO.kt
new file mode 100644
index 00000000..c736c121
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseListResponseDTO.kt
@@ -0,0 +1,14 @@
+package com.kappzzang.jeongsan.entity.expenselist
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ExpenseListResponseDTO(
+ @SerializedName("expenseList")
+ val expenseList: List,
+ @SerializedName("checked")
+ val checked: Boolean,
+ @SerializedName("totalPrice")
+ val totalPrice: Long
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseRemoteEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseRemoteEntity.kt
new file mode 100644
index 00000000..70f45415
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseRemoteEntity.kt
@@ -0,0 +1,20 @@
+package com.kappzzang.jeongsan.entity.expenselist
+
+import com.google.gson.annotations.SerializedName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ExpenseRemoteEntity(
+ @SerializedName("expenseId")
+ val id: Long,
+ @SerializedName("title")
+ val title: String,
+ @SerializedName("totalPrice")
+ val totalPrice: Int,
+ @SerializedName("createdAt")
+ val createdAt: String,
+ @SerializedName("state")
+ val state: String,
+ @SerializedName("category")
+ val category: CategoryEntity
+)
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ExpenseEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseRoomEntity.kt
similarity index 93%
rename from data/expense/src/main/java/com/kappzzang/jeongsan/entity/ExpenseEntity.kt
rename to data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseRoomEntity.kt
index e2f40309..a21d51e0 100644
--- a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/ExpenseEntity.kt
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expenselist/ExpenseRoomEntity.kt
@@ -1,4 +1,4 @@
-package com.kappzzang.jeongsan.entity
+package com.kappzzang.jeongsan.entity.expenselist
import androidx.room.ColumnInfo
import androidx.room.Entity
@@ -6,7 +6,7 @@ import androidx.room.PrimaryKey
import com.kappzzang.jeongsan.datasource.expense.ExpenseContract
@Entity(tableName = ExpenseContract.ExpenseEntity.TABLE_NAME)
-class ExpenseEntity(
+class ExpenseRoomEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = ExpenseContract.ExpenseEntity.COLUMN_ID) var id: Long = 0,
@ColumnInfo(name = ExpenseContract.ExpenseEntity.COLUMN_NAME) var name: String = "",
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/mapper/ExpenseEntityMapper.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/mapper/ExpenseEntityMapper.kt
index 4ac72253..ca188a1c 100644
--- a/data/expense/src/main/java/com/kappzzang/jeongsan/mapper/ExpenseEntityMapper.kt
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/mapper/ExpenseEntityMapper.kt
@@ -1,20 +1,95 @@
package com.kappzzang.jeongsan.mapper
-import com.kappzzang.jeongsan.entity.ExpenseEntity
+import com.kappzzang.jeongsan.entity.ExpenseItemEntity
+import com.kappzzang.jeongsan.entity.expensedetail.ExpenseDetailEntity
+import com.kappzzang.jeongsan.entity.expensedetail.ExpenseDetailItemEntity
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseRemoteEntity
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseRoomEntity
+import com.kappzzang.jeongsan.model.ExpenseDetailItem
import com.kappzzang.jeongsan.model.ExpenseItem
+import com.kappzzang.jeongsan.model.ExpenseItemWithCategory
+import com.kappzzang.jeongsan.model.ExpenseItemWithDetails
import com.kappzzang.jeongsan.model.ExpenseState
+import com.kappzzang.jeongsan.model.ReceiptDetailItem
import com.kappzzang.jeongsan.util.DateConverter
object ExpenseEntityMapper {
- fun mapExpenseEntityToModel(entity: ExpenseEntity): ExpenseItem = ExpenseItem(
- id = entity.id.toString(),
- name = entity.name,
- categoryColor = entity.categoryColor,
- price = entity.totalPrice,
- expenseImageUrl = entity.image,
- date = DateConverter.parseFromString(entity.createdTime),
- payerMemberId = "",
- payerName = "",
- state = ExpenseState.entries[entity.expenseState]
+ fun mapExpenseEntityToModel(entity: ExpenseRoomEntity): ExpenseItemWithCategory =
+ ExpenseItemWithCategory(
+ item = ExpenseItem(
+ id = entity.id.toString(),
+ name = entity.name,
+ price = entity.totalPrice,
+ state = ExpenseState.entries[entity.expenseState]
+ ),
+ date = DateConverter.parseFromString(entity.createdTime),
+ categoryColor = entity.categoryColor
+ )
+
+ fun mapExpenseEntityToModel(
+ entity: ExpenseRemoteEntity,
+ checked: Boolean = false
+ ): ExpenseItemWithCategory = ExpenseItemWithCategory(
+ item = ExpenseItem(
+ id = entity.id.toString(),
+ name = entity.title,
+ price = entity.totalPrice,
+ state = mapExpenseStateToDomainState(entity.state, checked)
+ ),
+ date = DateConverter.parseFromString(entity.createdAt),
+ categoryColor = entity.category.color
+ )
+
+ fun mapDetailedExpenseEntityToModel(
+ entity: ExpenseDetailEntity,
+ expenseId: String,
+ state: ExpenseState
+ ): ExpenseItemWithDetails = ExpenseItemWithDetails(
+ item = ExpenseItem(
+ id = expenseId,
+ name = entity.title,
+ price = getSumOfAllDetailItems(entity.detailItems),
+ state = state
+ ),
+ expenseImageUrl = entity.imageUrl,
+ expenseDetails = entity.detailItems.map {
+ mapExpenseDetailEntityToModel(it)
+ }
)
+
+ fun mapExpenseDetailEntityToModel(entity: ExpenseDetailItemEntity): ExpenseDetailItem =
+ ExpenseDetailItem(
+ selectedQuantity = entity.quantityConsumed,
+ itemQuantity = entity.quantity,
+ id = entity.id.toString(),
+ itemPrice = entity.unitPrice,
+ itemName = entity.name
+ )
+
+ fun mapReceiptDetailItemToExpenseItemEntity(model: ReceiptDetailItem): ExpenseItemEntity =
+ ExpenseItemEntity(
+ name = model.itemName,
+ quantity = model.itemQuantity,
+ unitPrice = model.itemPrice
+ )
+
+ private fun getSumOfAllDetailItems(detailItems: List): Int =
+ detailItems.sumOf {
+ it.unitPrice * it.quantity
+ }
+
+ private fun mapExpenseStateToDomainState(state: String, checked: Boolean): ExpenseState {
+ val trimmed = state.lowercase().trim()
+ return when (trimmed) {
+ "pending" -> ExpenseState.TRANSFER_PENDING
+ "completed" -> ExpenseState.TRANSFERED
+ "ongoing" -> {
+ if (checked) ExpenseState.CONFIRMED else ExpenseState.NOT_CONFIRMED
+ }
+
+ else -> {
+ throw IllegalStateException("Invalid Expense State")
+ }
+ }
+ }
}
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseFakeRepositoryImpl.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseFakeRepositoryImpl.kt
index bc2ae736..4d0a073e 100644
--- a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseFakeRepositoryImpl.kt
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseFakeRepositoryImpl.kt
@@ -1,17 +1,16 @@
package com.kappzzang.jeongsan.repositoryimpl
import com.kappzzang.jeongsan.datasource.ExpenseListFakeDatasource
+import com.kappzzang.jeongsan.model.ExpenseDetailItem
import com.kappzzang.jeongsan.model.ExpenseItem
+import com.kappzzang.jeongsan.model.ExpenseItemWithDetails
import com.kappzzang.jeongsan.model.ExpenseListResponse
import com.kappzzang.jeongsan.model.ExpenseState
import com.kappzzang.jeongsan.repository.ExpenseRepository
-import java.time.LocalDateTime
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
-data class ExpenseListCachingKey(val expenseState: ExpenseState, val groupId: String)
-
class ExpenseFakeRepositoryImpl @Inject constructor(
private val dataSource: ExpenseListFakeDatasource
) : ExpenseRepository {
@@ -39,18 +38,20 @@ class ExpenseFakeRepositoryImpl @Inject constructor(
)
}
- override suspend fun getExpense(id: Long) = ExpenseItem(
- id = "id",
- name = "지출 이름입니당",
- payerName = "돈 많은 사람",
- payerMemberId = "id",
- price = 15800,
+ override suspend fun getExpense(id: Long) = ExpenseItemWithDetails(
+ item = ExpenseItem(
+ id = id.toString(),
+ state = ExpenseState.NOT_CONFIRMED,
+ name = "지출 이름입니당",
+ price = 15800
+ ),
// 임시 지출 이미지 주소 (카카오테크 캠퍼스)
expenseImageUrl = "https://www.kakaotechcampus.com/fileUpDownload/" +
"download.do?p_savefile=gatepage_20230330053504999_1.png&p_realfile=" +
"GNB+%EB%A1%9C%EA%B3%A0%28%EB%B3%B4%EB%9D%BC%29.png",
- date = LocalDateTime.of(2024, 10, 14, 12, 30, 15, 0),
- state = ExpenseState.NOT_CONFIRMED,
- categoryColor = "#FFFFFF"
+ expenseDetails = listOf(
+ ExpenseDetailItem("", "1", 200, 1, 0),
+ ExpenseDetailItem("", "2", 300, 4, 1)
+ )
)
}
diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseListRepositoryImpl.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseListRepositoryImpl.kt
new file mode 100644
index 00000000..b27f1582
--- /dev/null
+++ b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseListRepositoryImpl.kt
@@ -0,0 +1,88 @@
+package com.kappzzang.jeongsan.repositoryimpl
+
+import com.kappzzang.jeongsan.datasource.ExpenseListRemoteDatasource
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseListResponseDTO
+import com.kappzzang.jeongsan.mapper.ExpenseEntityMapper
+import com.kappzzang.jeongsan.model.ExpenseDetailItem
+import com.kappzzang.jeongsan.model.ExpenseItem
+import com.kappzzang.jeongsan.model.ExpenseItemWithDetails
+import com.kappzzang.jeongsan.model.ExpenseListResponse
+import com.kappzzang.jeongsan.model.ExpenseState
+import com.kappzzang.jeongsan.repository.ExpenseRepository
+import com.kappzzang.jeongsan.repository.ServerAuthenticationRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+
+data class ExpenseListCachingKey(val expenseState: ExpenseState, val groupId: String)
+
+class ExpenseListRepositoryImpl @Inject constructor(
+ private val dataSource: ExpenseListRemoteDatasource,
+ private val auth: ServerAuthenticationRepository
+) : ExpenseRepository {
+
+ private val cachedData = HashMap()
+
+ private fun getJwt(): String = auth.getSavedJwt()
+ private fun isJwtValid(jwt: String): Boolean = (jwt != "")
+
+ private fun mapResponseBody(body: ExpenseListResponseDTO): ExpenseListResponse {
+ val expenses = body.expenseList.map { ExpenseEntityMapper.mapExpenseEntityToModel(it) }
+ return ExpenseListResponse(
+ totalExpenseToSend = body.totalPrice.toInt(),
+ expenseList = expenses,
+ totalPrice = body.totalPrice.toInt()
+ )
+ }
+
+ override fun getExpenseList(
+ groupId: String,
+ expenseState: ExpenseState
+ ): Flow = flow {
+ emit(
+ cachedData.getOrDefault(
+ ExpenseListCachingKey(expenseState, groupId),
+ ExpenseListResponse.emptyList()
+ )
+ )
+
+ val jwt = getJwt()
+ if (!isJwtValid(jwt)) {
+ cachedData[ExpenseListCachingKey(expenseState, groupId)] =
+ ExpenseListResponse.emptyList()
+ } else {
+ val response = dataSource.getExpenseList(
+ expenseState,
+ groupId = groupId,
+ jwt = jwt
+ )
+
+ response.body()?.let {
+ cachedData[ExpenseListCachingKey(expenseState, groupId)] = mapResponseBody(it)
+ }
+ }
+ emit(
+ cachedData.getOrDefault(
+ ExpenseListCachingKey(expenseState, groupId),
+ ExpenseListResponse.emptyList()
+ )
+ )
+ }
+
+ override suspend fun getExpense(id: Long) = ExpenseItemWithDetails(
+ item = ExpenseItem(
+ id = id.toString(),
+ state = ExpenseState.NOT_CONFIRMED,
+ name = "지출 이름입니당",
+ price = 15800
+ ),
+ // 임시 지출 이미지 주소 (카카오테크 캠퍼스)
+ expenseImageUrl = "https://www.kakaotechcampus.com/fileUpDownload/" +
+ "download.do?p_savefile=gatepage_20230330053504999_1.png&p_realfile=" +
+ "GNB+%EB%A1%9C%EA%B3%A0%28%EB%B3%B4%EB%9D%BC%29.png",
+ expenseDetails = listOf(
+ ExpenseDetailItem("", "1", 200, 1, 0),
+ ExpenseDetailItem("", "2", 300, 4, 1)
+ )
+ )
+}
diff --git a/data/expense/src/test/java/com/kappzzang/jeongsan/ExpenseEntityMapperTest.kt b/data/expense/src/test/java/com/kappzzang/jeongsan/ExpenseRoomEntityMapperTest.kt
similarity index 86%
rename from data/expense/src/test/java/com/kappzzang/jeongsan/ExpenseEntityMapperTest.kt
rename to data/expense/src/test/java/com/kappzzang/jeongsan/ExpenseRoomEntityMapperTest.kt
index 0ca70d18..0df761cf 100644
--- a/data/expense/src/test/java/com/kappzzang/jeongsan/ExpenseEntityMapperTest.kt
+++ b/data/expense/src/test/java/com/kappzzang/jeongsan/ExpenseRoomEntityMapperTest.kt
@@ -1,12 +1,12 @@
package com.kappzzang.jeongsan
-import com.kappzzang.jeongsan.entity.ExpenseEntity
+import com.kappzzang.jeongsan.entity.expenselist.ExpenseRoomEntity
import com.kappzzang.jeongsan.mapper.ExpenseEntityMapper
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
-class ExpenseEntityMapperTest {
- private fun getSampleEntity() = ExpenseEntity(
+class ExpenseRoomEntityMapperTest {
+ private fun getSampleEntity() = ExpenseRoomEntity(
id = 100,
name = "name",
expenseState = 0,
diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/ServerAuthenticationRepository.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/ServerAuthenticationRepository.kt
index 8acc1dec..f0c88add 100644
--- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/ServerAuthenticationRepository.kt
+++ b/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/ServerAuthenticationRepository.kt
@@ -6,4 +6,6 @@ interface ServerAuthenticationRepository {
fun registerToServer(authData: AuthData)
fun getJwtFromServer(authData: AuthData): AuthData
+
+ fun getSavedJwt(): String
}
diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItem.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItem.kt
index 54de5fcc..cffcfb16 100644
--- a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItem.kt
+++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItem.kt
@@ -1,31 +1,14 @@
package com.kappzzang.jeongsan.model
-import java.time.LocalDateTime
-
enum class ExpenseState { CONFIRMED, NOT_CONFIRMED, TRANSFER_PENDING, TRANSFERED }
-data class ExpenseItem(
- val id: String,
- val name: String,
- val payerName: String,
- val payerMemberId: String,
- val price: Int,
- val expenseImageUrl: String,
- val date: LocalDateTime,
- val state: ExpenseState,
- val categoryColor: String
-) {
+data class ExpenseItem(val id: String, val name: String, val price: Int, val state: ExpenseState) {
companion object {
val EMPTY = ExpenseItem(
id = "",
name = "",
- payerName = "",
- payerMemberId = "",
price = 0,
- expenseImageUrl = "",
- date = LocalDateTime.now(),
- state = ExpenseState.NOT_CONFIRMED,
- categoryColor = ""
+ state = ExpenseState.NOT_CONFIRMED
)
}
}
diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithCategory.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithCategory.kt
new file mode 100644
index 00000000..cd569359
--- /dev/null
+++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithCategory.kt
@@ -0,0 +1,26 @@
+package com.kappzzang.jeongsan.model
+
+import java.time.LocalDateTime
+
+data class ExpenseItemWithCategory(
+ private val item: ExpenseItem,
+ val categoryColor: String,
+ val date: LocalDateTime
+) {
+ val id
+ get() = item.id
+ val name
+ get() = item.name
+ val price
+ get() = item.price
+ val state
+ get() = item.state
+
+ companion object {
+ val EMPTY = ExpenseItemWithCategory(
+ item = ExpenseItem.EMPTY,
+ categoryColor = "",
+ date = LocalDateTime.now()
+ )
+ }
+}
diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithDetails.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithDetails.kt
new file mode 100644
index 00000000..e9895f5b
--- /dev/null
+++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithDetails.kt
@@ -0,0 +1,25 @@
+package com.kappzzang.jeongsan.model
+
+data class ExpenseItemWithDetails(
+ private val item: ExpenseItem,
+ val expenseImageUrl: String,
+ val expenseDetails: List
+
+) {
+ val id
+ get() = item.id
+ val name
+ get() = item.name
+ val price
+ get() = item.price
+ val state
+ get() = item.state
+
+ companion object {
+ val EMPTY = ExpenseItemWithDetails(
+ ExpenseItem.EMPTY,
+ "",
+ emptyList()
+ )
+ }
+}
diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseListResponse.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseListResponse.kt
index 5d2efda0..cf2b65ba 100644
--- a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseListResponse.kt
+++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseListResponse.kt
@@ -3,7 +3,7 @@ package com.kappzzang.jeongsan.model
data class ExpenseListResponse(
val totalPrice: Int,
val totalExpenseToSend: Int,
- val expenseList: List
+ val expenseList: List
) {
companion object {
fun emptyList() = ExpenseListResponse(0, 0, listOf())
diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ReceiptItem.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ReceiptItem.kt
index d139e20b..fad3b92b 100644
--- a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ReceiptItem.kt
+++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ReceiptItem.kt
@@ -1,8 +1,11 @@
package com.kappzzang.jeongsan.model
+import java.time.LocalDateTime
+
data class ReceiptItem(
val title: String,
val categoryColor: String,
val imageBase64: String?,
- val expenseDetailItemList: List
+ val expenseDetailItemList: List,
+ val paymentTime: LocalDateTime = LocalDateTime.now()
)
diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ExpenseRepository.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ExpenseRepository.kt
index ca9f7671..c8abf0b3 100644
--- a/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ExpenseRepository.kt
+++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ExpenseRepository.kt
@@ -1,6 +1,6 @@
package com.kappzzang.jeongsan.repository
-import com.kappzzang.jeongsan.model.ExpenseItem
+import com.kappzzang.jeongsan.model.ExpenseItemWithDetails
import com.kappzzang.jeongsan.model.ExpenseListResponse
import com.kappzzang.jeongsan.model.ExpenseState
import kotlinx.coroutines.flow.Flow
@@ -14,5 +14,5 @@ interface ExpenseRepository {
* @return 지출 목록 response flow
*/
fun getExpenseList(groupId: String, expenseState: ExpenseState): Flow
- suspend fun getExpense(id: Long): ExpenseItem
+ suspend fun getExpense(id: Long): ExpenseItemWithDetails
}
diff --git a/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseUiItem.kt b/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseUiItem.kt
index ea4df29d..573f2fae 100644
--- a/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseUiItem.kt
+++ b/ui/data/src/main/java/com/kappzzang/jeongsan/data/ExpenseUiItem.kt
@@ -7,8 +7,6 @@ data class ExpenseUiItem(
val name: String,
val isFirstItem: Boolean,
val isLastItem: Boolean,
- val payerName: String,
- val payerMemberId: String,
val price: String,
val date: LocalDateTime,
val categoryColor: String
diff --git a/ui/expensedetail/src/main/java/com/kappzzang/jeongsan/expensedetail/ExpenseDetailViewModel.kt b/ui/expensedetail/src/main/java/com/kappzzang/jeongsan/expensedetail/ExpenseDetailViewModel.kt
index 9d9ef464..508c2500 100644
--- a/ui/expensedetail/src/main/java/com/kappzzang/jeongsan/expensedetail/ExpenseDetailViewModel.kt
+++ b/ui/expensedetail/src/main/java/com/kappzzang/jeongsan/expensedetail/ExpenseDetailViewModel.kt
@@ -4,7 +4,7 @@ import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kappzzang.jeongsan.model.ExpenseDetailItem
-import com.kappzzang.jeongsan.model.ExpenseItem
+import com.kappzzang.jeongsan.model.ExpenseItemWithDetails
import com.kappzzang.jeongsan.usecase.EditExpenseDetailUseCase
import com.kappzzang.jeongsan.usecase.GetExpenseDetailUseCase
import com.kappzzang.jeongsan.usecase.GetExpenseUseCase
@@ -24,10 +24,10 @@ class ExpenseDetailViewModel @Inject constructor(
) : ViewModel() {
private val _expenseDetailList =
MutableStateFlow(listOf())
- private val _expense = MutableStateFlow(ExpenseItem.EMPTY)
+ private val _expense = MutableStateFlow(ExpenseItemWithDetails.EMPTY)
val expenseDetailList: StateFlow> = _expenseDetailList.asStateFlow()
- val expense: StateFlow = _expense.asStateFlow()
+ val expense: StateFlow = _expense.asStateFlow()
init {
initExpense()
diff --git a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/util/ExpenseUiItemMapper.kt b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/util/ExpenseUiItemMapper.kt
index 38b08d84..1b8b6c69 100644
--- a/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/util/ExpenseUiItemMapper.kt
+++ b/ui/expenselist/src/main/java/com/kappzzang/jeongsan/expenselist/util/ExpenseUiItemMapper.kt
@@ -2,27 +2,27 @@ package com.kappzzang.jeongsan.expenselist.util
import com.kappzzang.jeongsan.data.ExpenseUiItem
import com.kappzzang.jeongsan.expenselist.viewmodel.ExpenseListPageViewModel.Companion.CURRENCY_POSTFIX
-import com.kappzzang.jeongsan.model.ExpenseItem
+import com.kappzzang.jeongsan.model.ExpenseItemWithCategory
import com.kappzzang.jeongsan.util.IntegerFormatter.formatDecimalSeparator
object ExpenseUiItemMapper {
- internal fun mapToExpenseUiItem(expenseItemList: List): List =
- expenseItemList.mapIndexed { index, item ->
- ExpenseUiItem(
- isFirstItem = index == 0,
- isLastItem = index == expenseItemList.size - 1,
- id = item.id,
- name = item.name,
- date = item.date,
- payerMemberId = item.payerMemberId,
- categoryColor = item.categoryColor,
- payerName = item.payerName,
- price = "${item.price.formatDecimalSeparator()} $CURRENCY_POSTFIX"
- )
- }
+ internal fun mapToExpenseUiItem(
+ expenseItemList: List
+ ): List = expenseItemList.mapIndexed { index, item ->
+ ExpenseUiItem(
+ isFirstItem = index == 0,
+ isLastItem = index == expenseItemList.size - 1,
+ id = item.id,
+ name = item.name,
+ date = item.date,
+ categoryColor = item.categoryColor,
+ price = "${item.price.formatDecimalSeparator()} $CURRENCY_POSTFIX"
+ )
+ }
- internal fun sortItemsByTime(expenseItemList: List): List =
- expenseItemList.sortedBy {
- it.date
- }.reversed()
+ internal fun sortItemsByTime(
+ expenseItemList: List
+ ): List = expenseItemList.sortedByDescending {
+ it.date
+ }
}