diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f6b20e1d..5d24da75 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -48,7 +48,7 @@ android { } dependencies { - implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation("androidx.security:security-crypto-ktx:1.1.0-alpha06") implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("com.google.android.material:material:1.12.0") implementation("androidx.activity:activity:1.9.2") @@ -63,6 +63,7 @@ dependencies { implementation(project(":ui:camera")) implementation(project(":ui:addexpense")) implementation(project(":ui:data")) + implementation(project(":ui:sendmessage")) implementation(project(":domain:common-user")) implementation(project(":domain:group")) implementation(project(":domain:expense")) diff --git a/app/src/main/java/com/kappzzang/jeongsan/JeongsanApplication.kt b/app/src/main/java/com/kappzzang/jeongsan/JeongsanApplication.kt index 0260cbcc..a63dd5e3 100644 --- a/app/src/main/java/com/kappzzang/jeongsan/JeongsanApplication.kt +++ b/app/src/main/java/com/kappzzang/jeongsan/JeongsanApplication.kt @@ -21,6 +21,6 @@ class JeongsanApplication : Application() { } companion object { - const val DATASTORE_NAME = "JeongsanDatastore" + const val ENCRYPTED_SHARED_PREFERENCES_NAME = "JeongsanEncryptedSharedPreferences" } } diff --git a/app/src/main/java/com/kappzzang/jeongsan/StartActivity.kt b/app/src/main/java/com/kappzzang/jeongsan/StartActivity.kt index 9296bb34..9505e425 100644 --- a/app/src/main/java/com/kappzzang/jeongsan/StartActivity.kt +++ b/app/src/main/java/com/kappzzang/jeongsan/StartActivity.kt @@ -6,14 +6,14 @@ import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import com.kappzzang.jeongsan.intentcontract.StartContract -import com.kappzzang.jeongsan.navigation.AppNavigator +import com.kappzzang.jeongsan.navigation.LoginNavigator import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class StartActivity : AppCompatActivity() { @Inject - lateinit var appNavigator: AppNavigator + lateinit var appNavigator: LoginNavigator override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -36,15 +36,14 @@ class StartActivity : AppCompatActivity() { // 초대링크를 클릭해서 옴 inviteGroup != null -> { Log.d(TAG, inviteGroup) - appNavigator.navigateToLogin(this).also { - it.data = Uri.parse(inviteGroup) + appNavigator.loginAndEnterGroup(this, Uri.parse(inviteGroup)).also { startActivity(it) finish() } } // 그냥 옴 else -> { - appNavigator.navigateToLogin(this).also { + appNavigator.login(this).also { startActivity(it) finish() } diff --git a/app/src/main/java/com/kappzzang/jeongsan/di/DatastoreModule.kt b/app/src/main/java/com/kappzzang/jeongsan/di/DatastoreModule.kt deleted file mode 100644 index 4d0be908..00000000 --- a/app/src/main/java/com/kappzzang/jeongsan/di/DatastoreModule.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.kappzzang.jeongsan.di - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStore -import com.kappzzang.jeongsan.JeongsanApplication.Companion.DATASTORE_NAME -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object DatastoreModule { - private val Context.dataStore: DataStore - by preferencesDataStore(name = DATASTORE_NAME) - - @Provides - @Singleton - fun provideDatastore(@ApplicationContext context: Context): DataStore = - context.dataStore -} diff --git a/app/src/main/java/com/kappzzang/jeongsan/di/EncryptedSharedPreferencesModule.kt b/app/src/main/java/com/kappzzang/jeongsan/di/EncryptedSharedPreferencesModule.kt new file mode 100644 index 00000000..231a5c62 --- /dev/null +++ b/app/src/main/java/com/kappzzang/jeongsan/di/EncryptedSharedPreferencesModule.kt @@ -0,0 +1,34 @@ +package com.kappzzang.jeongsan.di + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.kappzzang.jeongsan.JeongsanApplication.Companion.ENCRYPTED_SHARED_PREFERENCES_NAME +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object EncryptedSharedPreferencesModule { + + @Provides + @Singleton + fun provideEncryptedSharedPreferences(@ApplicationContext context: Context): SharedPreferences { + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + return EncryptedSharedPreferences.create( + context, + ENCRYPTED_SHARED_PREFERENCES_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } +} diff --git a/app/src/main/java/com/kappzzang/jeongsan/di/NavigatorModule.kt b/app/src/main/java/com/kappzzang/jeongsan/di/NavigatorModule.kt deleted file mode 100644 index ff718a6e..00000000 --- a/app/src/main/java/com/kappzzang/jeongsan/di/NavigatorModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.kappzzang.jeongsan.di - -import com.kappzzang.jeongsan.navigation.AppNavigator -import com.kappzzang.jeongsan.navigation.NavigatorImpl -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -abstract class NavigatorModule { - - @Binds - @Singleton - abstract fun bindAppNavigator(appNavigatorImpl: NavigatorImpl): AppNavigator -} diff --git a/app/src/main/java/com/kappzzang/jeongsan/di/RepositoryModule.kt b/app/src/main/java/com/kappzzang/jeongsan/di/RepositoryModule.kt deleted file mode 100644 index aff33692..00000000 --- a/app/src/main/java/com/kappzzang/jeongsan/di/RepositoryModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.kappzzang.jeongsan.di - -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -abstract class RepositoryModule diff --git a/app/src/main/java/com/kappzzang/jeongsan/di/RoomModule.kt b/app/src/main/java/com/kappzzang/jeongsan/di/RoomModule.kt deleted file mode 100644 index 872c680d..00000000 --- a/app/src/main/java/com/kappzzang/jeongsan/di/RoomModule.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.kappzzang.jeongsan.di - -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -object RoomModule diff --git a/app/src/main/java/com/kappzzang/jeongsan/di/UseCaseModule.kt b/app/src/main/java/com/kappzzang/jeongsan/di/UseCaseModule.kt deleted file mode 100644 index c720989a..00000000 --- a/app/src/main/java/com/kappzzang/jeongsan/di/UseCaseModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.kappzzang.jeongsan.di - -import com.kappzzang.jeongsan.repository.TransferRepository -import com.kappzzang.jeongsan.repository.UserInfoRepository -import com.kappzzang.jeongsan.usecase.GetTransferInfoUseCase -import com.kappzzang.jeongsan.usecase.SendTransferMessageUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -object UseCaseModule { - @Provides - fun provideGetTransferInfoUseCase(transferRepository: TransferRepository) = - GetTransferInfoUseCase(transferRepository) - - @Provides - fun provideSendTransferMessageUseCase( - userInfoRepository: UserInfoRepository, - transferRepository: TransferRepository - ) = SendTransferMessageUseCase(userInfoRepository, transferRepository) -} diff --git a/app/src/main/java/com/kappzzang/jeongsan/navigation/NavigatorImpl.kt b/app/src/main/java/com/kappzzang/jeongsan/navigation/NavigatorImpl.kt deleted file mode 100644 index e5a3d2bf..00000000 --- a/app/src/main/java/com/kappzzang/jeongsan/navigation/NavigatorImpl.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.kappzzang.jeongsan.navigation - -import android.content.Context -import android.content.Intent -import com.kappzzang.jeongsan.addexpense.AddExpenseActivity -import com.kappzzang.jeongsan.camera.ReceiptCameraActivity -import com.kappzzang.jeongsan.creategroup.CreateGroupActivity -import com.kappzzang.jeongsan.expensedetail.ExpenseDetailActivity -import com.kappzzang.jeongsan.expenselist.ExpenseListActivity -import com.kappzzang.jeongsan.expenselist.sendcomplete.SendCompleteActivity -import com.kappzzang.jeongsan.login.LoginActivity -import com.kappzzang.jeongsan.main.MainActivity -import javax.inject.Inject - -class NavigatorImpl @Inject constructor() : AppNavigator { - override fun navigateToMainPage(packageContext: Context): Intent = - Intent(packageContext, MainActivity::class.java) - - override fun navigateToCreateGroup(packageContext: Context): Intent = - Intent(packageContext, CreateGroupActivity::class.java) - - override fun navigateToExpenseDetail(packageContext: Context): Intent = - Intent(packageContext, ExpenseDetailActivity::class.java) - - override fun navigateToExpenseList(packageContext: Context): Intent = - Intent(packageContext, ExpenseListActivity::class.java) - - override fun navigateToCamera(packageContext: Context): Intent = - Intent(packageContext, ReceiptCameraActivity::class.java) - - override fun navigateToLogin(packageContext: Context): Intent = - Intent(packageContext, LoginActivity::class.java) - - override fun navigateToAddExpense(packageContext: Context): Intent = - Intent(packageContext, AddExpenseActivity::class.java) - - override fun navigateToSendComplete(packageContext: Context): Intent = - Intent(packageContext, SendCompleteActivity::class.java) -} diff --git a/common/androidutil/src/main/java/com/kappzzang/jeongsan/data/AuthData.kt b/common/androidutil/src/main/java/com/kappzzang/jeongsan/data/KakaoAuthData.kt similarity index 59% rename from common/androidutil/src/main/java/com/kappzzang/jeongsan/data/AuthData.kt rename to common/androidutil/src/main/java/com/kappzzang/jeongsan/data/KakaoAuthData.kt index 72a1608b..4fa954b3 100644 --- a/common/androidutil/src/main/java/com/kappzzang/jeongsan/data/AuthData.kt +++ b/common/androidutil/src/main/java/com/kappzzang/jeongsan/data/KakaoAuthData.kt @@ -1,8 +1,7 @@ package com.kappzzang.jeongsan.data -data class AuthData( +data class KakaoAuthData( val kakaoAccessToken: String, val accessTokenExpirationTime: Long, - val kakaoRefreshToken: String, - val jwt: String? + val kakaoRefreshToken: String ) diff --git a/common/androidutil/src/main/java/com/kappzzang/jeongsan/data/ServerAuthData.kt b/common/androidutil/src/main/java/com/kappzzang/jeongsan/data/ServerAuthData.kt new file mode 100644 index 00000000..17a643c7 --- /dev/null +++ b/common/androidutil/src/main/java/com/kappzzang/jeongsan/data/ServerAuthData.kt @@ -0,0 +1,3 @@ +package com.kappzzang.jeongsan.data + +data class ServerAuthData(val accessToken: String, val refreshToken: String) diff --git a/common/androidutil/src/main/java/com/kappzzang/jeongsan/util/AuthenticationRepository.kt b/common/androidutil/src/main/java/com/kappzzang/jeongsan/util/AuthenticationRepository.kt index a570f7a7..42cb724c 100644 --- a/common/androidutil/src/main/java/com/kappzzang/jeongsan/util/AuthenticationRepository.kt +++ b/common/androidutil/src/main/java/com/kappzzang/jeongsan/util/AuthenticationRepository.kt @@ -1,12 +1,20 @@ package com.kappzzang.jeongsan.util -import com.kappzzang.jeongsan.data.AuthData -import kotlinx.coroutines.flow.Flow +import com.kappzzang.jeongsan.data.KakaoAuthData +import com.kappzzang.jeongsan.data.ServerAuthData interface AuthenticationRepository { - fun getAuthData(): Flow + fun getKakaoAuthData(): KakaoAuthData - suspend fun updateAuthData(newData: AuthData) + fun getServerAuthData(): ServerAuthData - suspend fun removeAuthData() + fun updateKakaoAuthData(newData: KakaoAuthData) + + fun updateServerAuthData(newData: ServerAuthData) + + fun removeKakaoAuthData() + + fun removeServerAuthData() + + suspend fun refreshJwtFromServer(authData: ServerAuthData): Result } diff --git a/common/dispatcher/.gitignore b/common/dispatcher/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/common/dispatcher/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/dispatcher/build.gradle.kts b/common/dispatcher/build.gradle.kts new file mode 100644 index 00000000..4aa9caa4 --- /dev/null +++ b/common/dispatcher/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("org.jlleitschuh.gradle.ktlint") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") +} + +android { + namespace = "com.kappzzang.jeongsan" +} + +dependencies { + implementation("com.google.dagger:hilt-android:2.48.1") + kapt("com.google.dagger:hilt-compiler:2.48.1") + implementation("androidx.core:core-ktx:1.13.1") +} diff --git a/common/dispatcher/proguard-rules.pro b/common/dispatcher/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/common/dispatcher/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/common/dispatcher/src/main/AndroidManifest.xml b/common/dispatcher/src/main/AndroidManifest.xml new file mode 100644 index 00000000..74b7379f --- /dev/null +++ b/common/dispatcher/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/java/com/kappzzang/jeongsan/di/DispatcherModule.kt b/common/dispatcher/src/main/java/com/kappzzang/jeongsan/dispatcher/DispatcherModule.kt similarity index 89% rename from app/src/main/java/com/kappzzang/jeongsan/di/DispatcherModule.kt rename to common/dispatcher/src/main/java/com/kappzzang/jeongsan/dispatcher/DispatcherModule.kt index 164c10b0..9892dfee 100644 --- a/app/src/main/java/com/kappzzang/jeongsan/di/DispatcherModule.kt +++ b/common/dispatcher/src/main/java/com/kappzzang/jeongsan/dispatcher/DispatcherModule.kt @@ -1,4 +1,4 @@ -package com.kappzzang.jeongsan.di +package com.kappzzang.jeongsan.dispatcher import dagger.Module import dagger.Provides diff --git a/common/navigation/build.gradle.kts b/common/navigation/build.gradle.kts index 5f595917..a3cb581e 100644 --- a/common/navigation/build.gradle.kts +++ b/common/navigation/build.gradle.kts @@ -18,4 +18,5 @@ 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:ocr")) } diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/AddExpenseNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/AddExpenseNavigator.kt new file mode 100644 index 00000000..424f83f7 --- /dev/null +++ b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/AddExpenseNavigator.kt @@ -0,0 +1,17 @@ +package com.kappzzang.jeongsan.navigation + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.kappzzang.jeongsan.model.OcrResultResponse + +interface AddExpenseNavigator { + fun navigateToAddExpenseWithImage( + packageContext: Context, + ocrResponse: OcrResultResponse.OcrSuccess, + image: Uri, + groupId: String + ): Intent + + fun navigateToAddExpenseManually(packageContext: Context, groupId: String): Intent +} diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/AppNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/AppNavigator.kt deleted file mode 100644 index 4976c703..00000000 --- a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/AppNavigator.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.kappzzang.jeongsan.navigation - -import android.content.Context -import android.content.Intent - -interface AppNavigator { - fun navigateToMainPage(packageContext: Context): Intent - - fun navigateToCreateGroup(packageContext: Context): Intent - - fun navigateToExpenseDetail(packageContext: Context): Intent - - fun navigateToExpenseList(packageContext: Context): Intent - - fun navigateToCamera(packageContext: Context): Intent - - fun navigateToLogin(packageContext: Context): Intent - - fun navigateToAddExpense(packageContext: Context): Intent - - fun navigateToSendComplete(packageContext: Context): Intent -} diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/CameraNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/CameraNavigator.kt new file mode 100644 index 00000000..9e341ef0 --- /dev/null +++ b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/CameraNavigator.kt @@ -0,0 +1,8 @@ +package com.kappzzang.jeongsan.navigation + +import android.content.Context +import android.content.Intent + +interface CameraNavigator { + fun navigateToCamera(packageContext: Context): Intent +} diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/CreateGroupNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/CreateGroupNavigator.kt new file mode 100644 index 00000000..ebe3f4eb --- /dev/null +++ b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/CreateGroupNavigator.kt @@ -0,0 +1,8 @@ +package com.kappzzang.jeongsan.navigation + +import android.content.Context +import android.content.Intent + +interface CreateGroupNavigator { + fun navigateToCreateGroup(packageContext: Context): Intent +} diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/ExpenseDetailNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/ExpenseDetailNavigator.kt new file mode 100644 index 00000000..d2d7b86b --- /dev/null +++ b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/ExpenseDetailNavigator.kt @@ -0,0 +1,13 @@ +package com.kappzzang.jeongsan.navigation + +import android.content.Context +import android.content.Intent + +interface ExpenseDetailNavigator { + fun navigateToExpenseDetail( + packageContext: Context, + expenseId: String, + groupId: String, + editable: Boolean + ): Intent +} diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/ExpenseListNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/ExpenseListNavigator.kt new file mode 100644 index 00000000..28071309 --- /dev/null +++ b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/ExpenseListNavigator.kt @@ -0,0 +1,18 @@ +package com.kappzzang.jeongsan.navigation + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.kappzzang.jeongsan.model.OcrResultResponse + +interface ExpenseListNavigator { + fun navigateToExpenseList(packageContext: Context, groupId: String): Intent + + fun getExpenseListCancelResult(packageContext: Context): Intent + + fun getExpenseListWithOcrDataResult( + packageContext: Context, + result: OcrResultResponse, + image: Uri + ): Intent +} diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/LoginNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/LoginNavigator.kt new file mode 100644 index 00000000..ab0d81a5 --- /dev/null +++ b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/LoginNavigator.kt @@ -0,0 +1,11 @@ +package com.kappzzang.jeongsan.navigation + +import android.content.Context +import android.content.Intent +import android.net.Uri + +interface LoginNavigator { + fun login(packageContext: Context): Intent + + fun loginAndEnterGroup(packageContext: Context, inviteGroup: Uri): Intent +} diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/MainPageNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/MainPageNavigator.kt new file mode 100644 index 00000000..e6e2b54b --- /dev/null +++ b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/MainPageNavigator.kt @@ -0,0 +1,11 @@ +package com.kappzzang.jeongsan.navigation + +import android.content.Context +import android.content.Intent +import android.net.Uri + +interface MainPageNavigator { + fun navigateToMainPage(packageContext: Context): Intent + + fun navigateToMainPageAndEnterGroup(packageContext: Context, inviteGroup: Uri): Intent +} diff --git a/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/SendMessageNavigator.kt b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/SendMessageNavigator.kt new file mode 100644 index 00000000..d7b16072 --- /dev/null +++ b/common/navigation/src/main/java/com/kappzzang/jeongsan/navigation/SendMessageNavigator.kt @@ -0,0 +1,9 @@ +package com.kappzzang.jeongsan.navigation + +import android.content.Context +import android.content.Intent + +interface SendMessageNavigator { + fun navigateToSendMessage(packageContext: Context, groupId: String): Intent + fun navigateToSendComplete(packageContext: Context, groupId: String): Intent +} diff --git a/common/retrofit/build.gradle.kts b/common/retrofit/build.gradle.kts index d6550c4d..79ea6144 100644 --- a/common/retrofit/build.gradle.kts +++ b/common/retrofit/build.gradle.kts @@ -12,6 +12,7 @@ android { dependencies { implementation("com.google.dagger:hilt-android:2.48.1") + implementation(project(":common:androidutil")) kapt("com.google.dagger:hilt-compiler:2.48.1") implementation("androidx.core:core-ktx:1.13.1") diff --git a/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/AuthInterceptor.kt b/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/AuthInterceptor.kt new file mode 100644 index 00000000..89417808 --- /dev/null +++ b/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/AuthInterceptor.kt @@ -0,0 +1,42 @@ +package com.kappzzang.jeongsan.retrofit + +import com.kappzzang.jeongsan.util.AuthenticationRepository +import javax.inject.Inject +import kotlinx.coroutines.runBlocking +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route + +class AuthInterceptor @Inject constructor(private val authRepository: AuthenticationRepository) : + Authenticator { + override fun authenticate(route: Route?, response: Response): Request? { + val originRequest = response.request() + if (originRequest.header(AUTH_HEADER_KEY).isNullOrEmpty()) { + return null + } + + // 기존의 authData + val authData = authRepository.getServerAuthData() + + // 서버에 새로운 토큰을 요청 + val newAuthData = runBlocking { + authRepository.refreshJwtFromServer(authData).getOrElse { + authRepository.removeServerAuthData() + return@runBlocking null + } + } ?: return null + + // 새로운 토큰을 저장 + authRepository.updateServerAuthData(newAuthData) + + return originRequest.newBuilder() + .removeHeader(AUTH_HEADER_KEY) + .addHeader(AUTH_HEADER_KEY, "Bearer ${newAuthData.accessToken}") + .build() + } + + companion object { + private const val AUTH_HEADER_KEY = "Authorization" + } +} diff --git a/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/HeaderInterceptor.kt b/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/HeaderInterceptor.kt new file mode 100644 index 00000000..3d6508c0 --- /dev/null +++ b/common/retrofit/src/main/java/com/kappzzang/jeongsan/retrofit/HeaderInterceptor.kt @@ -0,0 +1,33 @@ +package com.kappzzang.jeongsan.retrofit + +import com.kappzzang.jeongsan.util.AuthenticationRepository +import javax.inject.Inject +import okhttp3.Interceptor +import okhttp3.Response + +class HeaderInterceptor @Inject constructor(private val authRepository: AuthenticationRepository) : + Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + if (chain.request().headers()[SKIP_AUTH_KEY] == SKIP_AUTH_VALUE) { + val newRequest = chain.request().newBuilder() + .removeHeader(SKIP_AUTH_KEY) + .build() + return chain.proceed(newRequest) + } + + val serverAuthData = authRepository.getServerAuthData() + + val newRequest = chain.request().newBuilder() + .addHeader(AUTH_HEADER_KEY, "Bearer ${serverAuthData.accessToken}") + .build() + val response = chain.proceed(newRequest) + + return response + } + + companion object { + private const val AUTH_HEADER_KEY = "Authorization" + private const val SKIP_AUTH_KEY = "Auth" + private const val SKIP_AUTH_VALUE = "false" + } +} 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 fb0e133e..43838fcc 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,12 +1,14 @@ package com.kappzzang.jeongsan.retrofit import com.kappzzang.jeongsan.build_config.BuildConfig +import com.kappzzang.jeongsan.util.AuthenticationRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Qualifier import javax.inject.Singleton +import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -25,6 +27,10 @@ object RetrofitModule { @Retention(AnnotationRetention.BINARY) annotation class ServiceRetrofit + @Qualifier + @Retention(AnnotationRetention.BINARY) + annotation class ServiceAuthRetrofit + @Provides @Singleton @KakaoAuthRetrofit @@ -41,11 +47,40 @@ object RetrofitModule { .addConverterFactory(GsonConverterFactory.create()) .build() + @Provides + @Singleton + @ServiceAuthRetrofit + fun provideServiceAuthRetrofitBuilder(): Retrofit = Retrofit.Builder() + .baseUrl(BuildConfig.SERVICE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + @Provides @Singleton @ServiceRetrofit - fun provideServiceRetrofitBuilder(): Retrofit = Retrofit.Builder() + fun provideServiceRetrofitBuilder(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder() .baseUrl(BuildConfig.SERVICE_URL) .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build() + + @Provides + @Singleton + fun provideHeaderInterceptor(authRepository: AuthenticationRepository) = + HeaderInterceptor(authRepository) + + @Provides + @Singleton + fun provideAuthInterceptor(authRepository: AuthenticationRepository) = + AuthInterceptor(authRepository) + + @Provides + @Singleton + fun provideOkHttpClient( + headerInterceptor: HeaderInterceptor, + authInterceptor: AuthInterceptor + ): OkHttpClient = OkHttpClient.Builder() + .addInterceptor(headerInterceptor) + .authenticator(authInterceptor) .build() } diff --git a/data/build.gradle.kts b/data/build.gradle.kts index ab496e6d..50a07702 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -30,6 +30,8 @@ subprojects { implementation(project(":domain")) implementation(project(":domain:group")) implementation(project(":build-config")) + implementation("com.squareup.retrofit2:retrofit:2.11.0") + implementation("com.google.code.gson:gson:2.10.1") // Test Dependencies testImplementation("org.assertj:assertj-core:3.25.3") 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 index 47da1b99..50770bb3 100644 --- a/data/expense/src/main/java/com/kappzzang/jeongsan/api/ReceiptRetrofitService.kt +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/api/ReceiptRetrofitService.kt @@ -1,13 +1,14 @@ package com.kappzzang.jeongsan.api +import com.kappzzang.jeongsan.entity.GetCategoryListResponseDTO import com.kappzzang.jeongsan.entity.ResponseWithExpenseIdDTO import com.kappzzang.jeongsan.entity.SaveExpensePayloadDTO import com.kappzzang.jeongsan.entity.expensedetail.ExpenseDetailEntity +import com.kappzzang.jeongsan.entity.expensedetail.UpdateExpenseDetailPayloadDTO 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 @@ -16,35 +17,28 @@ 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 + suspend fun getExpenseDetail( + @Path(value = "expenseId") expenseId: String ): Response @POST("/api/expenses/personal/{teamId}/{expenseId}") - suspend fun updateExpenseDetails( + suspend fun updateExpenseDetail( @Path(value = "teamId") groupId: String, @Path(value = "expenseId") expenseId: String, - @Header("accessToken") jwt: String - ) + @Body body: UpdateExpenseDetailPayloadDTO + ): Response @GET("/api/expenses/{teamId}") suspend fun getExpenseList( @Path(value = "teamId") groupId: String, - @Header("accessToken") jwt: String, @Query("state") state: String, - @Query("isChecked") checked: Boolean + @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 + @GET("/api/expenses/categories") + suspend fun getCategoryColorList(): 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 index 72095635..5a21207f 100644 --- a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseDetailRemoteDatasource.kt +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseDetailRemoteDatasource.kt @@ -2,15 +2,69 @@ package com.kappzzang.jeongsan.datasource import com.kappzzang.jeongsan.api.ReceiptRetrofitService import com.kappzzang.jeongsan.entity.expensedetail.ExpenseDetailEntity +import com.kappzzang.jeongsan.entity.expensedetail.ExpenseDetailSelectionInfoEntity +import com.kappzzang.jeongsan.entity.expensedetail.UpdateExpenseDetailPayloadDTO +import com.kappzzang.jeongsan.model.ExpenseDetailItem 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 - ) + suspend fun getExpenseDetail(expenseId: String): Result { + val response = try { + receiptRetrofitService.getExpenseDetail( + expenseId = expenseId + ) + } catch (e: Exception) { + return (Result.failure(e)) + } + + return processResponseBody(response) + } + + suspend fun updateExpenseDetail( + expenseId: String, + groupId: String, + edited: List + ): Result { + val response = try { + val body = UpdateExpenseDetailPayloadDTO( + items = edited.map { + ExpenseDetailSelectionInfoEntity( + quantity = it.selectedQuantity, + itemId = it.id.toInt() + ) + } + ) + receiptRetrofitService.updateExpenseDetail( + expenseId = expenseId, + groupId = groupId, + body = body + ) + } catch (e: Exception) { + return (Result.failure(e)) + } + + return processResponseBody(response) + } + + private fun processResponseBody(response: Response): Result { + when (response.code()) { + 404 -> return Result.failure(IllegalStateException("존재하지 않는 지출")) + 500 -> return Result.failure(IllegalStateException(response.message())) + else -> { + return if (response.code() / 100 == 2) { + response.body()?.let { + return Result.success(it) + } + ?: Result.failure( + IllegalStateException("알 수 없는 오류 발생: ${response.message()}") + ) + } else { + Result.failure(IllegalStateException("알 수 없는 오류 발생: ${response.message()}")) + } + } + } + } } 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 45ced2cf..ed0b39b7 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 @@ -40,13 +40,20 @@ class ExpenseListFakeDatasource @Inject constructor(private val expenseDatabase: emit(fakeResponse) } + private fun getCategoryFromId(id: String): String { + val r = id.toIntOrNull() ?: 0 + val g = (r * 4 + 3) % 10 + val b = (g * 4 + 3) % 10 + return "#$r$r$g$g$b$b" + } + 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 + categoryColor = getCategoryFromId(receiptItem.categoryId), + expenseState = ExpenseState.NOT_CONFIRMED.ordinal ) expenseDatabase.expenseDao().addExpense(expenseEntity) 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 index c018eb97..e3733152 100644 --- a/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListRemoteDatasource.kt +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/datasource/ExpenseListRemoteDatasource.kt @@ -1,16 +1,15 @@ package com.kappzzang.jeongsan.datasource import com.kappzzang.jeongsan.api.ReceiptRetrofitService +import com.kappzzang.jeongsan.entity.GetCategoryListResponseDTO 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 @@ -25,75 +24,91 @@ class ExpenseListRemoteDatasource @Inject constructor( 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) { + ): Result { + val response = try { receiptRetrofitService.getExpenseList( groupId = groupId, - jwt = jwt, state = mapExpenseStateToDtoState(expenseState), checked = checkIsChecked(expenseState) ) - } else { - receiptRetrofitService.getExpenseList( - groupId = groupId, - jwt = jwt, - state = mapExpenseStateToDtoState(expenseState) - ) + } catch (e: Exception) { + return Result.failure(e) } - return result + return processResponseCode(response) + } + + private fun checkIsChecked(state: ExpenseState): Boolean? = when (state) { + ExpenseState.CONFIRMED -> true + ExpenseState.NOT_CONFIRMED -> false + else -> null } 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 - ) + ): Result { + val response = try { + 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 = receiptItem.categoryId.toLong() + ) - val result = receiptRetrofitService.saveExpense( - groupId = groupId, - jwt = jwt, - body = postBody - ) + receiptRetrofitService.saveExpense( + groupId = groupId, + body = postBody + ) + } catch (e: Exception) { + return Result.failure(e) + } + + return processResponseCode(response) + } + + suspend fun getCategoryList(): Result { + val response = try { + receiptRetrofitService.getCategoryColorList() + } catch (e: Exception) { + return Result.failure(e) + } - return result + return processResponseCode(response) } - 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 - ) + private fun processResponseCode(response: Response): Result { + when (response.code()) { + 400 -> throw IllegalArgumentException("유효하지 않는 입력 값") + 404 -> throw IllegalStateException(response.message()) + 500 -> throw IllegalStateException(response.message()) - // expenseDatabase.expenseDao().addExpense(expenseEntity) - return expenseEntity.id.toString() + else -> { + if (response.code() / 100 == 2) { + response.body()?.let { + return Result.success(it) + } + ?: return Result.failure( + IllegalStateException("알 수 없는 오류 발생: ${response.message()}") + ) + } else { + return Result.failure( + IllegalStateException("알 수 없는 오류 발생: ${response.message()}") + ) + } + } + } } companion object { diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/di/ExpenseRepositoryModule.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/di/ExpenseRepositoryModule.kt index 268a4cd4..2f2d6465 100644 --- a/data/expense/src/main/java/com/kappzzang/jeongsan/di/ExpenseRepositoryModule.kt +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/di/ExpenseRepositoryModule.kt @@ -2,11 +2,9 @@ package com.kappzzang.jeongsan.di import com.kappzzang.jeongsan.repository.ExpenseDetailRepository import com.kappzzang.jeongsan.repository.ExpenseRepository -import com.kappzzang.jeongsan.repository.ReceiptRepository import com.kappzzang.jeongsan.repository.TransferRepository -import com.kappzzang.jeongsan.repositoryimpl.ExpenseDetailRepositoryImpl +import com.kappzzang.jeongsan.repositoryimpl.ExpenseDetailFakeRepositoryImpl import com.kappzzang.jeongsan.repositoryimpl.ExpenseFakeRepositoryImpl -import com.kappzzang.jeongsan.repositoryimpl.ReceiptRepositoryImpl import com.kappzzang.jeongsan.repositoryimpl.TransferRepositoryImpl import dagger.Binds import dagger.Module @@ -26,15 +24,9 @@ abstract class ExpenseRepositoryModule { @Binds @Singleton abstract fun bindExpenseDetailRepository( - expenseDetailRepositoryImpl: ExpenseDetailRepositoryImpl + expenseDetailRepositoryImpl: ExpenseDetailFakeRepositoryImpl ): ExpenseDetailRepository - @Binds - @Singleton - abstract fun bindReceiptRepository( - receiptRepositoryImpl: ReceiptRepositoryImpl - ): ReceiptRepository - @Binds @Singleton abstract fun bindTransferRepository( diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/CategoryEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/CategoryEntity.kt new file mode 100644 index 00000000..71493181 --- /dev/null +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/CategoryEntity.kt @@ -0,0 +1,14 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class CategoryEntity( + @SerializedName("id") + val id: Int, + @SerializedName("name") + val name: String, + @SerializedName("color") + val color: String +) diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/GetCategoryListResponseDTO.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/GetCategoryListResponseDTO.kt new file mode 100644 index 00000000..8c1811e0 --- /dev/null +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/GetCategoryListResponseDTO.kt @@ -0,0 +1,10 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class GetCategoryListResponseDTO( + @SerializedName("categoryList") + val categoryList: List +) diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailSelectionInfoEntity.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailSelectionInfoEntity.kt new file mode 100644 index 00000000..cada0f16 --- /dev/null +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/ExpenseDetailSelectionInfoEntity.kt @@ -0,0 +1,12 @@ +package com.kappzzang.jeongsan.entity.expensedetail + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class ExpenseDetailSelectionInfoEntity( + @SerializedName("itemId") + val itemId: Int, + @SerializedName("quantity") + val quantity: Int +) diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/UpdateExpenseDetailPayloadDTO.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/UpdateExpenseDetailPayloadDTO.kt new file mode 100644 index 00000000..47d4dc84 --- /dev/null +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/entity/expensedetail/UpdateExpenseDetailPayloadDTO.kt @@ -0,0 +1,10 @@ +package com.kappzzang.jeongsan.entity.expensedetail + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateExpenseDetailPayloadDTO( + @SerializedName("items") + val items: List +) 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 index c736c121..5fc98898 100644 --- 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 @@ -7,8 +7,8 @@ import kotlinx.serialization.Serializable data class ExpenseListResponseDTO( @SerializedName("expenseList") val expenseList: List, - @SerializedName("checked") - val checked: Boolean, @SerializedName("totalPrice") - val totalPrice: Long + val totalPrice: Long, + @SerializedName("totalExpense") + val myTotalExpense: Int? = null ) 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 index 70f45415..f79b7845 100644 --- 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 @@ -9,6 +9,8 @@ data class ExpenseRemoteEntity( val id: Long, @SerializedName("title") val title: String, + @SerializedName("payerId") + val payerUuid: String, @SerializedName("totalPrice") val totalPrice: Int, @SerializedName("createdAt") @@ -16,5 +18,9 @@ data class ExpenseRemoteEntity( @SerializedName("state") val state: String, @SerializedName("category") - val category: CategoryEntity + val category: CategoryEntity, + @SerializedName("checked") + val checked: Boolean? = null, + @SerializedName("personalExpense") + val myExpense: Int? = null ) 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 ca188a1c..5c7270bc 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,10 +1,13 @@ package com.kappzzang.jeongsan.mapper +import com.kappzzang.jeongsan.entity.CategoryEntity import com.kappzzang.jeongsan.entity.ExpenseItemEntity +import com.kappzzang.jeongsan.entity.ResponseWithExpenseIdDTO 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.ExpenseCategory import com.kappzzang.jeongsan.model.ExpenseDetailItem import com.kappzzang.jeongsan.model.ExpenseItem import com.kappzzang.jeongsan.model.ExpenseItemWithCategory @@ -23,7 +26,8 @@ object ExpenseEntityMapper { state = ExpenseState.entries[entity.expenseState] ), date = DateConverter.parseFromString(entity.createdTime), - categoryColor = entity.categoryColor + categoryColor = entity.categoryColor, + payerUuid = "" ) fun mapExpenseEntityToModel( @@ -37,7 +41,8 @@ object ExpenseEntityMapper { state = mapExpenseStateToDomainState(entity.state, checked) ), date = DateConverter.parseFromString(entity.createdAt), - categoryColor = entity.category.color + categoryColor = entity.category.color, + payerUuid = entity.payerUuid ) fun mapDetailedExpenseEntityToModel( @@ -53,18 +58,19 @@ object ExpenseEntityMapper { ), expenseImageUrl = entity.imageUrl, expenseDetails = entity.detailItems.map { - mapExpenseDetailEntityToModel(it) + mapExpenseDetailItemEntityToModel(it) } ) - fun mapExpenseDetailEntityToModel(entity: ExpenseDetailItemEntity): ExpenseDetailItem = - ExpenseDetailItem( - selectedQuantity = entity.quantityConsumed, - itemQuantity = entity.quantity, - id = entity.id.toString(), - itemPrice = entity.unitPrice, - itemName = entity.name - ) + private fun mapExpenseDetailItemEntityToModel( + 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( @@ -92,4 +98,22 @@ object ExpenseEntityMapper { } } } + + fun mapResponseWithExpenseEntityToModel(entity: ResponseWithExpenseIdDTO): String = + entity.expenseId + + fun mapCategoryToModel(entity: CategoryEntity): ExpenseCategory { + val colorCode = + if (entity.color.startsWith("#")) { + entity.color + } else { + "#${entity.color}" + } + + return ExpenseCategory( + id = entity.id.toString(), + color = colorCode, + name = entity.name + ) + } } diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseDetailFakeRepositoryImpl.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseDetailFakeRepositoryImpl.kt new file mode 100644 index 00000000..a3132329 --- /dev/null +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseDetailFakeRepositoryImpl.kt @@ -0,0 +1,67 @@ +package com.kappzzang.jeongsan.repositoryimpl + +import com.kappzzang.jeongsan.model.ExpenseDetailItem +import com.kappzzang.jeongsan.model.ExpenseItem +import com.kappzzang.jeongsan.model.ExpenseItemWithDetails +import com.kappzzang.jeongsan.model.ExpenseState +import com.kappzzang.jeongsan.repository.ExpenseDetailRepository +import javax.inject.Inject + +class ExpenseDetailFakeRepositoryImpl @Inject constructor() : ExpenseDetailRepository { + override suspend fun getExpenseDetail(expenseId: String): Result { + val details = + listOf( + ExpenseDetailItem( + "id1", + "아주 맛있는 과자", + 4000, + 3, + 0 + ), + ExpenseDetailItem( + "id2", + "아주 맛없는 고기", + 50000, + 1, + 0 + ), + ExpenseDetailItem( + "id3", + "밍밍한 국", + 500, + 6, + 0 + ), + ExpenseDetailItem( + "id4", + "상차림비", + 100, + 10, + 0 + ) + ) + val expenseItemWithDetails = ExpenseItemWithDetails( + expenseImageUrl = FAKE_IMAGE_URL, + item = ExpenseItem( + id = "id", + state = ExpenseState.NOT_CONFIRMED, + name = "지출 이름입니당", + price = 15800 + ), + expenseDetails = details + ) + return Result.success(expenseItemWithDetails) + } + + override suspend fun saveExpenseDetail( + edited: List, + expenseId: String, + groupId: String + ): Result = Result.success(Unit) + + companion object { + const val FAKE_IMAGE_URL = "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" + } +} diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseDetailRepositoryImpl.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseDetailRepositoryImpl.kt index 775f57b3..0f139dd5 100644 --- a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseDetailRepositoryImpl.kt +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseDetailRepositoryImpl.kt @@ -1,42 +1,39 @@ package com.kappzzang.jeongsan.repositoryimpl +import com.kappzzang.jeongsan.datasource.ExpenseDetailRemoteDatasource +import com.kappzzang.jeongsan.entity.expensedetail.ExpenseDetailEntity +import com.kappzzang.jeongsan.mapper.ExpenseEntityMapper import com.kappzzang.jeongsan.model.ExpenseDetailItem +import com.kappzzang.jeongsan.model.ExpenseItemWithDetails +import com.kappzzang.jeongsan.model.ExpenseState import com.kappzzang.jeongsan.repository.ExpenseDetailRepository import javax.inject.Inject -class ExpenseDetailRepositoryImpl @Inject constructor() : ExpenseDetailRepository { - override suspend fun getExpenseDetail(): List = listOf( - ExpenseDetailItem( - "id1", - "아주 맛있는 과자", - 4000, - 3, - 0 - ), - ExpenseDetailItem( - "id2", - "아주 맛없는 고기", - 50000, - 1, - 0 - ), - ExpenseDetailItem( - "id3", - "밍밍한 국", - 500, - 6, - 0 - ), - ExpenseDetailItem( - "id4", - "상차림비", - 100, - 10, - 0 +class ExpenseDetailRepositoryImpl @Inject constructor( + private val expenseDetailRemoteDatasource: ExpenseDetailRemoteDatasource +) : ExpenseDetailRepository { + + override suspend fun getExpenseDetail(expenseId: String): Result = + expenseDetailRemoteDatasource.getExpenseDetail( + expenseId = expenseId + ).mapCatching { + mapResponseToExpenseDetail(it, expenseId) + } + + private fun mapResponseToExpenseDetail(entity: ExpenseDetailEntity, expenseId: String) = + ExpenseEntityMapper.mapDetailedExpenseEntityToModel( + entity, + expenseId, + ExpenseState.NOT_CONFIRMED ) - ) - override suspend fun saveExpenseDetail(edited: List) { - // 저장하기 - } + override suspend fun saveExpenseDetail( + edited: List, + expenseId: String, + groupId: String + ): Result = expenseDetailRemoteDatasource.updateExpenseDetail( + expenseId = expenseId, + groupId = groupId, + edited = edited + ) } 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 4d0a073e..5faecf2c 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,15 +1,15 @@ 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.ExpenseCategory import com.kappzzang.jeongsan.model.ExpenseListResponse import com.kappzzang.jeongsan.model.ExpenseState +import com.kappzzang.jeongsan.model.ReceiptItem import com.kappzzang.jeongsan.repository.ExpenseRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.last class ExpenseFakeRepositoryImpl @Inject constructor( private val dataSource: ExpenseListFakeDatasource @@ -17,41 +17,48 @@ class ExpenseFakeRepositoryImpl @Inject constructor( private val cachedData = HashMap() + private fun getFakeCategoryList(): List = listOf( + ExpenseCategory(id = "1", color = "#ff0000", name = "편의점"), + ExpenseCategory(id = "2", color = "#4f6622", name = "영화관"), + ExpenseCategory(id = "3", color = "#7777ff", name = "숙박"), + ExpenseCategory(id = "4", color = "#999999", name = "기타") + ) + override fun getExpenseList( groupId: String, expenseState: ExpenseState - ): Flow = flow { + ): Flow> = flow { emit( - cachedData.getOrDefault( - ExpenseListCachingKey(expenseState, groupId), - ExpenseListResponse.emptyList() + Result.success( + cachedData.getOrDefault( + ExpenseListCachingKey(expenseState, groupId), + ExpenseListResponse.emptyList() + ) ) ) dataSource.getExpenseData(expenseState).collect { cachedData[ExpenseListCachingKey(expenseState, groupId)] = it } emit( - cachedData.getOrDefault( - ExpenseListCachingKey(expenseState, groupId), - ExpenseListResponse.emptyList() + Result.success( + 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) + override suspend fun getExpenseListToGetPaid(groupId: String): Result = + Result.success( + dataSource.getExpenseData( + ExpenseState.TRANSFER_PENDING + ).last() ) - ) + + override suspend fun uploadExpense(receiptItem: ReceiptItem, groupId: String): Result = + Result.success(dataSource.addExpense(receiptItem)) + + override suspend fun getExpenseCategoryList(): Result> = + Result.success(getFakeCategoryList()) } 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 index b27f1582..6747364d 100644 --- a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseListRepositoryImpl.kt +++ b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ExpenseListRepositoryImpl.kt @@ -3,13 +3,11 @@ 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.ExpenseCategory import com.kappzzang.jeongsan.model.ExpenseListResponse import com.kappzzang.jeongsan.model.ExpenseState +import com.kappzzang.jeongsan.model.ReceiptItem 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 @@ -17,72 +15,68 @@ 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 + private val dataSource: ExpenseListRemoteDatasource ) : ExpenseRepository { private val cachedData = HashMap() - private fun getJwt(): String = auth.getSavedJwt() - private fun isJwtValid(jwt: String): Boolean = (jwt != "") + override suspend fun uploadExpense(receiptItem: ReceiptItem, groupId: String): Result = + dataSource.addExpense(receiptItem, groupId) + .mapCatching { + ExpenseEntityMapper.mapResponseWithExpenseEntityToModel(it) + } - 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 suspend fun getExpenseCategoryList(): Result> = + dataSource.getCategoryList().mapCatching { + it.categoryList.map { category -> + ExpenseEntityMapper.mapCategoryToModel(category) + } + } override fun getExpenseList( groupId: String, expenseState: ExpenseState - ): Flow = flow { + ): Flow> = flow { + // 먼저 캐싱된 데이터 emit emit( - cachedData.getOrDefault( - ExpenseListCachingKey(expenseState, groupId), - ExpenseListResponse.emptyList() + Result.success( + 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 - ) + // Remote API로 지출 목록 불러오고 캐싱 데이터 갱신 - response.body()?.let { - cachedData[ExpenseListCachingKey(expenseState, groupId)] = mapResponseBody(it) - } + val response = getExpenseListResponseFromAPI(groupId, expenseState) + response.onSuccess { + cachedData[ExpenseListCachingKey(expenseState, groupId)] = it } - emit( - cachedData.getOrDefault( - ExpenseListCachingKey(expenseState, groupId), - ExpenseListResponse.emptyList() - ) - ) + + emit(response) } - 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) + private suspend fun getExpenseListResponseFromAPI( + groupId: String, + expenseState: ExpenseState + ): Result = dataSource.getExpenseList( + expenseState, + groupId = groupId + ).mapCatching { + mapResponseBody(it) + } + + private fun mapResponseBody(body: ExpenseListResponseDTO): ExpenseListResponse { + val expenses = body.expenseList.map { ExpenseEntityMapper.mapExpenseEntityToModel(it) } + return ExpenseListResponse( + totalExpenseToSend = body.myTotalExpense ?: 0, + expenseList = expenses, + totalPrice = body.totalPrice.toInt() ) - ) + } + + override suspend fun getExpenseListToGetPaid(groupId: String): Result { + TODO("Not yet implemented") + } } diff --git a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ReceiptRepositoryImpl.kt b/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ReceiptRepositoryImpl.kt deleted file mode 100644 index 8802a16e..00000000 --- a/data/expense/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ReceiptRepositoryImpl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.kappzzang.jeongsan.repositoryimpl - -import com.kappzzang.jeongsan.datasource.ExpenseListFakeDatasource -import com.kappzzang.jeongsan.model.ReceiptItem -import com.kappzzang.jeongsan.repository.ReceiptRepository -import javax.inject.Inject - -class ReceiptRepositoryImpl @Inject constructor( - private val expenseDataSource: ExpenseListFakeDatasource -) : ReceiptRepository { - - override suspend fun uploadExpense(receiptItem: ReceiptItem): String = - expenseDataSource.addExpense(receiptItem) -} diff --git a/data/group/build.gradle.kts b/data/group/build.gradle.kts index dc1315d2..20314cc9 100644 --- a/data/group/build.gradle.kts +++ b/data/group/build.gradle.kts @@ -7,4 +7,5 @@ dependencies { implementation(project(":domain:group")) implementation(project(":domain:expense")) implementation("com.kakao.sdk:v2-talk:2.20.6") + implementation(project(":common:retrofit")) } diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/api/GroupRetrofitService.kt b/data/group/src/main/java/com/kappzzang/jeongsan/api/GroupRetrofitService.kt new file mode 100644 index 00000000..fe41a969 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/api/GroupRetrofitService.kt @@ -0,0 +1,52 @@ +package com.kappzzang.jeongsan.api + +import com.kappzzang.jeongsan.entity.CompleteGroupResponse +import com.kappzzang.jeongsan.entity.CreateGroupResponse +import com.kappzzang.jeongsan.entity.GetGroupResponse +import com.kappzzang.jeongsan.entity.GetLinkResponse +import com.kappzzang.jeongsan.entity.GetMemberInfoResponse +import com.kappzzang.jeongsan.entity.GetMyExpenseResponse +import com.kappzzang.jeongsan.entity.GetTargetGroupResponse +import com.kappzzang.jeongsan.entity.JoinGroupRequest +import com.kappzzang.jeongsan.entity.JoinGroupResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface GroupRetrofitService { + @GET("/api/teams") + suspend fun getGroupInfo(@Query("isClosed") isCompleted: Boolean): Response + + @GET("/api/teams/{teamId}") + suspend fun getTargetGroupInfo(@Path("teamId") groupId: Long): Response + + @POST("/api/teams") + suspend fun createGroup( + @Query("name") name: String, + @Query("subject") subject: String, + @Query("members") memberIdList: List + ): Response + + @PATCH("/api/teams/{teamId}") + suspend fun completeGroup(@Path("teamId") groupId: Long): Response + + @GET("/api/teams/{teamId}/members") + suspend fun getMemberInfo(@Path("teamId") groupId: Long): Response + + @POST("/api/members/join/{teamId}") + suspend fun joinGroup( + @Path("teamId") groupId: Long, + @Body request: JoinGroupRequest + ): Response + + @GET("/api/members/link") + suspend fun getLink(): Response + + // expense모듈에 속해야하는 것 같아 구현을 마치지 않음 + @GET("/api/expenses/ipaid/{teamId}") + suspend fun getMyExpense(): Response +} diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/datasource/remote/GroupRemoteDataSource.kt b/data/group/src/main/java/com/kappzzang/jeongsan/datasource/remote/GroupRemoteDataSource.kt new file mode 100644 index 00000000..cf43da48 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/datasource/remote/GroupRemoteDataSource.kt @@ -0,0 +1,175 @@ +package com.kappzzang.jeongsan.datasource.remote + +import com.kappzzang.jeongsan.api.GroupRetrofitService +import com.kappzzang.jeongsan.entity.CompleteGroupResponse +import com.kappzzang.jeongsan.entity.CreateGroupResponse +import com.kappzzang.jeongsan.entity.GetGroupResponse +import com.kappzzang.jeongsan.entity.GetLinkResponse +import com.kappzzang.jeongsan.entity.GetMemberInfoResponse +import com.kappzzang.jeongsan.entity.GetTargetGroupResponse +import com.kappzzang.jeongsan.entity.GroupInfo +import com.kappzzang.jeongsan.entity.JoinGroupRequest +import com.kappzzang.jeongsan.entity.JoinGroupResponse +import com.kappzzang.jeongsan.entity.MemberInfo +import javax.inject.Inject +import retrofit2.Response + +class GroupRemoteDataSource @Inject constructor(private val groupApi: GroupRetrofitService) { + suspend fun getGroupInfo(isCompleted: Boolean): Result> = try { + val response = groupApi.getGroupInfo(isCompleted = isCompleted) + handleGetGroupInfoResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + private fun handleGetGroupInfoResponse( + response: Response + ): Result> = when { + response.isSuccessful && !response.body()?.groupList.isNullOrEmpty() -> { + Result.success(response.body()!!.groupList) + } + else -> { + Result.failure(Exception("그룹 정보를 가져오는데 실패")) + } + } + + suspend fun getTargetGroupInfo(groupId: Long) = try { + val response = groupApi.getTargetGroupInfo(groupId = groupId) + handleGetTargetGroupInfoResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + private fun handleGetTargetGroupInfoResponse( + response: Response + ): Result = when { + response.isSuccessful && response.body()?.groupInfo != null -> { + Result.success(response.body()!!.groupInfo) + } + else -> { + Result.failure(Exception("그룹 정보를 가져오는데 실패")) + } + } + + suspend fun createGroup( + groupName: String, + groupSubject: String, + groupMemberUuidList: List + ): Result = try { + val response = groupApi.createGroup( + name = groupName, + subject = groupSubject, + memberIdList = groupMemberUuidList + ) + handleCreateGroupResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + private fun handleCreateGroupResponse(response: Response): Result = + when { + response.isSuccessful -> { + Result.success(response.body()!!.groupId) + } + response.code() == 404 -> { + Result.failure(Exception("유저를 찾을 수 없음")) + } + // 겹쳐도 되기로 했던 것 같은데 + response.code() == 409 -> { + Result.failure(Exception("중복된 모임 이름이 존재")) + } + else -> { + Result.failure(Exception("알수없는 오류 발생")) + } + } + + suspend fun completeGroup(groupId: Long): Result = try { + val response = groupApi.completeGroup(groupId = groupId) + handleCompleteGroupResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + private fun handleCompleteGroupResponse( + response: Response + ): Result = when { + response.isSuccessful -> { + Result.success(true) + } + response.code() == 400 -> { + Result.failure(Exception("모임이 이미 종료된 상태")) + } + response.code() == 404 -> { + Result.failure(Exception("완료하고자 하는 모임을 찾을 수 없음")) + } + else -> { + Result.failure(Exception("알수없는 오류 발생")) + } + } + + suspend fun getMemberInfo(groupId: Long): Result> = try { + val response = groupApi.getMemberInfo(groupId = groupId) + handleGetMemberInfoResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + private fun handleGetMemberInfoResponse( + response: Response + ): Result> = when { + response.isSuccessful && !response.body()?.memberList.isNullOrEmpty() -> { + Result.success(response.body()!!.memberList) + } + response.code() == 404 -> { + Result.failure(Exception("모임의 멤버 초대 현황 목록을 찾을 수 없음")) + } + else -> { + Result.failure(Exception("알수없는 오류 발생")) + } + } + + suspend fun joinGroup(groupId: Long, myId: Long): Result = try { + val response = groupApi.joinGroup( + groupId = groupId, + request = JoinGroupRequest(myId) + ) + handleJoinGroupResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + private fun handleJoinGroupResponse(response: Response): Result = + when { + response.isSuccessful -> { + Result.success(true) + } + response.code() == 400 -> { + Result.failure(Exception("모임에 초대 되지 않은 유저")) + } + response.code() == 404 -> { + Result.failure(Exception("잘못된 memberId, 사용자를 찾을 수 없음")) + } + else -> { + Result.failure(Exception("알수없는 오류 발생")) + } + } + + suspend fun getLink(): Result = try { + val response = groupApi.getLink() + handleGetLinkResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + private fun handleGetLinkResponse(response: Response): Result = when { + response.isSuccessful -> { + Result.success(response.body()!!.link) + } + response.code() == 404 -> { + Result.failure(Exception("카카오 페이 송금 링크를 찾을 수 없음")) + } + else -> { + Result.failure(Exception("알수없는 오류 발생")) + } + } +} diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/di/GroupRetrofitModule.kt b/data/group/src/main/java/com/kappzzang/jeongsan/di/GroupRetrofitModule.kt new file mode 100644 index 00000000..830d1b74 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/di/GroupRetrofitModule.kt @@ -0,0 +1,28 @@ +package com.kappzzang.jeongsan.di + +import com.kappzzang.jeongsan.api.GroupRetrofitService +import com.kappzzang.jeongsan.datasource.remote.GroupRemoteDataSource +import com.kappzzang.jeongsan.retrofit.RetrofitModule.ServiceRetrofit +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import retrofit2.Retrofit + +@Module +@InstallIn(SingletonComponent::class) +object GroupRetrofitModule { + + @Provides + @Singleton + fun provideGroupRemoteDataSource(groupApi: GroupRetrofitService) = + GroupRemoteDataSource(groupApi) + + @Provides + @Singleton + fun provideGroupRetrofitService( + @ServiceRetrofit serviceRetrofit: Retrofit + ): GroupRetrofitService = serviceRetrofit + .create(GroupRetrofitService::class.java) +} diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/CompleteGroupResponse.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/CompleteGroupResponse.kt new file mode 100644 index 00000000..395e1305 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/CompleteGroupResponse.kt @@ -0,0 +1,12 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class CompleteGroupResponse( + @SerializedName("status") + val status: String, + @SerializedName("errorCode") + val errorCode: String, + @SerializedName("message") + val message: String +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/CreateGroupResponse.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/CreateGroupResponse.kt new file mode 100644 index 00000000..c5f8a89f --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/CreateGroupResponse.kt @@ -0,0 +1,23 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class CreateGroupResponse( + @SerializedName("status") + val status: String, + @SerializedName("errorCode") + val errorCode: String, + @SerializedName("message") + val message: String, + @SerializedName("data") + val data: GroupId +) { + // data 내부에 groupId밖에 없어 바로 접근 하기 위해 구현 + val groupId: Long + get() = data.groupId +} + +data class GroupId( + @SerializedName("teamId") + val groupId: Long +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetGroupResponse.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetGroupResponse.kt new file mode 100644 index 00000000..ff80527b --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetGroupResponse.kt @@ -0,0 +1,8 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class GetGroupResponse( + @SerializedName("teamsWithProfiles") + val groupList: List +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetLinkResponse.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetLinkResponse.kt new file mode 100644 index 00000000..ba383df3 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetLinkResponse.kt @@ -0,0 +1,22 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class GetLinkResponse( + @SerializedName("status") + val status: String, + @SerializedName("errorCode") + val errorCode: String, + @SerializedName("message") + val message: String, + @SerializedName("data") + val data: KakaoPayLink +) { + val link: String + get() = data.link +} + +data class KakaoPayLink( + @SerializedName("kakaoPayLink") + val link: String +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetMemberInfoResponse.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetMemberInfoResponse.kt new file mode 100644 index 00000000..1d16f228 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetMemberInfoResponse.kt @@ -0,0 +1,14 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class GetMemberInfoResponse( + @SerializedName("status") + val status: String, + @SerializedName("errorCode") + val errorCode: String, + @SerializedName("message") + val message: String, + @SerializedName("data") + val memberList: List +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetMyExpenseResponse.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetMyExpenseResponse.kt new file mode 100644 index 00000000..cb0dcfe7 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetMyExpenseResponse.kt @@ -0,0 +1,13 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +// expense모듈에 속해야하는 것 같아 구현을 마치지 않음 +data class GetMyExpenseResponse( + @SerializedName("status") + val status: String, + @SerializedName("errorCode") + val errorCode: String, + @SerializedName("message") + val message: String +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetTargetGroupResponse.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetTargetGroupResponse.kt new file mode 100644 index 00000000..464ee678 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GetTargetGroupResponse.kt @@ -0,0 +1,3 @@ +package com.kappzzang.jeongsan.entity + +data class GetTargetGroupResponse(val groupInfo: GroupInfo) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/GroupInfo.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GroupInfo.kt new file mode 100644 index 00000000..8813f6a9 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/GroupInfo.kt @@ -0,0 +1,18 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class GroupInfo( + @SerializedName("teamId") + val id: Long, + @SerializedName("name") + val name: String, + @SerializedName("ownerKakaoId") + val ownerUuid: String, + @SerializedName("isCompleted") + val isCompleted: Boolean, + @SerializedName("subject") + val subject: String, + @SerializedName("memberPreviews") + val previewList: List +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/JoinGroupRequest.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/JoinGroupRequest.kt new file mode 100644 index 00000000..761bc353 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/JoinGroupRequest.kt @@ -0,0 +1,8 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class JoinGroupRequest( + @SerializedName("memberId") + val myId: Long +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/JoinGroupResponse.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/JoinGroupResponse.kt new file mode 100644 index 00000000..f442336d --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/JoinGroupResponse.kt @@ -0,0 +1,12 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class JoinGroupResponse( + @SerializedName("status") + val status: String, + @SerializedName("errorCode") + val errorCode: String, + @SerializedName("message") + val message: String +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/MemberInfo.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/MemberInfo.kt new file mode 100644 index 00000000..36c84772 --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/MemberInfo.kt @@ -0,0 +1,14 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class MemberInfo( + @SerializedName("memberId") + val id: Long, + @SerializedName("nickname") + val name: String, + @SerializedName("profileImage") + val profileImageUrl: String, + @SerializedName("isInviteAccepted") + val isInvited: Boolean +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/entity/MemberPreview.kt b/data/group/src/main/java/com/kappzzang/jeongsan/entity/MemberPreview.kt new file mode 100644 index 00000000..fc5eb0ca --- /dev/null +++ b/data/group/src/main/java/com/kappzzang/jeongsan/entity/MemberPreview.kt @@ -0,0 +1,10 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class MemberPreview( + @SerializedName("name") + val name: String, + @SerializedName("profileImage") + val profileImageUrl: String +) diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/mapper/GroupEntityMapper.kt b/data/group/src/main/java/com/kappzzang/jeongsan/mapper/GroupEntityMapper.kt index 2b4d08b6..4ad66508 100644 --- a/data/group/src/main/java/com/kappzzang/jeongsan/mapper/GroupEntityMapper.kt +++ b/data/group/src/main/java/com/kappzzang/jeongsan/mapper/GroupEntityMapper.kt @@ -1,7 +1,9 @@ package com.kappzzang.jeongsan.mapper import com.kappzzang.jeongsan.entity.GroupEntity +import com.kappzzang.jeongsan.entity.GroupInfo import com.kappzzang.jeongsan.model.GroupCreateItem +import com.kappzzang.jeongsan.model.GroupItem object GroupEntityMapper { fun mapGroupCreateToGroupEntity(groupCreateItem: GroupCreateItem): GroupEntity = GroupEntity( @@ -10,4 +12,12 @@ object GroupEntityMapper { subject = groupCreateItem.subject, memberProfileImage = "https://avatars.githubusercontent.com/u/38340588?v=4" ) + + fun GroupInfo.toGroupItem() = GroupItem( + id = id.toString(), + name = name, + isCompleted = isCompleted, + subject = subject, + profileImageURL = previewList.map { it.profileImageUrl } + ) } diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/mapper/MemberEntityMapper.kt b/data/group/src/main/java/com/kappzzang/jeongsan/mapper/MemberEntityMapper.kt index 722fbadd..e1ae9c29 100644 --- a/data/group/src/main/java/com/kappzzang/jeongsan/mapper/MemberEntityMapper.kt +++ b/data/group/src/main/java/com/kappzzang/jeongsan/mapper/MemberEntityMapper.kt @@ -1,6 +1,7 @@ package com.kappzzang.jeongsan.mapper import com.kappzzang.jeongsan.entity.MemberEntity +import com.kappzzang.jeongsan.entity.MemberInfo import com.kappzzang.jeongsan.model.MemberItem object MemberEntityMapper { @@ -17,4 +18,11 @@ object MemberEntityMapper { profileImageUrl = memberItem.profileImageUrl, isInvited = memberItem.isInvited ) + + fun MemberInfo.toMemberItem() = MemberItem( + id = id.toString(), + name = name, + profileImageUrl = profileImageUrl, + isInvited = isInvited + ) } diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/GroupInfoRepositoryImpl.kt b/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/GroupInfoRepositoryImpl.kt index 14fad12d..ea19284f 100644 --- a/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/GroupInfoRepositoryImpl.kt +++ b/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/GroupInfoRepositoryImpl.kt @@ -1,8 +1,8 @@ package com.kappzzang.jeongsan.repositoryimpl -import com.kappzzang.jeongsan.datasource.group.GroupDatabase -import com.kappzzang.jeongsan.entity.GroupEntity -import com.kappzzang.jeongsan.mapper.GroupEntityMapper +import android.util.Log +import com.kappzzang.jeongsan.datasource.remote.GroupRemoteDataSource +import com.kappzzang.jeongsan.mapper.GroupEntityMapper.toGroupItem import com.kappzzang.jeongsan.model.GroupCreateItem import com.kappzzang.jeongsan.model.GroupItem import com.kappzzang.jeongsan.repository.GroupInfoRepository @@ -10,39 +10,66 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -class GroupInfoRepositoryImpl @Inject constructor(private val groupDatabase: GroupDatabase) : - GroupInfoRepository { +class GroupInfoRepositoryImpl @Inject constructor( + private val groupRemoteDataSource: GroupRemoteDataSource +) : GroupInfoRepository { override suspend fun getProgressingGroupInfo(): List { - // TODO: 임시로 테스트 데이터 반환 - 이후에 서버에서 받아오도록 수정 - return groupDatabase.groupDao().getProgressingGroup().map { getGroupItemFromEntity(it) } + val result = groupRemoteDataSource.getGroupInfo(false) + result.fold( + onSuccess = { + return it.map { it.toGroupItem() } + }, + onFailure = { + return emptyList() + } + ) } override suspend fun getDoneGroupInfo(): List { - // TODO: 임시로 테스트 데이터 반환 - 이후에 서버에서 받아오도록 수정 - return groupDatabase.groupDao().getDoneGroup().map { getGroupItemFromEntity(it) } + val result = groupRemoteDataSource.getGroupInfo(true) + result.fold( + onSuccess = { + return it.map { it.toGroupItem() } + }, + onFailure = { + return emptyList() + } + ) } - private fun getGroupItemFromEntity(entity: GroupEntity) = GroupItem( - entity.id.toString(), - entity.name, - entity.isCompleted, - entity.subject, - if (entity.memberProfileImage == "") emptyList() else listOf(entity.memberProfileImage) - ) + override fun getTargetGroupInfo(groupId: String): Flow = flow { + groupId.toLongOrNull()?.let { id -> + groupRemoteDataSource.getTargetGroupInfo(id).fold( + onSuccess = { + it.toGroupItem() + }, + onFailure = { + getBlankGroupItem() + } + ) + }?.let { + emit( + getBlankGroupItem() + ) + } + } - override fun getGroupInfo(groupId: String): Flow = flow { - emit( - groupId.toLongOrNull()?.let { id -> - groupDatabase.groupDao().inquireGroupInfo(id).firstOrNull()?.let { - getGroupItemFromEntity(it) - } ?: GroupItem("0", "", false, "", emptyList()) - } ?: GroupItem("0", "", false, "", emptyList()) + override suspend fun uploadGroupInfo(createdGroup: GroupCreateItem): Long { + Log.d("GroupRepositoryImpl", createdGroup.memberUuidList.toString()) + val result = groupRemoteDataSource.createGroup( + groupName = createdGroup.name, + groupSubject = createdGroup.subject, + groupMemberUuidList = createdGroup.memberUuidList ) + return result.getOrThrow() } - override suspend fun uploadGroupInfo(createdGroup: GroupCreateItem) { - groupDatabase.groupDao() - .addGroup(GroupEntityMapper.mapGroupCreateToGroupEntity(createdGroup)) - } + private fun getBlankGroupItem(): GroupItem = GroupItem( + "", + "", + true, + "", + listOf() + ) } diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/InviteRepositoryImpl.kt b/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/InviteRepositoryImpl.kt index cd811a9d..965b824d 100644 --- a/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/InviteRepositoryImpl.kt +++ b/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/InviteRepositoryImpl.kt @@ -2,31 +2,37 @@ package com.kappzzang.jeongsan.repositoryimpl import android.util.Log import com.kakao.sdk.talk.TalkApiClient +import com.kappzzang.jeongsan.datasource.remote.GroupRemoteDataSource import com.kappzzang.jeongsan.repository.InviteRepository import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -class InviteRepositoryImpl @Inject constructor() : InviteRepository { +class InviteRepositoryImpl @Inject constructor( + private val groupRemoteDataSource: GroupRemoteDataSource +) : InviteRepository { - // TODO: 추후 친구에게 보내기로 수정 override suspend fun sendInviteMessage( groupId: String, groupName: String, - memberId: String + memberUuidList: List ): Boolean = suspendCoroutine { continuation -> - TalkApiClient.instance.sendCustomMemo( + TalkApiClient.instance.sendCustomMessage( + receiverUuids = memberUuidList, templateId = INVITE_MESSAGE_TEMPLATE_ID, templateArgs = mapOf( GROUP_ID to groupId, GROUP_NAME to groupName ) - ) { error -> + ) { result, error -> if (error != null) { Log.e(TAG, "초대 메시지 전송 실패", error) continuation.resume(false) - } else { + } else if (result != null) { Log.i(TAG, "초대 메시지 전송 성공") + if (result.failureInfos != null) { + Log.i(TAG, "일부에게 초대 메시지 전송 실패") + } continuation.resume(true) } } diff --git a/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/MemberRepositoryImpl.kt b/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/MemberRepositoryImpl.kt index 418db8d1..ac77b3fa 100644 --- a/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/MemberRepositoryImpl.kt +++ b/data/group/src/main/java/com/kappzzang/jeongsan/repositoryimpl/MemberRepositoryImpl.kt @@ -1,26 +1,31 @@ package com.kappzzang.jeongsan.repositoryimpl -import com.kappzzang.jeongsan.datasource.member.MemberDatabase -import com.kappzzang.jeongsan.mapper.MemberEntityMapper +import com.kappzzang.jeongsan.datasource.remote.GroupRemoteDataSource +import com.kappzzang.jeongsan.mapper.MemberEntityMapper.toMemberItem import com.kappzzang.jeongsan.model.MemberItem import com.kappzzang.jeongsan.repository.MemberRepository import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -class MemberRepositoryImpl @Inject constructor(private val memberDatabase: MemberDatabase) : - MemberRepository { - override suspend fun addMember(member: MemberItem) { +class MemberRepositoryImpl @Inject constructor( + private val groupRemoteDataSource: GroupRemoteDataSource +) : MemberRepository { + override suspend fun addMember(groupId: String, memberId: String) { withContext(Dispatchers.IO) { - memberDatabase.getMemberDao().addMember( - MemberEntityMapper.mapMemberToMemberEntity(member) - ) + groupRemoteDataSource.joinGroup(groupId.toLong(), memberId.toLong()) } } - override suspend fun getAllMember(): List = withContext(Dispatchers.IO) { - memberDatabase.getMemberDao().getAllMember().map { - MemberEntityMapper.mapMemberEntityToMember(it) + override suspend fun getAllMember(groupId: String): List = + withContext(Dispatchers.IO) { + groupRemoteDataSource.getMemberInfo(groupId.toLong()).fold( + onSuccess = { memberInfoList -> + memberInfoList.map { it.toMemberItem() } + }, + onFailure = { + listOf() + } + ) } - } } diff --git a/data/ocr/src/main/java/com/kappzzang/jeongsan/api/OcrRetrofitService.kt b/data/ocr/src/main/java/com/kappzzang/jeongsan/api/OcrRetrofitService.kt new file mode 100644 index 00000000..a4ddd795 --- /dev/null +++ b/data/ocr/src/main/java/com/kappzzang/jeongsan/api/OcrRetrofitService.kt @@ -0,0 +1,12 @@ +package com.kappzzang.jeongsan.api + +import com.kappzzang.jeongsan.entity.ReceiptAnalyzeResponse +import com.kappzzang.jeongsan.entity.ReceiptImage +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface OcrRetrofitService { + @POST("/api/receipts/analyze") + suspend fun analyzeReceipt(@Body receiptImage: ReceiptImage): Response +} diff --git a/data/ocr/src/main/java/com/kappzzang/jeongsan/datasource/ReceiptCaptureRemoteDatasource.kt b/data/ocr/src/main/java/com/kappzzang/jeongsan/datasource/ReceiptCaptureRemoteDatasource.kt new file mode 100644 index 00000000..015eda2e --- /dev/null +++ b/data/ocr/src/main/java/com/kappzzang/jeongsan/datasource/ReceiptCaptureRemoteDatasource.kt @@ -0,0 +1,21 @@ +package com.kappzzang.jeongsan.datasource + +import com.kappzzang.jeongsan.api.OcrRetrofitService +import com.kappzzang.jeongsan.entity.ReceiptAnalyzeResponse +import com.kappzzang.jeongsan.entity.ReceiptImage +import javax.inject.Inject +import retrofit2.Response + +class ReceiptCaptureRemoteDatasource @Inject constructor( + private val ocrRetrofitService: OcrRetrofitService +) { + suspend fun analyzeReceipt(base64Encoded: String): Response = + ocrRetrofitService.analyzeReceipt( + receiptImage = ReceiptImage( + format = "", + name = "", + base64Encoded = base64Encoded, + url = "" + ) + ) +} diff --git a/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/OcrResultDetailItem.kt b/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/OcrResultDetailItem.kt index c434e775..36c7d1e2 100644 --- a/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/OcrResultDetailItem.kt +++ b/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/OcrResultDetailItem.kt @@ -1,3 +1,12 @@ package com.kappzzang.jeongsan.entity -data class OcrResultDetailItem(val name: String, val quantity: Int, val unitPrice: Int) +import com.google.gson.annotations.SerializedName + +data class OcrResultDetailItem( + @SerializedName("name") + val name: String, + @SerializedName("quantity") + val quantity: Int, + @SerializedName("unitPrice") + val unitPrice: Int +) diff --git a/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/ReceiptAnalyzeResponse.kt b/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/ReceiptAnalyzeResponse.kt new file mode 100644 index 00000000..9760259d --- /dev/null +++ b/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/ReceiptAnalyzeResponse.kt @@ -0,0 +1,12 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class ReceiptAnalyzeResponse( + @SerializedName("title") + val title: String, + @SerializedName("paymentTime") + val paymentTime: String, + @SerializedName("items") + val items: List +) diff --git a/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/ReceiptImage.kt b/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/ReceiptImage.kt new file mode 100644 index 00000000..052e9a8c --- /dev/null +++ b/data/ocr/src/main/java/com/kappzzang/jeongsan/entity/ReceiptImage.kt @@ -0,0 +1,14 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class ReceiptImage( + @SerializedName("format") + val format: String, + @SerializedName("name") + val name: String, + @SerializedName("data") + val base64Encoded: String, + @SerializedName("url") + val url: String +) diff --git a/data/user/build.gradle.kts b/data/user/build.gradle.kts index 539f56a9..f43fc10c 100644 --- a/data/user/build.gradle.kts +++ b/data/user/build.gradle.kts @@ -6,7 +6,6 @@ android { namespace = "com.kappzzang.jeongsan.user" } dependencies { - implementation("androidx.datastore:datastore-preferences:1.1.1") implementation("com.kakao.sdk:v2-user:2.20.6") implementation(project(":domain:common-user")) implementation(project(":common:androidutil")) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/api/ServerAuthRetrofitService.kt b/data/user/src/main/java/com/kappzzang/jeongsan/api/ServerAuthRetrofitService.kt new file mode 100644 index 00000000..f2efccbc --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/api/ServerAuthRetrofitService.kt @@ -0,0 +1,22 @@ +package com.kappzzang.jeongsan.api + +import com.kappzzang.jeongsan.entity.AuthResponse +import com.kappzzang.jeongsan.entity.LoginRequest +import com.kappzzang.jeongsan.entity.RefreshResponse +import com.kappzzang.jeongsan.entity.RefreshTokenRequest +import com.kappzzang.jeongsan.entity.RegisterRequest +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.POST + +interface ServerAuthRetrofitService { + + @POST("/api/members/token/refresh") + suspend fun refreshToken(@Body request: RefreshTokenRequest): Response + + @POST("/api/members/register") + suspend fun register(@Body request: RegisterRequest): Response + + @POST("/api/members/login") + suspend fun login(@Body request: LoginRequest): Response +} diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/datasource/AuthLocalDataSource.kt b/data/user/src/main/java/com/kappzzang/jeongsan/datasource/AuthLocalDataSource.kt index 3ae8cb6c..24bef95c 100644 --- a/data/user/src/main/java/com/kappzzang/jeongsan/datasource/AuthLocalDataSource.kt +++ b/data/user/src/main/java/com/kappzzang/jeongsan/datasource/AuthLocalDataSource.kt @@ -1,48 +1,59 @@ package com.kappzzang.jeongsan.datasource -import androidx.datastore.core.DataStore -import androidx.datastore.core.IOException -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey -import com.kappzzang.jeongsan.data.AuthData +import android.content.SharedPreferences +import androidx.core.content.edit +import com.kappzzang.jeongsan.data.KakaoAuthData +import com.kappzzang.jeongsan.data.ServerAuthData import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -class AuthLocalDataSource @Inject constructor(private val dataStore: DataStore) { +class AuthLocalDataSource @Inject constructor(private val sharedPreferences: SharedPreferences) { - fun getAuthDataFlow(): Flow = dataStore.data.catch { exception -> - if (exception is IOException) { - emit(emptyPreferences()) - } else { - throw exception + fun getKakaoAuthData() = KakaoAuthData( + kakaoAccessToken = sharedPreferences.getString(KAKAO_ACCESS_TOKEN, "") ?: "", + kakaoRefreshToken = sharedPreferences.getString(KAKAO_REFRESH_TOKEN, "") ?: "", + accessTokenExpirationTime = sharedPreferences.getLong(KAKAO_ACCESS_EXPIRATION, 0L) + ) + + fun getServerAuthData() = ServerAuthData( + accessToken = sharedPreferences.getString(SERVER_ACCESS_TOKEN, "") ?: "", + refreshToken = sharedPreferences.getString(SERVER_REFRESH_TOKEN, "") ?: "" + ) + + fun removeKakaoAuthData() { + sharedPreferences.edit { + remove(KAKAO_ACCESS_TOKEN) + remove(KAKAO_REFRESH_TOKEN) + remove(KAKAO_ACCESS_EXPIRATION) + } + } + + fun removeServerAuthData() { + sharedPreferences.edit { + remove(SERVER_ACCESS_TOKEN) + remove(SERVER_REFRESH_TOKEN) + } + } + + fun updateKakaoPreference(data: KakaoAuthData) { + sharedPreferences.edit { + putString(KAKAO_ACCESS_TOKEN, data.kakaoAccessToken) + putString(KAKAO_REFRESH_TOKEN, data.kakaoRefreshToken) + putLong(KAKAO_ACCESS_EXPIRATION, data.accessTokenExpirationTime) } - }.map { preferences -> - AuthData( - kakaoAccessToken = preferences[ACCESS_TOKEN] ?: "", - kakaoRefreshToken = preferences[REFRESH_TOKEN] ?: "", - accessTokenExpirationTime = preferences[ACCESS_EXPIRATION] ?: 0L, - jwt = preferences[JWT] - ) } - suspend fun updatePreference(data: AuthData) { - dataStore.edit { preferences -> - preferences[ACCESS_TOKEN] = data.kakaoAccessToken - preferences[REFRESH_TOKEN] = data.kakaoRefreshToken - preferences[ACCESS_EXPIRATION] = data.accessTokenExpirationTime - data.jwt?.let { preferences[JWT] = it } + fun updateServerPreference(data: ServerAuthData) { + sharedPreferences.edit { + putString(SERVER_ACCESS_TOKEN, data.accessToken) + putString(SERVER_REFRESH_TOKEN, data.refreshToken) } } companion object { - val ACCESS_TOKEN = stringPreferencesKey("kakao_access_token") - val REFRESH_TOKEN = stringPreferencesKey("kakao_refresh_token") - val ACCESS_EXPIRATION = longPreferencesKey("kakao_access_token_expiration") - val JWT = stringPreferencesKey("service_jwt") + const val KAKAO_ACCESS_TOKEN = "kakao_access_token" + private const val KAKAO_REFRESH_TOKEN = "kakao_refresh_token" + private const val KAKAO_ACCESS_EXPIRATION = "kakao_access_token_expiration" + private const val SERVER_ACCESS_TOKEN = "server_access_token" + private const val SERVER_REFRESH_TOKEN = "server_refresh_token" } } diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthenticationDataSource.kt b/data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthRemoteDataSource.kt similarity index 95% rename from data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthenticationDataSource.kt rename to data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthRemoteDataSource.kt index c05787ba..0c92c658 100644 --- a/data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthenticationDataSource.kt +++ b/data/user/src/main/java/com/kappzzang/jeongsan/datasource/KakaoAuthRemoteDataSource.kt @@ -7,7 +7,7 @@ import com.kappzzang.jeongsan.entity.KakaoRefreshTokenResponseDTO import javax.inject.Inject import retrofit2.Response -class KakaoAuthenticationDataSource @Inject constructor( +class KakaoAuthRemoteDataSource @Inject constructor( private val kakaoApi: KakaoAuthRetrofitService ) { diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/datasource/ServerAuthRemoteDataSource.kt b/data/user/src/main/java/com/kappzzang/jeongsan/datasource/ServerAuthRemoteDataSource.kt new file mode 100644 index 00000000..fb39e356 --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/datasource/ServerAuthRemoteDataSource.kt @@ -0,0 +1,107 @@ +package com.kappzzang.jeongsan.datasource + +import com.kappzzang.jeongsan.api.ServerAuthRetrofitService +import com.kappzzang.jeongsan.entity.AuthResponse +import com.kappzzang.jeongsan.entity.LoginRequest +import com.kappzzang.jeongsan.entity.RefreshResponse +import com.kappzzang.jeongsan.entity.RefreshTokenData +import com.kappzzang.jeongsan.entity.RefreshTokenRequest +import com.kappzzang.jeongsan.entity.RegisterRequest +import com.kappzzang.jeongsan.entity.TokenData +import javax.inject.Inject +import retrofit2.Response + +class ServerAuthRemoteDataSource @Inject constructor( + private val authApi: ServerAuthRetrofitService +) { + + suspend fun refreshToken(refreshToken: String): Result = try { + val response = authApi.refreshToken(RefreshTokenRequest(refreshToken = refreshToken)) + handleRefreshResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + // 리프레시 할 때 Response를 처리 + private fun handleRefreshResponse( + response: Response + ): Result = when { + response.isSuccessful && response.body()?.data != null -> { + Result.success(response.body()!!.data) + } + + response.code() == 403 -> { + Result.failure(SecurityException("리프레시 토큰이 유효하지 않음.")) + } + + response.code() == 404 -> { + Result.failure(NoSuchElementException("사용자를 찾을 수 없음.")) + } + + else -> { + Result.failure( + Exception("Unexpected Error: ${response.code()} - ${response.message()}") + ) + } + } + + suspend fun register( + uuid: String, + nickname: String, + email: String, + profileUrl: String + ): Result = try { + val response = authApi.register( + RegisterRequest( + uuid = uuid, + nickname = nickname, + email = email, + profileImageUrl = profileUrl + ) + ) + handleRegisterResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + // 회원가입 시 Response를 처리 + private fun handleRegisterResponse(response: Response): Result = when { + response.isSuccessful && response.body()?.data != null -> { + Result.success(response.body()!!.data) + } + + response.code() == 400 -> { + Result.failure(IllegalStateException("해당 사용자는 이미 회원가입됨.")) + } + + else -> { + Result.failure( + Exception("Unexpected error: ${response.code()} - ${response.message()}") + ) + } + } + + suspend fun login(email: String): Result = try { + val response = authApi.login(LoginRequest(email = email)) + handleLoginResponse(response) + } catch (e: Exception) { + Result.failure(e) + } + + // 로그인 시 Response를 처리 + private fun handleLoginResponse(response: Response): Result = when { + response.isSuccessful && response.body()?.data != null -> { + Result.success(response.body()!!.data) + } + + response.code() == 404 -> { + Result.failure(NoSuchElementException("사용자를 찾을 수 없음.")) + } + + else -> { + Result.failure( + Exception("Unexpected error: ${response.code()} - ${response.message()}") + ) + } + } +} diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/di/RetrofitModule.kt b/data/user/src/main/java/com/kappzzang/jeongsan/di/RetrofitModule.kt index a9de18fa..9152e1ac 100644 --- a/data/user/src/main/java/com/kappzzang/jeongsan/di/RetrofitModule.kt +++ b/data/user/src/main/java/com/kappzzang/jeongsan/di/RetrofitModule.kt @@ -1,6 +1,7 @@ package com.kappzzang.jeongsan.di import com.kappzzang.jeongsan.api.KakaoAuthRetrofitService +import com.kappzzang.jeongsan.api.ServerAuthRetrofitService import com.kappzzang.jeongsan.retrofit.RetrofitModule import dagger.Module import dagger.Provides @@ -15,4 +16,9 @@ object RetrofitModule { fun provideKakaoAuthRetrofitService( @RetrofitModule.KakaoAuthRetrofit kakaoAuthRetrofit: Retrofit ): KakaoAuthRetrofitService = kakaoAuthRetrofit.create(KakaoAuthRetrofitService::class.java) + + @Provides + fun provideServerAuthRetrofitService( + @RetrofitModule.ServiceAuthRetrofit serverAuthRetrofit: Retrofit + ): ServerAuthRetrofitService = serverAuthRetrofit.create(ServerAuthRetrofitService::class.java) } diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/di/UserRepositoryModule.kt b/data/user/src/main/java/com/kappzzang/jeongsan/di/UserRepositoryModule.kt index b8d82223..80da923e 100644 --- a/data/user/src/main/java/com/kappzzang/jeongsan/di/UserRepositoryModule.kt +++ b/data/user/src/main/java/com/kappzzang/jeongsan/di/UserRepositoryModule.kt @@ -1,9 +1,11 @@ package com.kappzzang.jeongsan.di import com.kappzzang.jeongsan.repository.KakaoAuthenticationRepository +import com.kappzzang.jeongsan.repository.ServerAuthenticationRepository import com.kappzzang.jeongsan.repository.UserInfoRepository import com.kappzzang.jeongsan.repositoryimpl.AuthenticationRepositoryImpl import com.kappzzang.jeongsan.repositoryimpl.KakaoAuthenticationRepositoryImpl +import com.kappzzang.jeongsan.repositoryimpl.ServerAuthenticationRepositoryImpl import com.kappzzang.jeongsan.repositoryimpl.UserInfoRepositoryImpl import com.kappzzang.jeongsan.util.AuthenticationRepository import dagger.Binds @@ -32,4 +34,10 @@ abstract class UserRepositoryModule { abstract fun bindAuthenticationRepository( authenticationRepositoryImpl: AuthenticationRepositoryImpl ): AuthenticationRepository + + @Binds + @Singleton + abstract fun bindServerAuthenticationRepository( + serverAuthenticationRepositoryImpl: ServerAuthenticationRepositoryImpl + ): ServerAuthenticationRepository } diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/entity/AuthResponse.kt b/data/user/src/main/java/com/kappzzang/jeongsan/entity/AuthResponse.kt new file mode 100644 index 00000000..c0942d28 --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/entity/AuthResponse.kt @@ -0,0 +1,14 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class AuthResponse( + @SerializedName("status") + val status: String, + @SerializedName("errorCode") + val errorCode: String, + @SerializedName("message") + val message: String, + @SerializedName("data") + val data: TokenData +) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/entity/LoginRequest.kt b/data/user/src/main/java/com/kappzzang/jeongsan/entity/LoginRequest.kt new file mode 100644 index 00000000..1c778cd2 --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/entity/LoginRequest.kt @@ -0,0 +1,8 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class LoginRequest( + @SerializedName("email") + val email: String +) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshResponse.kt b/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshResponse.kt new file mode 100644 index 00000000..5007f181 --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshResponse.kt @@ -0,0 +1,14 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class RefreshResponse( + @SerializedName("status") + val status: String, + @SerializedName("errorCode") + val errorCode: String, + @SerializedName("message") + val message: String, + @SerializedName("data") + val data: RefreshTokenData +) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshTokenData.kt b/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshTokenData.kt new file mode 100644 index 00000000..a705b03c --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshTokenData.kt @@ -0,0 +1,10 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class RefreshTokenData( + @SerializedName("tokenType") + val tokenType: String, + @SerializedName("accessToken") + val accessToken: String +) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshTokenRequest.kt b/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshTokenRequest.kt new file mode 100644 index 00000000..9ab68142 --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/entity/RefreshTokenRequest.kt @@ -0,0 +1,8 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class RefreshTokenRequest( + @SerializedName("refreshToken") + val refreshToken: String +) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/entity/RegisterRequest.kt b/data/user/src/main/java/com/kappzzang/jeongsan/entity/RegisterRequest.kt new file mode 100644 index 00000000..9bcce56f --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/entity/RegisterRequest.kt @@ -0,0 +1,14 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class RegisterRequest( + @SerializedName("uuid") + val uuid: String, + @SerializedName("nickname") + val nickname: String, + @SerializedName("email") + val email: String, + @SerializedName("profileImage") + val profileImageUrl: String +) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/entity/TokenData.kt b/data/user/src/main/java/com/kappzzang/jeongsan/entity/TokenData.kt new file mode 100644 index 00000000..a9f7e61a --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/entity/TokenData.kt @@ -0,0 +1,12 @@ +package com.kappzzang.jeongsan.entity + +import com.google.gson.annotations.SerializedName + +data class TokenData( + @SerializedName("tokenType") + val tokenType: String, + @SerializedName("accessToken") + val accessToken: String, + @SerializedName("refreshToken") + val refreshToken: String +) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoOAuthTokenAuthDataMapper.kt b/data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoOAuthTokenKakaoAuthDataMapper.kt similarity index 87% rename from data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoOAuthTokenAuthDataMapper.kt rename to data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoOAuthTokenKakaoAuthDataMapper.kt index 1b69cbc5..3264f041 100644 --- a/data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoOAuthTokenAuthDataMapper.kt +++ b/data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoOAuthTokenKakaoAuthDataMapper.kt @@ -1,22 +1,21 @@ package com.kappzzang.jeongsan.mapper -import com.kappzzang.jeongsan.data.AuthData +import com.kappzzang.jeongsan.data.KakaoAuthData import com.kappzzang.jeongsan.entity.KakaoRefreshTokenResponseDTO -object KakaoOAuthTokenAuthDataMapper { +object KakaoOAuthTokenKakaoAuthDataMapper { private fun getExpirationTime(accessTokenExpirationTimeInSeconds: Int): Long = System.currentTimeMillis() + accessTokenExpirationTimeInSeconds * 1_000L fun mapRefreshDtoToAuthData( refreshTokenResponseDTO: KakaoRefreshTokenResponseDTO, - originalAuthData: AuthData? = null - ): AuthData { - val result: AuthData + originalAuthData: KakaoAuthData? = null + ): KakaoAuthData { + val result: KakaoAuthData if (originalAuthData == null) { - result = AuthData( + result = KakaoAuthData( kakaoRefreshToken = refreshTokenResponseDTO.refreshToken ?: "", kakaoAccessToken = refreshTokenResponseDTO.accessToken, - jwt = "", accessTokenExpirationTime = getExpirationTime( refreshTokenResponseDTO.accessTokenExpiresInSeconds ) diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoUserInfoMapper.kt b/data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoUserInfoMapper.kt index 7b81f7a1..c259bc9b 100644 --- a/data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoUserInfoMapper.kt +++ b/data/user/src/main/java/com/kappzzang/jeongsan/mapper/KakaoUserInfoMapper.kt @@ -7,6 +7,7 @@ object KakaoUserInfoMapper { fun mapKakaoUserModelToUserItem(user: User): UserItem = UserItem( uuid = user.uuid ?: "", name = user.kakaoAccount?.profile?.nickname ?: "", + email = user.kakaoAccount?.email ?: "", profileUrl = user.kakaoAccount?.profile?.profileImageUrl ?: "" ) } diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/mapper/TokenDataToServerAuthDataMapper.kt b/data/user/src/main/java/com/kappzzang/jeongsan/mapper/TokenDataToServerAuthDataMapper.kt new file mode 100644 index 00000000..6dea7fa5 --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/mapper/TokenDataToServerAuthDataMapper.kt @@ -0,0 +1,12 @@ +package com.kappzzang.jeongsan.mapper + +import com.kappzzang.jeongsan.data.ServerAuthData +import com.kappzzang.jeongsan.entity.TokenData + +object TokenDataToServerAuthDataMapper { + + fun mapTokenDataToServerAuthData(tokenData: TokenData) = ServerAuthData( + accessToken = tokenData.accessToken, + refreshToken = tokenData.refreshToken + ) +} diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/AuthenticationRepositoryImpl.kt b/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/AuthenticationRepositoryImpl.kt index 8c6d5034..1cc4e7aa 100644 --- a/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/AuthenticationRepositoryImpl.kt +++ b/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/AuthenticationRepositoryImpl.kt @@ -1,23 +1,54 @@ package com.kappzzang.jeongsan.repositoryimpl -import com.kappzzang.jeongsan.data.AuthData +import android.util.Log +import com.kappzzang.jeongsan.data.KakaoAuthData +import com.kappzzang.jeongsan.data.ServerAuthData import com.kappzzang.jeongsan.datasource.AuthLocalDataSource +import com.kappzzang.jeongsan.datasource.ServerAuthRemoteDataSource import com.kappzzang.jeongsan.util.AuthenticationRepository import javax.inject.Inject -import kotlinx.coroutines.flow.Flow class AuthenticationRepositoryImpl @Inject constructor( - private val datasource: AuthLocalDataSource + private val authLocalDataSource: AuthLocalDataSource, + private val serverAuthRemoteDataSource: ServerAuthRemoteDataSource ) : AuthenticationRepository { - override fun getAuthData(): Flow = datasource.getAuthDataFlow() + override fun getKakaoAuthData(): KakaoAuthData = authLocalDataSource.getKakaoAuthData() - override suspend fun updateAuthData(newData: AuthData) { - datasource.updatePreference(newData) + override fun getServerAuthData(): ServerAuthData = authLocalDataSource.getServerAuthData() + + override fun updateKakaoAuthData(newData: KakaoAuthData) { + authLocalDataSource.updateKakaoPreference(newData) + } + + override fun updateServerAuthData(newData: ServerAuthData) { + authLocalDataSource.updateServerPreference(newData) + } + + override fun removeKakaoAuthData() { + authLocalDataSource.removeKakaoAuthData() + } + + override fun removeServerAuthData() { + authLocalDataSource.removeServerAuthData() } - override suspend fun removeAuthData() { - TODO("Not yet implemented") + override suspend fun refreshJwtFromServer(authData: ServerAuthData): Result = + serverAuthRemoteDataSource.refreshToken(authData.refreshToken).fold( + onSuccess = { refreshTokenData -> + val serverAuthData = authData.copy( + accessToken = refreshTokenData.accessToken + ) + Result.success(serverAuthData) + }, + onFailure = { exception -> + Log.e(TAG, exception.message, exception.cause) + Result.failure(exception) + } + ) + + companion object { + private const val TAG = "AuthenticationRepositoryImpl" } } diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/KakaoAuthenticationRepositoryImpl.kt b/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/KakaoAuthenticationRepositoryImpl.kt index 93619dc2..eb374372 100644 --- a/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/KakaoAuthenticationRepositoryImpl.kt +++ b/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/KakaoAuthenticationRepositoryImpl.kt @@ -1,16 +1,16 @@ package com.kappzzang.jeongsan.repositoryimpl import android.util.Log -import com.kappzzang.jeongsan.data.AuthData -import com.kappzzang.jeongsan.datasource.KakaoAuthenticationDataSource -import com.kappzzang.jeongsan.mapper.KakaoOAuthTokenAuthDataMapper.mapRefreshDtoToAuthData +import com.kappzzang.jeongsan.data.KakaoAuthData +import com.kappzzang.jeongsan.datasource.KakaoAuthRemoteDataSource +import com.kappzzang.jeongsan.mapper.KakaoOAuthTokenKakaoAuthDataMapper.mapRefreshDtoToAuthData import com.kappzzang.jeongsan.repository.KakaoAuthenticationRepository import javax.inject.Inject class KakaoAuthenticationRepositoryImpl @Inject constructor( - private val dataSource: KakaoAuthenticationDataSource + private val dataSource: KakaoAuthRemoteDataSource ) : KakaoAuthenticationRepository { - override suspend fun refreshKakaoToken(authData: AuthData): AuthData { + override suspend fun refreshKakaoToken(authData: KakaoAuthData): KakaoAuthData { val response = dataSource.refreshKakaoToken(authData.kakaoRefreshToken) if (!response.isSuccessful) { @@ -21,7 +21,7 @@ class KakaoAuthenticationRepositoryImpl @Inject constructor( response.body()?.let { return mapRefreshDtoToAuthData(it) } ?: let { - return AuthData("", 0L, "", null) + return KakaoAuthData("", 0L, "") } } } diff --git a/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ServerAuthenticationRepositoryImpl.kt b/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ServerAuthenticationRepositoryImpl.kt new file mode 100644 index 00000000..ea70a145 --- /dev/null +++ b/data/user/src/main/java/com/kappzzang/jeongsan/repositoryimpl/ServerAuthenticationRepositoryImpl.kt @@ -0,0 +1,47 @@ +package com.kappzzang.jeongsan.repositoryimpl + +import android.util.Log +import com.kappzzang.jeongsan.data.ServerAuthData +import com.kappzzang.jeongsan.datasource.ServerAuthRemoteDataSource +import com.kappzzang.jeongsan.mapper.TokenDataToServerAuthDataMapper +import com.kappzzang.jeongsan.repository.ServerAuthenticationRepository +import javax.inject.Inject + +class ServerAuthenticationRepositoryImpl @Inject constructor( + private val dataSource: ServerAuthRemoteDataSource +) : ServerAuthenticationRepository { + + override suspend fun loginToServer(email: String): Result = + dataSource.login(email).fold( + onSuccess = { tokenData -> + val serverAuthData = + TokenDataToServerAuthDataMapper.mapTokenDataToServerAuthData(tokenData) + Result.success(serverAuthData) + }, + onFailure = { exception -> + Log.e(TAG, exception.message, exception.cause) + Result.failure(exception) + } + ) + + override suspend fun registerToServer( + uuid: String, + nickname: String, + email: String, + profileImageUrl: String + ): Result = dataSource.register(uuid, nickname, email, profileImageUrl).fold( + onSuccess = { tokenData -> + val serverAuthData = + TokenDataToServerAuthDataMapper.mapTokenDataToServerAuthData(tokenData) + Result.success(serverAuthData) + }, + onFailure = { exception -> + Log.e(TAG, exception.message, exception.cause) + Result.failure(exception) + } + ) + + companion object { + private const val TAG = "ServerAuthenticationRepositoryImpl" + } +} diff --git a/data/user/src/test/java/com/kappzzang/jeongsan/KakaoOAuthTokenAuthDataMapperTest.kt b/data/user/src/test/java/com/kappzzang/jeongsan/KakaoOAuthTokenKakaoAuthDataMapperTest.kt similarity index 74% rename from data/user/src/test/java/com/kappzzang/jeongsan/KakaoOAuthTokenAuthDataMapperTest.kt rename to data/user/src/test/java/com/kappzzang/jeongsan/KakaoOAuthTokenKakaoAuthDataMapperTest.kt index 373ae5cb..f24bf94e 100644 --- a/data/user/src/test/java/com/kappzzang/jeongsan/KakaoOAuthTokenAuthDataMapperTest.kt +++ b/data/user/src/test/java/com/kappzzang/jeongsan/KakaoOAuthTokenKakaoAuthDataMapperTest.kt @@ -1,12 +1,12 @@ package com.kappzzang.jeongsan -import com.kappzzang.jeongsan.data.AuthData +import com.kappzzang.jeongsan.data.KakaoAuthData import com.kappzzang.jeongsan.entity.KakaoRefreshTokenResponseDTO -import com.kappzzang.jeongsan.mapper.KakaoOAuthTokenAuthDataMapper +import com.kappzzang.jeongsan.mapper.KakaoOAuthTokenKakaoAuthDataMapper import org.assertj.core.api.Assertions.assertThat import org.junit.Test -class KakaoOAuthTokenAuthDataMapperTest { +class KakaoOAuthTokenKakaoAuthDataMapperTest { private fun getSampleDTO() = KakaoRefreshTokenResponseDTO( refreshToken = null, tokenType = "type", @@ -26,7 +26,7 @@ class KakaoOAuthTokenAuthDataMapperTest { ) // when - val mapped = KakaoOAuthTokenAuthDataMapper.mapRefreshDtoToAuthData(responseDto, null) + val mapped = KakaoOAuthTokenKakaoAuthDataMapper.mapRefreshDtoToAuthData(responseDto, null) // then assertThat(mapped.kakaoRefreshToken).isEqualTo(refreshToken) @@ -41,15 +41,15 @@ class KakaoOAuthTokenAuthDataMapperTest { val responseDto = getSampleDTO().copy( accessToken = newAccessToken ) - val authData = AuthData( + val authData = KakaoAuthData( kakaoAccessToken = "oldAccessToken", kakaoRefreshToken = "refreshToken", - accessTokenExpirationTime = outdatedExpirationTime, - jwt = "jwt" + accessTokenExpirationTime = outdatedExpirationTime ) // when - val mapped = KakaoOAuthTokenAuthDataMapper.mapRefreshDtoToAuthData(responseDto, authData) + val mapped = + KakaoOAuthTokenKakaoAuthDataMapper.mapRefreshDtoToAuthData(responseDto, authData) // then assertThat(mapped.kakaoAccessToken).isEqualTo(newAccessToken) @@ -62,19 +62,18 @@ class KakaoOAuthTokenAuthDataMapperTest { // given val refreshToken = "refresh" val jwt = "jwt" - val authData = AuthData( + val authData = KakaoAuthData( kakaoAccessToken = "", kakaoRefreshToken = refreshToken, - accessTokenExpirationTime = 0, - jwt = jwt + accessTokenExpirationTime = 0 ) val responseDTO = getSampleDTO() // when - val mapped = KakaoOAuthTokenAuthDataMapper.mapRefreshDtoToAuthData(responseDTO, authData) + val mapped = + KakaoOAuthTokenKakaoAuthDataMapper.mapRefreshDtoToAuthData(responseDTO, authData) // then - assertThat(mapped.jwt).isEqualTo(jwt) assertThat(mapped.kakaoRefreshToken).isEqualTo(refreshToken) } @@ -84,18 +83,18 @@ class KakaoOAuthTokenAuthDataMapperTest { val refreshToken = "refresh" val newRefreshToken = "newRefresh" - val authData = AuthData( + val authData = KakaoAuthData( kakaoAccessToken = "", kakaoRefreshToken = refreshToken, - accessTokenExpirationTime = 0, - jwt = "jwt" + accessTokenExpirationTime = 0 ) val responseDTO = getSampleDTO().copy( refreshToken = newRefreshToken ) // when - val mapped = KakaoOAuthTokenAuthDataMapper.mapRefreshDtoToAuthData(responseDTO, authData) + val mapped = + KakaoOAuthTokenKakaoAuthDataMapper.mapRefreshDtoToAuthData(responseDTO, authData) // then assertThat(mapped.kakaoRefreshToken).isEqualTo(newRefreshToken) diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/di/UserUseCaseModule.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/di/UserUseCaseModule.kt index 02b7d62f..ab0d4c8f 100644 --- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/di/UserUseCaseModule.kt +++ b/domain/common-user/src/main/java/com/kappzzang/jeongsan/di/UserUseCaseModule.kt @@ -1,11 +1,12 @@ package com.kappzzang.jeongsan.di import com.kappzzang.jeongsan.repository.KakaoAuthenticationRepository +import com.kappzzang.jeongsan.repository.ServerAuthenticationRepository import com.kappzzang.jeongsan.repository.UserInfoRepository import com.kappzzang.jeongsan.usecase.AuthenticateWithKakaoUseCase +import com.kappzzang.jeongsan.usecase.AuthenticateWithServerUseCase import com.kappzzang.jeongsan.usecase.AuthorizeWithKakaoUseCase import com.kappzzang.jeongsan.usecase.GetUserInfoUseCase -import com.kappzzang.jeongsan.usecase.RegisterWithKakaoUseCase import com.kappzzang.jeongsan.util.AuthenticationRepository import dagger.Module import dagger.Provides @@ -22,6 +23,12 @@ object UserUseCaseModule { kakaoAuthenticationRepository: KakaoAuthenticationRepository ) = AuthenticateWithKakaoUseCase(authenticationRepository, kakaoAuthenticationRepository) + @Provides + fun provideAuthenticateWithServerUseCase( + authenticationRepository: AuthenticationRepository, + serverAuthenticationRepository: ServerAuthenticationRepository + ) = AuthenticateWithServerUseCase(authenticationRepository, serverAuthenticationRepository) + @Provides fun provideAuthorizeWithKakaoUseCase(authenticationRepository: AuthenticationRepository) = AuthorizeWithKakaoUseCase(authenticationRepository) @@ -29,7 +36,4 @@ object UserUseCaseModule { @Provides fun provideGetUserInfoUseCase(userInfoRepository: UserInfoRepository) = GetUserInfoUseCase(userInfoRepository) - - @Provides - fun registerWithKakaoUseCase() = RegisterWithKakaoUseCase() } diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/model/AuthenticationResult.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/model/AuthenticationResult.kt index 11b220f6..5d63f3bd 100644 --- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/model/AuthenticationResult.kt +++ b/domain/common-user/src/main/java/com/kappzzang/jeongsan/model/AuthenticationResult.kt @@ -1,9 +1,9 @@ package com.kappzzang.jeongsan.model -import com.kappzzang.jeongsan.data.AuthData +import com.kappzzang.jeongsan.data.KakaoAuthData sealed class AuthenticationResult { - data class AuthenticationSuccess(val authData: AuthData) : AuthenticationResult() + data class AuthenticationSuccess(val authData: KakaoAuthData) : AuthenticationResult() data object NoToken : AuthenticationResult() diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/model/UserItem.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/model/UserItem.kt index 13ee69cb..39355567 100644 --- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/model/UserItem.kt +++ b/domain/common-user/src/main/java/com/kappzzang/jeongsan/model/UserItem.kt @@ -1,3 +1,3 @@ package com.kappzzang.jeongsan.model -data class UserItem(val uuid: String, val name: String, val profileUrl: String) +data class UserItem(val uuid: String, val name: String, val email: String, val profileUrl: String) diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/KakaoAuthenticationRepository.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/KakaoAuthenticationRepository.kt index f0e868c5..7fb52dd7 100644 --- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/KakaoAuthenticationRepository.kt +++ b/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/KakaoAuthenticationRepository.kt @@ -1,7 +1,7 @@ package com.kappzzang.jeongsan.repository -import com.kappzzang.jeongsan.data.AuthData +import com.kappzzang.jeongsan.data.KakaoAuthData interface KakaoAuthenticationRepository { - suspend fun refreshKakaoToken(authData: AuthData): AuthData + suspend fun refreshKakaoToken(authData: KakaoAuthData): KakaoAuthData } diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/KakaoAuthorizationRepository.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/KakaoAuthorizationRepository.kt deleted file mode 100644 index a57c9b47..00000000 --- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/repository/KakaoAuthorizationRepository.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.kappzzang.jeongsan.repository - -interface KakaoAuthorizationRepository { - suspend fun getAuthorizationToken(): String -} 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 f0c88add..1695bd52 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 @@ -1,11 +1,14 @@ package com.kappzzang.jeongsan.repository -import com.kappzzang.jeongsan.data.AuthData +import com.kappzzang.jeongsan.data.ServerAuthData interface ServerAuthenticationRepository { - fun registerToServer(authData: AuthData) - - fun getJwtFromServer(authData: AuthData): AuthData - - fun getSavedJwt(): String + suspend fun loginToServer(email: String): Result + + suspend fun registerToServer( + uuid: String, + nickname: String, + email: String, + profileImageUrl: String + ): Result } diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthenticateWithKakaoUseCase.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthenticateWithKakaoUseCase.kt index b20f674d..47007bdc 100644 --- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthenticateWithKakaoUseCase.kt +++ b/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthenticateWithKakaoUseCase.kt @@ -1,52 +1,50 @@ package com.kappzzang.jeongsan.usecase -import com.kappzzang.jeongsan.data.AuthData +import com.kappzzang.jeongsan.data.KakaoAuthData import com.kappzzang.jeongsan.model.AuthenticationResult import com.kappzzang.jeongsan.repository.KakaoAuthenticationRepository import com.kappzzang.jeongsan.util.AuthenticationRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map class AuthenticateWithKakaoUseCase @Inject constructor( private val authenticationRepository: AuthenticationRepository, private val kakaoAuthenticationRepository: KakaoAuthenticationRepository ) { - private fun updateAccessToken(old: AuthData, new: AuthData): AuthData = + private fun updateAccessToken(old: KakaoAuthData, new: KakaoAuthData): KakaoAuthData = if (new.kakaoRefreshToken.isEmpty()) { new.copy( - kakaoRefreshToken = old.kakaoRefreshToken, - jwt = old.jwt + kakaoRefreshToken = old.kakaoRefreshToken ) } else { - new.copy( - jwt = old.jwt - ) + new.copy() } private fun getCurrentTime(): Long = System.currentTimeMillis() - private fun checkNeedToRefresh(data: AuthData): Boolean = + private fun checkNeedToRefresh(data: KakaoAuthData): Boolean = (data.accessTokenExpirationTime - getCurrentTime()) < REFRESH_TIME_WITHIN_MILLISECONDS - private fun checkIsEmptyAuthData(authData: AuthData): Boolean = authData.kakaoAccessToken == "" + private fun checkIsEmptyAuthData(authData: KakaoAuthData): Boolean = + authData.kakaoAccessToken == "" - operator fun invoke(): Flow { - val authDataFlow = authenticationRepository.getAuthData() + operator fun invoke(): Flow = flow { + val authData = authenticationRepository.getKakaoAuthData() + emit(authData) + }.map { authData -> + if (checkIsEmptyAuthData(authData)) { + AuthenticationResult.NoToken + } else { + if (checkNeedToRefresh(authData)) { + val newData = kakaoAuthenticationRepository.refreshKakaoToken(authData) + val updateData = updateAccessToken(authData, newData) - return authDataFlow.map { authData -> - if (checkIsEmptyAuthData(authData)) { - AuthenticationResult.NoToken + authenticationRepository.updateKakaoAuthData(updateData) + AuthenticationResult.AuthenticationSuccess(updateData) } else { - if (checkNeedToRefresh(authData)) { - val newData = kakaoAuthenticationRepository.refreshKakaoToken(authData) - val updateData = updateAccessToken(authData, newData) - - authenticationRepository.updateAuthData(updateData) - AuthenticationResult.AuthenticationSuccess(updateData) - } else { - AuthenticationResult.AuthenticationSuccess(authData) - } + AuthenticationResult.AuthenticationSuccess(authData) } } } diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthenticateWithServerUseCase.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthenticateWithServerUseCase.kt new file mode 100644 index 00000000..994911b6 --- /dev/null +++ b/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthenticateWithServerUseCase.kt @@ -0,0 +1,43 @@ +package com.kappzzang.jeongsan.usecase + +import com.kappzzang.jeongsan.data.ServerAuthData +import com.kappzzang.jeongsan.repository.ServerAuthenticationRepository +import com.kappzzang.jeongsan.util.AuthenticationRepository +import javax.inject.Inject + +class AuthenticateWithServerUseCase @Inject constructor( + private val authenticationRepository: AuthenticationRepository, + private val serverAuthenticationRepository: ServerAuthenticationRepository +) { + + suspend operator fun invoke( + uuid: String, + nickname: String, + email: String, + profileImageUrl: String + ) { + val authData = attemptLoginOrRegister(uuid, nickname, email, profileImageUrl) + authenticationRepository.updateServerAuthData(authData) + } + + private suspend fun attemptLoginOrRegister( + uuid: String, + nickname: String, + email: String, + profileImageUrl: String + ): ServerAuthData = serverAuthenticationRepository.loginToServer(email).getOrElse { exception -> + when (exception) { + is NoSuchElementException -> registerToServer(uuid, nickname, email, profileImageUrl) + else -> throw exception + } + } + + private suspend fun registerToServer( + uuid: String, + nickname: String, + email: String, + profileImageUrl: String + ): ServerAuthData = + serverAuthenticationRepository.registerToServer(uuid, nickname, email, profileImageUrl) + .getOrThrow() +} diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthorizeWithKakaoUseCase.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthorizeWithKakaoUseCase.kt index b96d71da..bce6a48f 100644 --- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthorizeWithKakaoUseCase.kt +++ b/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/AuthorizeWithKakaoUseCase.kt @@ -1,15 +1,13 @@ package com.kappzzang.jeongsan.usecase -import com.kappzzang.jeongsan.data.AuthData +import com.kappzzang.jeongsan.data.KakaoAuthData import com.kappzzang.jeongsan.util.AuthenticationRepository import javax.inject.Inject class AuthorizeWithKakaoUseCase @Inject constructor( private val authenticationRepository: AuthenticationRepository ) { - suspend operator fun invoke(authData: AuthData) { - authenticationRepository.updateAuthData( - authData - ) + suspend operator fun invoke(authData: KakaoAuthData) { + authenticationRepository.updateKakaoAuthData(authData) } } diff --git a/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/RegisterWithKakaoUseCase.kt b/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/RegisterWithKakaoUseCase.kt deleted file mode 100644 index a9a83366..00000000 --- a/domain/common-user/src/main/java/com/kappzzang/jeongsan/usecase/RegisterWithKakaoUseCase.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.kappzzang.jeongsan.usecase - -class RegisterWithKakaoUseCase diff --git a/domain/common-user/src/test/java/com/kappzzang/jeongsan/AuthenticateWithKakaoUseCaseTest.kt b/domain/common-user/src/test/java/com/kappzzang/jeongsan/AuthenticateWithKakaoUseCaseTest.kt index 9c73b78b..9454f428 100644 --- a/domain/common-user/src/test/java/com/kappzzang/jeongsan/AuthenticateWithKakaoUseCaseTest.kt +++ b/domain/common-user/src/test/java/com/kappzzang/jeongsan/AuthenticateWithKakaoUseCaseTest.kt @@ -1,6 +1,6 @@ package com.kappzzang.jeongsan -import com.kappzzang.jeongsan.data.AuthData +import com.kappzzang.jeongsan.data.KakaoAuthData import com.kappzzang.jeongsan.model.AuthenticationResult import com.kappzzang.jeongsan.repository.KakaoAuthenticationRepository import com.kappzzang.jeongsan.usecase.AuthenticateWithKakaoUseCase @@ -26,14 +26,15 @@ class AuthenticateWithKakaoUseCaseTest { @Test fun `Access 토큰이 존재하지 않으면 NoToken을 리턴한다`() { // given - val emptyAuthData = AuthData( - jwt = null, + val emptyAuthData = KakaoAuthData( kakaoAccessToken = "", kakaoRefreshToken = "", accessTokenExpirationTime = 0 ) - every { mockAuthenticationRepository.getAuthData() } returns flow { emit(emptyAuthData) } + every { mockAuthenticationRepository.getKakaoAuthData() } returns flow { + emit(emptyAuthData) + } // when val useCase = AuthenticateWithKakaoUseCase( @@ -52,14 +53,13 @@ class AuthenticateWithKakaoUseCaseTest { fun `만료 기한이 얼마 남지 않은 AccessToken은 Refresh하여 리턴한다`() { // given val expirationTime = System.currentTimeMillis() + 1000L * 60 - val authData = AuthData( - jwt = null, + val authData = KakaoAuthData( kakaoAccessToken = "token", kakaoRefreshToken = "refresh", accessTokenExpirationTime = expirationTime ) - every { mockAuthenticationRepository.getAuthData() } returns flow { emit(authData) } + every { mockAuthenticationRepository.getKakaoAuthData() } returns flow { emit(authData) } // when val useCase = AuthenticateWithKakaoUseCase( @@ -84,21 +84,19 @@ class AuthenticateWithKakaoUseCaseTest { val oldJwt = "jwt" val newExpirationTime = System.currentTimeMillis() + 500_000L - val authData = AuthData( - jwt = oldJwt, + val authData = KakaoAuthData( kakaoAccessToken = "oldAccessToken", kakaoRefreshToken = "refreshToken", accessTokenExpirationTime = System.currentTimeMillis() ) - val newData = AuthData( - jwt = "", + val newData = KakaoAuthData( kakaoAccessToken = newAccessToken, kakaoRefreshToken = newRefreshToken, accessTokenExpirationTime = newExpirationTime ) - every { mockAuthenticationRepository.getAuthData() } returns flow { emit(authData) } + every { mockAuthenticationRepository.getKakaoAuthData() } returns flow { emit(authData) } coEvery { mockKakaoAuthenticationRepository.refreshKakaoToken(any()) } returns newData // when @@ -116,7 +114,6 @@ class AuthenticateWithKakaoUseCaseTest { assertThat(resultAuthData?.kakaoAccessToken).isEqualTo(newAccessToken) assertThat(resultAuthData?.kakaoRefreshToken).isEqualTo(newRefreshToken) assertThat(resultAuthData?.accessTokenExpirationTime).isEqualTo(newExpirationTime) - assertThat(resultAuthData?.jwt).isEqualTo(oldJwt) } } } diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/di/ExpenseUseCaseModule.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/di/ExpenseUseCaseModule.kt index 62137777..e2aeea59 100644 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/di/ExpenseUseCaseModule.kt +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/di/ExpenseUseCaseModule.kt @@ -2,10 +2,12 @@ package com.kappzzang.jeongsan.di import com.kappzzang.jeongsan.repository.ExpenseDetailRepository import com.kappzzang.jeongsan.repository.ExpenseRepository -import com.kappzzang.jeongsan.repository.ReceiptRepository +import com.kappzzang.jeongsan.repository.TransferRepository +import com.kappzzang.jeongsan.repository.UserInfoRepository import com.kappzzang.jeongsan.usecase.EditExpenseDetailUseCase import com.kappzzang.jeongsan.usecase.GetExpenseDetailUseCase -import com.kappzzang.jeongsan.usecase.GetExpenseUseCase +import com.kappzzang.jeongsan.usecase.GetTransferInfoUseCase +import com.kappzzang.jeongsan.usecase.SendTransferMessageUseCase import com.kappzzang.jeongsan.usecase.UploadExpenseUseCase import dagger.Module import dagger.Provides @@ -19,15 +21,21 @@ object ExpenseUseCaseModule { fun provideGetExpenseDetailUseCase(expenseDetailRepository: ExpenseDetailRepository) = GetExpenseDetailUseCase(expenseDetailRepository) - @Provides - fun provideGetExpenseUseCase(expenseRepository: ExpenseRepository) = - GetExpenseUseCase(expenseRepository) - @Provides fun provideEditExpenseDetailUseCase(expenseDetailRepository: ExpenseDetailRepository) = EditExpenseDetailUseCase(expenseDetailRepository) @Provides - fun provideUploadExpenseUseCase(receiptRepository: ReceiptRepository) = - UploadExpenseUseCase(receiptRepository) + fun provideUploadExpenseUseCase(expenseRepository: ExpenseRepository) = + UploadExpenseUseCase(expenseRepository) + + @Provides + fun provideGetTransferInfoUseCase(transferRepository: TransferRepository) = + GetTransferInfoUseCase(transferRepository) + + @Provides + fun provideSendTransferMessageUseCase( + userInfoRepository: UserInfoRepository, + transferRepository: TransferRepository + ) = SendTransferMessageUseCase(userInfoRepository, transferRepository) } diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseCategory.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseCategory.kt new file mode 100644 index 00000000..9d00b2a6 --- /dev/null +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseCategory.kt @@ -0,0 +1,3 @@ +package com.kappzzang.jeongsan.model + +data class ExpenseCategory(val id: String, val color: String, val name: String) 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 index cd569359..c3440646 100644 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithCategory.kt +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/model/ExpenseItemWithCategory.kt @@ -5,7 +5,8 @@ import java.time.LocalDateTime data class ExpenseItemWithCategory( private val item: ExpenseItem, val categoryColor: String, - val date: LocalDateTime + val date: LocalDateTime, + val payerUuid: String ) { val id get() = item.id @@ -20,7 +21,8 @@ data class ExpenseItemWithCategory( val EMPTY = ExpenseItemWithCategory( item = ExpenseItem.EMPTY, categoryColor = "", - date = LocalDateTime.now() + date = LocalDateTime.now(), + payerUuid = "" ) } } 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 fad3b92b..91cf189e 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 @@ -4,7 +4,7 @@ import java.time.LocalDateTime data class ReceiptItem( val title: String, - val categoryColor: String, + val categoryId: String, val imageBase64: String?, val expenseDetailItemList: List, val paymentTime: LocalDateTime = LocalDateTime.now() diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ExpenseDetailRepository.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ExpenseDetailRepository.kt index 4dfd6fb0..9a59ef33 100644 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ExpenseDetailRepository.kt +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ExpenseDetailRepository.kt @@ -1,8 +1,13 @@ package com.kappzzang.jeongsan.repository import com.kappzzang.jeongsan.model.ExpenseDetailItem +import com.kappzzang.jeongsan.model.ExpenseItemWithDetails interface ExpenseDetailRepository { - suspend fun getExpenseDetail(): List - suspend fun saveExpenseDetail(edited: List) + suspend fun getExpenseDetail(expenseId: String): Result + suspend fun saveExpenseDetail( + edited: List, + expenseId: String, + groupId: String + ): Result } 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 c8abf0b3..9d81f0ce 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,8 +1,9 @@ package com.kappzzang.jeongsan.repository -import com.kappzzang.jeongsan.model.ExpenseItemWithDetails +import com.kappzzang.jeongsan.model.ExpenseCategory import com.kappzzang.jeongsan.model.ExpenseListResponse import com.kappzzang.jeongsan.model.ExpenseState +import com.kappzzang.jeongsan.model.ReceiptItem import kotlinx.coroutines.flow.Flow interface ExpenseRepository { @@ -13,6 +14,14 @@ interface ExpenseRepository { * @param expenseState 조회할 지출의 상태 (정산 중, 송금 요청, 송금 완료 ...) * @return 지출 목록 response flow */ - fun getExpenseList(groupId: String, expenseState: ExpenseState): Flow - suspend fun getExpense(id: Long): ExpenseItemWithDetails + fun getExpenseList( + groupId: String, + expenseState: ExpenseState + ): Flow> + + suspend fun getExpenseListToGetPaid(groupId: String): Result + + suspend fun uploadExpense(receiptItem: ReceiptItem, groupId: String): Result + + suspend fun getExpenseCategoryList(): Result> } diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ReceiptRepository.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ReceiptRepository.kt deleted file mode 100644 index 7eb46d08..00000000 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/repository/ReceiptRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.kappzzang.jeongsan.repository - -import com.kappzzang.jeongsan.model.ReceiptItem - -interface ReceiptRepository { - // 업로드 요청 후, 지출 id를 받아옴 - suspend fun uploadExpense(receiptItem: ReceiptItem): String -} diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/EditExpenseDetailUseCase.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/EditExpenseDetailUseCase.kt index 57250356..837b1f2c 100644 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/EditExpenseDetailUseCase.kt +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/EditExpenseDetailUseCase.kt @@ -4,7 +4,9 @@ import com.kappzzang.jeongsan.model.ExpenseDetailItem import com.kappzzang.jeongsan.repository.ExpenseDetailRepository class EditExpenseDetailUseCase(private val expenseDetailRepository: ExpenseDetailRepository) { - suspend operator fun invoke(edited: List) { - expenseDetailRepository.saveExpenseDetail(edited) - } + suspend operator fun invoke( + edited: List, + expenseId: String, + groupId: String + ): Result = expenseDetailRepository.saveExpenseDetail(edited, expenseId, groupId) } diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetCategoryListUseCase.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetCategoryListUseCase.kt new file mode 100644 index 00000000..1180d9b4 --- /dev/null +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetCategoryListUseCase.kt @@ -0,0 +1,10 @@ +package com.kappzzang.jeongsan.usecase + +import com.kappzzang.jeongsan.model.ExpenseCategory +import com.kappzzang.jeongsan.repository.ExpenseRepository +import javax.inject.Inject + +class GetCategoryListUseCase @Inject constructor(private val repository: ExpenseRepository) { + suspend operator fun invoke(): Result> = + repository.getExpenseCategoryList() +} diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseDetailUseCase.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseDetailUseCase.kt index 8662f917..e0e842d7 100644 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseDetailUseCase.kt +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseDetailUseCase.kt @@ -1,9 +1,9 @@ package com.kappzzang.jeongsan.usecase -import com.kappzzang.jeongsan.model.ExpenseDetailItem +import com.kappzzang.jeongsan.model.ExpenseItemWithDetails import com.kappzzang.jeongsan.repository.ExpenseDetailRepository class GetExpenseDetailUseCase(private val expenseDetailRepository: ExpenseDetailRepository) { - suspend operator fun invoke(): List = - expenseDetailRepository.getExpenseDetail() + suspend operator fun invoke(expenseId: String): Result = + expenseDetailRepository.getExpenseDetail(expenseId) } diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseListUseCase.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseListUseCase.kt index 4ea26058..3156a177 100644 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseListUseCase.kt +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseListUseCase.kt @@ -10,7 +10,7 @@ class GetExpenseListUseCase @Inject constructor(private val repository: ExpenseR operator fun invoke( groupId: String, queryExpenseState: ExpenseState - ): Flow = repository.getExpenseList( + ): Flow> = repository.getExpenseList( groupId, queryExpenseState ) diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseUseCase.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseUseCase.kt deleted file mode 100644 index 22f24b70..00000000 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/GetExpenseUseCase.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.kappzzang.jeongsan.usecase - -import com.kappzzang.jeongsan.repository.ExpenseRepository - -class GetExpenseUseCase(private val expenseRepository: ExpenseRepository) { - suspend operator fun invoke(expenseId: Long) = expenseRepository.getExpense(expenseId) -} diff --git a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/UploadExpenseUseCase.kt b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/UploadExpenseUseCase.kt index 5dbe4163..7739af70 100644 --- a/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/UploadExpenseUseCase.kt +++ b/domain/expense/src/main/java/com/kappzzang/jeongsan/usecase/UploadExpenseUseCase.kt @@ -1,10 +1,10 @@ package com.kappzzang.jeongsan.usecase import com.kappzzang.jeongsan.model.ReceiptItem -import com.kappzzang.jeongsan.repository.ReceiptRepository +import com.kappzzang.jeongsan.repository.ExpenseRepository import javax.inject.Inject -class UploadExpenseUseCase @Inject constructor(private val receiptRepository: ReceiptRepository) { - suspend operator fun invoke(receiptItem: ReceiptItem): String = - receiptRepository.uploadExpense(receiptItem) +class UploadExpenseUseCase @Inject constructor(private val expenseRepository: ExpenseRepository) { + suspend operator fun invoke(receiptItem: ReceiptItem, groupId: String): Result = + expenseRepository.uploadExpense(receiptItem, groupId) } diff --git a/domain/expense/src/test/java/com/kappzzang/jeongsan/SendTransferMessageUseCaseTest.kt b/domain/expense/src/test/java/com/kappzzang/jeongsan/SendTransferMessageUseCaseTest.kt index 8eeacb2b..766d6d04 100644 --- a/domain/expense/src/test/java/com/kappzzang/jeongsan/SendTransferMessageUseCaseTest.kt +++ b/domain/expense/src/test/java/com/kappzzang/jeongsan/SendTransferMessageUseCaseTest.kt @@ -104,7 +104,8 @@ class SendTransferMessageUseCaseTest { UserItem( name = "sampleUser", uuid = "1234", - profileUrl = "https://example.org/" + profileUrl = "https://example.org/", + email = "example@domain.com" ) val sampleTransferLink = diff --git a/domain/group/src/main/java/com/kappzzang/jeongsan/model/GroupCreateItem.kt b/domain/group/src/main/java/com/kappzzang/jeongsan/model/GroupCreateItem.kt index dc5ec9f2..19be3cf4 100644 --- a/domain/group/src/main/java/com/kappzzang/jeongsan/model/GroupCreateItem.kt +++ b/domain/group/src/main/java/com/kappzzang/jeongsan/model/GroupCreateItem.kt @@ -1,3 +1,3 @@ package com.kappzzang.jeongsan.model -data class GroupCreateItem(val name: String, val subject: String, val memberIdList: List) +data class GroupCreateItem(val name: String, val subject: String, val memberUuidList: List) diff --git a/domain/group/src/main/java/com/kappzzang/jeongsan/repository/GroupInfoRepository.kt b/domain/group/src/main/java/com/kappzzang/jeongsan/repository/GroupInfoRepository.kt index 2f7365de..e3af9f79 100644 --- a/domain/group/src/main/java/com/kappzzang/jeongsan/repository/GroupInfoRepository.kt +++ b/domain/group/src/main/java/com/kappzzang/jeongsan/repository/GroupInfoRepository.kt @@ -14,7 +14,7 @@ interface GroupInfoRepository { * @param groupId 그룹명 * @return 그룹 정보 */ - fun getGroupInfo(groupId: String): Flow + fun getTargetGroupInfo(groupId: String): Flow - suspend fun uploadGroupInfo(createdGroup: GroupCreateItem) + suspend fun uploadGroupInfo(createdGroup: GroupCreateItem): Long } diff --git a/domain/group/src/main/java/com/kappzzang/jeongsan/repository/InviteRepository.kt b/domain/group/src/main/java/com/kappzzang/jeongsan/repository/InviteRepository.kt index 96652773..b351b5d4 100644 --- a/domain/group/src/main/java/com/kappzzang/jeongsan/repository/InviteRepository.kt +++ b/domain/group/src/main/java/com/kappzzang/jeongsan/repository/InviteRepository.kt @@ -1,5 +1,9 @@ package com.kappzzang.jeongsan.repository interface InviteRepository { - suspend fun sendInviteMessage(groupId: String, groupName: String, memberId: String): Boolean + suspend fun sendInviteMessage( + groupId: String, + groupName: String, + memberUuidList: List + ): Boolean } diff --git a/domain/group/src/main/java/com/kappzzang/jeongsan/repository/MemberRepository.kt b/domain/group/src/main/java/com/kappzzang/jeongsan/repository/MemberRepository.kt index 142e24be..2d0a09bd 100644 --- a/domain/group/src/main/java/com/kappzzang/jeongsan/repository/MemberRepository.kt +++ b/domain/group/src/main/java/com/kappzzang/jeongsan/repository/MemberRepository.kt @@ -3,6 +3,6 @@ package com.kappzzang.jeongsan.repository import com.kappzzang.jeongsan.model.MemberItem interface MemberRepository { - suspend fun addMember(member: MemberItem) - suspend fun getAllMember(): List + suspend fun addMember(groupId: String, memberId: String) + suspend fun getAllMember(groupId: String): List } diff --git a/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/GetCurrentGroupInfoUseCase.kt b/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/GetCurrentGroupInfoUseCase.kt index 7b17770a..b5dcca48 100644 --- a/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/GetCurrentGroupInfoUseCase.kt +++ b/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/GetCurrentGroupInfoUseCase.kt @@ -6,5 +6,5 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow class GetCurrentGroupInfoUseCase @Inject constructor(private val repository: GroupInfoRepository) { - operator fun invoke(groupId: String): Flow = repository.getGroupInfo(groupId) + operator fun invoke(groupId: String): Flow = repository.getTargetGroupInfo(groupId) } diff --git a/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/GetInviteInfoUseCase.kt b/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/GetInviteInfoUseCase.kt index 214c8a62..58f389db 100644 --- a/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/GetInviteInfoUseCase.kt +++ b/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/GetInviteInfoUseCase.kt @@ -4,19 +4,20 @@ import com.kappzzang.jeongsan.model.MemberItem import com.kappzzang.jeongsan.repository.MemberRepository class GetInviteInfoUseCase(private val memberRepository: MemberRepository) { - suspend operator fun invoke(): List = memberRepository.getAllMember() + suspend operator fun invoke(groupId: String): List = + memberRepository.getAllMember(groupId) // 더미 데이터 삽입용 임시 함수 suspend fun insertDummyData() { - for (i in 1..10) { - memberRepository.addMember( - MemberItem( - id = "id$i", - name = "멤버 이름$i", - profileImageUrl = "", - isInvited = i % 2 != 0 - ) - ) - } +// for (i in 1..10) { +// memberRepository.addMember( +// MemberItem( +// id = "id$i", +// name = "멤버 이름$i", +// profileImageUrl = "", +// isInvited = i % 2 != 0 +// ) +// ) +// } } } diff --git a/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/SendInviteMessageUseCase.kt b/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/SendInviteMessageUseCase.kt index 854b7e5d..efb5c122 100644 --- a/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/SendInviteMessageUseCase.kt +++ b/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/SendInviteMessageUseCase.kt @@ -4,10 +4,13 @@ import com.kappzzang.jeongsan.repository.InviteRepository import javax.inject.Inject class SendInviteMessageUseCase @Inject constructor(private val inviteRepository: InviteRepository) { - suspend operator fun invoke(groupId: String, groupName: String, memberId: String): Boolean = - inviteRepository.sendInviteMessage( - groupId, - groupName, - memberId - ) + suspend operator fun invoke( + groupId: String, + groupName: String, + memberUuidList: List + ): Boolean = inviteRepository.sendInviteMessage( + groupId, + groupName, + memberUuidList + ) } diff --git a/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/UploadGroupInfoUseCase.kt b/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/UploadGroupInfoUseCase.kt index 74f5b28e..0f7f5937 100644 --- a/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/UploadGroupInfoUseCase.kt +++ b/domain/group/src/main/java/com/kappzzang/jeongsan/usecase/UploadGroupInfoUseCase.kt @@ -7,7 +7,6 @@ import javax.inject.Inject class UploadGroupInfoUseCase @Inject constructor( private val groupInfoRepository: GroupInfoRepository ) { - suspend operator fun invoke(createdGroup: GroupCreateItem) { + suspend operator fun invoke(createdGroup: GroupCreateItem) = groupInfoRepository.uploadGroupInfo(createdGroup) - } } diff --git a/settings.gradle.kts b/settings.gradle.kts index fd6dd1bf..cf0a4a64 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,3 +46,5 @@ project(":data").children.forEach { module -> module.name = "data-${module.name} include(":common:navigation") include(":common:retrofit") include(":build-config") +include(":ui:sendmessage") +include(":common:dispatcher") diff --git a/ui/addexpense/build.gradle.kts b/ui/addexpense/build.gradle.kts index 43b8f29c..f50b147a 100644 --- a/ui/addexpense/build.gradle.kts +++ b/ui/addexpense/build.gradle.kts @@ -3,7 +3,12 @@ android { } dependencies { + implementation("androidx.navigation:navigation-fragment-ktx:2.8.1") + implementation("androidx.navigation:navigation-ui-ktx:2.8.1") + implementation("androidx.hilt:hilt-navigation-fragment:1.1.0") + implementation(project(":domain:ocr")) implementation(project(":domain:expense")) implementation(project(":ui:data")) + implementation(project(":common:dispatcher")) } diff --git a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseActivity.kt b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseActivity.kt index 41137c79..73019bf9 100644 --- a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseActivity.kt +++ b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseActivity.kt @@ -14,10 +14,11 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager +import com.kappzzang.jeongsan.addexpense.colorpicker.ColorPickerDialog import com.kappzzang.jeongsan.addexpense.databinding.ActivityAddExpenseBinding import com.kappzzang.jeongsan.intentcontract.AddExpenseContract import com.kappzzang.jeongsan.model.OcrResultResponse -import com.kappzzang.jeongsan.navigation.AppNavigator +import com.kappzzang.jeongsan.navigation.ExpenseDetailNavigator import com.kappzzang.jeongsan.util.Base64BitmapEncoder import com.kappzzang.jeongsan.util.IntentHelper.getParcelableData import dagger.hilt.android.AndroidEntryPoint @@ -27,7 +28,10 @@ import kotlinx.coroutines.launch @AndroidEntryPoint class AddExpenseActivity : AppCompatActivity() { @Inject - lateinit var appNavigator: AppNavigator + lateinit var appNavigator: ExpenseDetailNavigator + private val colorPickerDialogFragment: ColorPickerDialog by lazy { + createColorPickerDialogFragment() + } private val viewModel: AddExpenseViewModel by viewModels() private val binding: ActivityAddExpenseBinding by lazy { ActivityAddExpenseBinding.inflate( @@ -47,16 +51,14 @@ class AddExpenseActivity : AppCompatActivity() { initiateRecyclerView() setContentView(binding.root) - // TODO: 임시 연결용 코드 binding.addexpenseSubmitButton.setOnClickListener { - if (viewModel.uploadExpense()) { - startActivity(appNavigator.navigateToExpenseDetail(this)) - finish() - return@setOnClickListener + if (!viewModel.uploadExpense()) { + Toast.makeText( + this, + getString(R.string.add_expense_complete_form_notify), + Toast.LENGTH_SHORT + ).show() } - - // TODO: 값이 완전히 채워지지 않은 경우 - Toast.makeText(this, "지출 내역을 완성해주세요!", Toast.LENGTH_SHORT).show() } lifecycleScope.launch { @@ -66,6 +68,55 @@ class AddExpenseActivity : AppCompatActivity() { } } } + subscribeExpenseUploadState() + binding.expenseSelectedCategory.setOnClickListener { + showColorPickerDialog() + } + } + + private fun createColorPickerDialogFragment(): ColorPickerDialog { + val dialog = ColorPickerDialog() + dialog.dialog?.setCanceledOnTouchOutside(true) + return dialog + } + + private fun subscribeExpenseUploadState() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.uploadingProgress.collect { state -> + when (state) { + ExpenseUploadingProgress.NOT_STARTED -> { + viewModel.setInputsLock(false) + } + ExpenseUploadingProgress.UPLOADING -> { + viewModel.setInputsLock(true) + } + ExpenseUploadingProgress.UPLOAD_SUCCESS -> { + startExpenseDetailActivityAndFinish() + } + ExpenseUploadingProgress.UPLOAD_FAILED -> { + viewModel.setInputsLock(false) + } + } + } + } + } + } + + private fun startExpenseDetailActivityAndFinish() { + startActivity( + appNavigator.navigateToExpenseDetail( + packageContext = this, + groupId = viewModel.groupId.value, + expenseId = viewModel.createdExpenseId.value, + editable = true + ) + ) + finish() + } + + private fun showColorPickerDialog() { + colorPickerDialogFragment.show(supportFragmentManager, "colorPickerDialog") } private fun updateExpenseImage(imageBitmap: Bitmap?) { @@ -89,6 +140,7 @@ class AddExpenseActivity : AppCompatActivity() { } private fun initiateViewModel() { + getGroupId() if (checkIfReceiptMode()) { viewModel.setManualMode(AddExpenseViewModel.Companion.ManualMode.RECEIPT) getExpenseData() @@ -128,6 +180,22 @@ class AddExpenseActivity : AppCompatActivity() { } } + private fun getGroupId() { + val groupId = intent?.getParcelableData( + AddExpenseContract.GROUP_ID + ) + + groupId?.let { + viewModel.initGroupId(it) + } ?: let { + Toast.makeText( + this, + getString(R.string.add_expense_error_message_load_group_info), + Toast.LENGTH_LONG + ).show() + } + } + private fun getExpenseData() { val intentData = intent?.getParcelableData( AddExpenseContract.EXPENSE_DATA diff --git a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseBindingAdapter.kt b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseBindingAdapter.kt index 82fdb13f..b069d9a2 100644 --- a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseBindingAdapter.kt +++ b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/AddExpenseBindingAdapter.kt @@ -1,11 +1,15 @@ package com.kappzzang.jeongsan.addexpense +import android.graphics.Color import android.widget.EditText +import android.widget.ImageView import androidx.core.widget.addTextChangedListener import androidx.databinding.BindingAdapter import androidx.databinding.InverseBindingAdapter import androidx.databinding.InverseBindingListener import androidx.recyclerview.widget.RecyclerView +import com.kappzzang.jeongsan.addexpense.colorpicker.CategoryListAdapter +import com.kappzzang.jeongsan.data.ExpenseCategoryUIItem import kotlinx.coroutines.flow.StateFlow object AddExpenseBindingAdapter { @@ -61,4 +65,29 @@ object AddExpenseBindingAdapter { (recyclerView.adapter as? ExpenseItemListAdapter)?.submitList(it.value) } } + + @BindingAdapter("imageColor") + @JvmStatic + fun ImageView.setImageColor(backgroundColor: String) { + val color: Int = try { + Color.parseColor(backgroundColor) + } catch (e: Exception) { + Color.parseColor("#$backgroundColor") + } + setColorFilter(color) + } + + @BindingAdapter("categoryItems") + @JvmStatic + fun attachExpenseList( + recyclerView: RecyclerView, + items: StateFlow> + ) { + items.let { + (recyclerView.adapter as? CategoryListAdapter) + ?.submitList( + it.value + ) + } + } } 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 73706713..e5729dea 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 @@ -1,16 +1,17 @@ package com.kappzzang.jeongsan.addexpense import android.graphics.Bitmap -import android.util.Base64 import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kappzzang.jeongsan.data.ExpenseItemInput +import com.kappzzang.jeongsan.model.ExpenseCategory import com.kappzzang.jeongsan.model.OcrResultResponse import com.kappzzang.jeongsan.model.ReceiptDetailItem import com.kappzzang.jeongsan.model.ReceiptItem +import com.kappzzang.jeongsan.usecase.GetCategoryListUseCase import com.kappzzang.jeongsan.usecase.UploadExpenseUseCase +import com.kappzzang.jeongsan.util.Base64BitmapEncoder import dagger.hilt.android.lifecycle.HiltViewModel -import java.io.ByteArrayOutputStream import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -19,10 +20,13 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +enum class ExpenseUploadingProgress { NOT_STARTED, UPLOADING, UPLOAD_SUCCESS, UPLOAD_FAILED } + @HiltViewModel class AddExpenseViewModel @Inject constructor( private val uploadExpenseUseCase: UploadExpenseUseCase, - private val ioDispatcher: CoroutineDispatcher + private val ioDispatcher: CoroutineDispatcher, + private val getCategoryListUseCase: GetCategoryListUseCase ) : ViewModel() { private val _expenseItemList by lazy { MutableStateFlow( @@ -32,16 +36,42 @@ class AddExpenseViewModel @Inject constructor( ) } + private val _selectedCategory = MutableStateFlow(ExpenseCategory("", "", "")) + private val _inputsLocked = MutableStateFlow(false) + private val _createdExpenseId = MutableStateFlow("") + private val _categoryList by lazy { getCategoryList() } + private val _uploadingProgress = MutableStateFlow(ExpenseUploadingProgress.NOT_STARTED) private val _expenseImageBitmap = MutableStateFlow(null) private val _manualMode = MutableStateFlow(true) private val _uploadedImage = MutableStateFlow(false) + private val _groupId = MutableStateFlow("") + val inputsLocked = _inputsLocked.asStateFlow() + val uploadingProgress = _uploadingProgress.asStateFlow() val expenseImageBitmap: StateFlow = _expenseImageBitmap.asStateFlow() val manualMode: StateFlow = _manualMode.asStateFlow() val uploadedImage: StateFlow = _uploadedImage.asStateFlow() val expenseItemList: StateFlow> = _expenseItemList.asStateFlow() val expenseName = MutableStateFlow("Demo") + val groupId = _groupId.asStateFlow() + val createdExpenseId = _createdExpenseId.asStateFlow() + + var selectedCategory = _selectedCategory.asStateFlow() + val categoryList = _categoryList.asStateFlow() + + private fun getCategoryList(): MutableStateFlow> { + val mList = MutableStateFlow>(emptyList()) + viewModelScope.launch(Dispatchers.IO) { + getCategoryListUseCase.invoke().onSuccess { + mList.emit(it) + it.lastOrNull()?.let { item -> + updateSelectedCategory(item.id) + } + } + } + return mList + } fun setManualMode(mode: ManualMode) { viewModelScope.launch(Dispatchers.Main) { @@ -67,10 +97,8 @@ class AddExpenseViewModel @Inject constructor( } } - private suspend fun insertExpenseItemList(expenseItemList: List) { - _expenseItemList.emit( - expenseItemList + _expenseItemList.value - ) + fun initGroupId(groupId: String) { + this._groupId.value = groupId } fun addNewExpense() { @@ -94,10 +122,15 @@ class AddExpenseViewModel @Inject constructor( if (!checkItemValid()) { return false } + if (uploadingProgress.value == ExpenseUploadingProgress.UPLOADING) { + return true + } + + _uploadingProgress.value = ExpenseUploadingProgress.UPLOADING val receiptItem = ReceiptItem( title = expenseName.value, - categoryColor = "#FF0000", // TODO: 카테고리 색을 넣도록 UI 수정 필요 + categoryId = selectedCategory.value.id, imageBase64 = convertBitmapToBase64(_expenseImageBitmap.value), expenseDetailItemList = _expenseItemList.value.subList( 0, @@ -112,21 +145,35 @@ class AddExpenseViewModel @Inject constructor( ) viewModelScope.launch(ioDispatcher) { - uploadExpenseUseCase(receiptItem) + uploadExpenseUseCase(receiptItem, _groupId.value) + .onSuccess { + _createdExpenseId.emit(it) + _uploadingProgress.emit(ExpenseUploadingProgress.UPLOAD_SUCCESS) + } + .onFailure { + _uploadingProgress.emit(ExpenseUploadingProgress.UPLOAD_FAILED) + } } return true } + fun setInputsLock(locked: Boolean) { + _inputsLocked.value = locked + } + fun convertBitmapToBase64(bitmap: Bitmap?): String? { if (bitmap == null) { return null } - val byteArrayOutputStream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream) - val byteArray = byteArrayOutputStream.toByteArray() - return Base64.encodeToString(byteArray, Base64.DEFAULT) + return Base64BitmapEncoder.convertBitmapToBase64String(bitmap) + } + + fun updateSelectedCategory(categoryId: String) { + categoryList.value.find { it.id == categoryId }?.let { + _selectedCategory.value = it + } } private fun checkItemValid(): Boolean { @@ -152,6 +199,7 @@ class AddExpenseViewModel @Inject constructor( } companion object { + const val UNDEFINED_COLOR = "#000000" enum class ManualMode { MANUAL, RECEIPT } } } diff --git a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/CategoryListAdapter.kt b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/CategoryListAdapter.kt new file mode 100644 index 00000000..8a6afbce --- /dev/null +++ b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/CategoryListAdapter.kt @@ -0,0 +1,55 @@ +package com.kappzzang.jeongsan.addexpense.colorpicker + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.kappzzang.jeongsan.addexpense.databinding.ItemExpenseCategoryBinding +import com.kappzzang.jeongsan.data.ExpenseCategoryUIItem + +class CategoryListAdapter(private val onCategoryItemClickListener: (categoryId: String) -> Unit) : + ListAdapter( + object : + DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ExpenseCategoryUIItem, + newItem: ExpenseCategoryUIItem + ): Boolean = oldItem.id == newItem.id + + override fun areContentsTheSame( + oldItem: ExpenseCategoryUIItem, + newItem: ExpenseCategoryUIItem + ): Boolean = oldItem == newItem + } + ) { + + inner class CategoryViewHolder( + private val binding: ItemExpenseCategoryBinding, + private val onExpenseItemClickListener: (expenseId: String) -> Unit + ) : RecyclerView.ViewHolder(binding.root) { + init { + binding.root.setOnClickListener { + onExpenseItemClickListener.invoke(binding.item?.id ?: "") + } + } + + fun bind(categoryItem: ExpenseCategoryUIItem) { + binding.item = categoryItem + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder = + CategoryViewHolder( + ItemExpenseCategoryBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ), + onCategoryItemClickListener + ) + + override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) { + holder.bind(currentList[position]) + } +} diff --git a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/ColorPickerDialog.kt b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/ColorPickerDialog.kt new file mode 100644 index 00000000..9054792e --- /dev/null +++ b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/ColorPickerDialog.kt @@ -0,0 +1,86 @@ +package com.kappzzang.jeongsan.addexpense.colorpicker + +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import com.kappzzang.jeongsan.addexpense.AddExpenseViewModel +import com.kappzzang.jeongsan.addexpense.databinding.DialogColorPickerBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ColorPickerDialog : DialogFragment() { + private val viewModel: ColorPickerViewModel by viewModels() + private val activityViewModel: AddExpenseViewModel by activityViewModels() + private lateinit var binding: DialogColorPickerBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DialogColorPickerBinding.inflate(inflater, container, false) + binding.viewModel = viewModel + binding.lifecycleOwner = this + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initiateRecyclerView() + observeSelectedCategoryId() + setDialogStyle() + initiateConfirmButton() + } + + private fun initiateConfirmButton() { + binding.categoryListConfirmButton.setOnClickListener { + dismiss() + } + } + + private fun initiateRecyclerView() { + binding.categoryListRecyclerview.apply { + adapter = CategoryListAdapter { + activityViewModel.updateSelectedCategory(it) + } + layoutManager = LinearLayoutManager(context) + } + } + + private fun setDialogStyle() { + dialog?.window?.let { window -> + window.setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) + + val width = (resources.displayMetrics.widthPixels * 0.8).toInt() + val height = (resources.displayMetrics.heightPixels * 0.7).toInt() + window.setLayout(width, height) + } + } + + private fun observeSelectedCategoryId() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + activityViewModel.selectedCategory.collect { + viewModel.updateSelectedItem(it) + } + } + } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + activityViewModel.categoryList.collect { + viewModel.updateUIItemList(it, activityViewModel.selectedCategory.value.id) + } + } + } + } +} diff --git a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/ColorPickerViewModel.kt b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/ColorPickerViewModel.kt new file mode 100644 index 00000000..34647ec5 --- /dev/null +++ b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/colorpicker/ColorPickerViewModel.kt @@ -0,0 +1,39 @@ +package com.kappzzang.jeongsan.addexpense.colorpicker + +import android.util.Log +import androidx.lifecycle.ViewModel +import com.kappzzang.jeongsan.data.ExpenseCategoryUIItem +import com.kappzzang.jeongsan.model.ExpenseCategory +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +@HiltViewModel +class ColorPickerViewModel @Inject constructor() : ViewModel() { + private val _expenseCategoryUIItemList = + MutableStateFlow>(emptyList()) + val expenseCategoryUIItemList = _expenseCategoryUIItemList.asStateFlow() + + fun updateUIItemList(expenseCategoryList: List, selectedId: String) { + Log.d("KSC", "dialog: ${expenseCategoryList.size}") + _expenseCategoryUIItemList.value = + expenseCategoryList.map { + ExpenseCategoryUIItem( + color = it.color, + name = it.name, + id = it.id, + selected = it.id == selectedId + ) + } + } + + fun updateSelectedItem(item: ExpenseCategory) { + _expenseCategoryUIItemList.value = + expenseCategoryUIItemList.value.map { + it.copy( + selected = it.id == item.id + ) + } + } +} diff --git a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/di/NavigatorModule.kt b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/di/NavigatorModule.kt new file mode 100644 index 00000000..e85398ce --- /dev/null +++ b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/di/NavigatorModule.kt @@ -0,0 +1,17 @@ +package com.kappzzang.jeongsan.addexpense.di + +import com.kappzzang.jeongsan.addexpense.navigation.AddExpenseNavigatorImpl +import com.kappzzang.jeongsan.navigation.AddExpenseNavigator +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +abstract class NavigatorModule { + @Binds + abstract fun bindExpenseListNavigator( + appNavigatorImpl: AddExpenseNavigatorImpl + ): AddExpenseNavigator +} diff --git a/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/navigation/AddExpenseNavigatorImpl.kt b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/navigation/AddExpenseNavigatorImpl.kt new file mode 100644 index 00000000..c29e7efe --- /dev/null +++ b/ui/addexpense/src/main/java/com/kappzzang/jeongsan/addexpense/navigation/AddExpenseNavigatorImpl.kt @@ -0,0 +1,53 @@ +package com.kappzzang.jeongsan.addexpense.navigation + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.kappzzang.jeongsan.addexpense.AddExpenseActivity +import com.kappzzang.jeongsan.intentcontract.AddExpenseContract +import com.kappzzang.jeongsan.model.OcrResultResponse +import com.kappzzang.jeongsan.navigation.AddExpenseNavigator +import javax.inject.Inject + +class AddExpenseNavigatorImpl @Inject constructor() : AddExpenseNavigator { + override fun navigateToAddExpenseWithImage( + packageContext: Context, + ocrResponse: OcrResultResponse.OcrSuccess, + image: Uri, + groupId: String + ): Intent { + val intent = Intent(packageContext, AddExpenseActivity::class.java) + intent.putExtra( + AddExpenseContract.INTENT_EXPENSE_MODE, + AddExpenseContract.EXPENSE_MODE_RECEIPT + ) + intent.putExtra( + AddExpenseContract.EXPENSE_IMAGE, + image + ) + intent.putExtra( + AddExpenseContract.EXPENSE_DATA, + ocrResponse + ) + intent.putExtra( + AddExpenseContract.GROUP_ID, + groupId + ) + + return intent + } + + override fun navigateToAddExpenseManually(packageContext: Context, groupId: String): Intent { + val intent = Intent(packageContext, AddExpenseActivity::class.java) + intent.putExtra( + AddExpenseContract.INTENT_EXPENSE_MODE, + AddExpenseContract.EXPENSE_MODE_MANUAL + ) + intent.putExtra( + AddExpenseContract.GROUP_ID, + groupId + ) + + return intent + } +} diff --git a/ui/addexpense/src/main/res/drawable/add_expense_color_dot.xml b/ui/addexpense/src/main/res/drawable/add_expense_color_dot.xml new file mode 100644 index 00000000..76f061c2 --- /dev/null +++ b/ui/addexpense/src/main/res/drawable/add_expense_color_dot.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/ui/addexpense/src/main/res/drawable/baseline_check_24.xml b/ui/addexpense/src/main/res/drawable/baseline_check_24.xml new file mode 100644 index 00000000..39dc6ab9 --- /dev/null +++ b/ui/addexpense/src/main/res/drawable/baseline_check_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/addexpense/src/main/res/layout/activity_add_expense.xml b/ui/addexpense/src/main/res/layout/activity_add_expense.xml index 8f6d8544..e4a33495 100644 --- a/ui/addexpense/src/main/res/layout/activity_add_expense.xml +++ b/ui/addexpense/src/main/res/layout/activity_add_expense.xml @@ -115,11 +115,42 @@ android:text="@={viewModel.expenseName}" android:textAlignment="center" android:textSize="24sp" + app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintRight_toRightOf="parent" + app:layout_constraintRight_toLeftOf="@+id/expense_selected_category" app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_percent=".5" /> + + + + + + + + + + app:layout_constraintStart_toStartOf="parent" + android:enabled="@{viewModel.inputsLocked?false:true}"/> diff --git a/ui/addexpense/src/main/res/layout/dialog_color_picker.xml b/ui/addexpense/src/main/res/layout/dialog_color_picker.xml new file mode 100644 index 00000000..c26362fc --- /dev/null +++ b/ui/addexpense/src/main/res/layout/dialog_color_picker.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + +