diff --git a/build-logic/convention/src/main/java/FeatureComposeConventionPlugin.kt b/build-logic/convention/src/main/java/FeatureComposeConventionPlugin.kt index 7d0374b0..1631be8d 100644 --- a/build-logic/convention/src/main/java/FeatureComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/java/FeatureComposeConventionPlugin.kt @@ -23,6 +23,7 @@ internal class FeatureComposeConventionPlugin : Plugin { "implementation"(project(":domain")) "implementation"(libs.findLibrary("kotlinx.immutable").get()) + "implementation"(libs.findLibrary("kotlinx.datetime").get()) "implementation"(libs.findLibrary("kotlinx.coroutines.android").get()) "implementation"(libs.findLibrary("kotlinx.coroutines.core").get()) diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/bottomsheet/datepicker/SusuDatePickerBottomSheet.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/bottomsheet/datepicker/SusuDatePickerBottomSheet.kt index b9f0850a..5d9293bd 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/bottomsheet/datepicker/SusuDatePickerBottomSheet.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/bottomsheet/datepicker/SusuDatePickerBottomSheet.kt @@ -221,7 +221,7 @@ fun SusuLimitDatePickerBottomSheet( InfiniteColumn( modifier = Modifier.width(100.dp), items = yearList, - initialItem = stringResource(R.string.word_year_format, criteriaYear), + initialItem = stringResource(R.string.word_year_format, selectedYear), itemHeight = itemHeight, numberOfDisplayedItems = numberOfDisplayedItems, onItemSelected = { _, item -> @@ -233,7 +233,7 @@ fun SusuLimitDatePickerBottomSheet( InfiniteColumn( modifier = Modifier.width(100.dp), items = monthRange.map { stringResource(id = R.string.word_month_format, it) }, - initialItem = stringResource(R.string.word_month_format, criteriaMonth), + initialItem = stringResource(R.string.word_month_format, selectedMonth), itemHeight = itemHeight, numberOfDisplayedItems = numberOfDisplayedItems, onItemSelected = { _, item -> diff --git a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt index a3ba9f6b..6188e66d 100644 --- a/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt +++ b/core/designsystem/src/main/java/com/susu/core/designsystem/component/textfieldbutton/SusuTextFieldButton.kt @@ -28,6 +28,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.painterResource @@ -185,7 +187,8 @@ fun SusuTextFieldWrapContentButton( onClickClearIcon: () -> Unit = {}, onClickCloseIcon: () -> Unit = {}, onClickFilledButton: () -> Unit = {}, - onClickButton: (isFocused: Boolean) -> Unit = {}, + onClickButton: () -> Unit = {}, + focusRequester: FocusRequester = remember { FocusRequester() }, ) { val (backgroundColor, textColor) = with(color) { when { @@ -200,7 +203,7 @@ fun SusuTextFieldWrapContentButton( modifier = modifier .clip(shape) .background(backgroundColor) - .susuClickable { onClickButton(isFocused) } + .susuClickable(onClick = onClickButton) .padding(paddingValues), horizontalArrangement = Arrangement.spacedBy(iconSpacing), verticalAlignment = Alignment.CenterVertically, @@ -217,7 +220,8 @@ fun SusuTextFieldWrapContentButton( * see -> https://stackoverflow.com/questions/67719981/resizeable-basictextfield-in-jetpack-compose */ .width(IntrinsicSize.Min) - .susuClickable(rippleEnabled = false, runIf = isFocused.not(), onClick = { onClickButton(isFocused) }), + .susuClickable(rippleEnabled = false, runIf = isFocused.not(), onClick = onClickButton) + .focusRequester(focusRequester), value = text, enabled = isSaved.not() && isFocused, onValueChange = onTextChange, diff --git a/core/model/build.gradle.kts b/core/model/build.gradle.kts index d9a44165..4d4be39f 100644 --- a/core/model/build.gradle.kts +++ b/core/model/build.gradle.kts @@ -3,8 +3,11 @@ plugins { alias(libs.plugins.susu.java.library) + alias(libs.plugins.kotlin.serialization) } dependencies { compileOnly(libs.compose.stable.marker) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.datetime) } diff --git a/core/model/src/main/java/com/susu/core/model/Category.kt b/core/model/src/main/java/com/susu/core/model/Category.kt index e35d01ed..a10be68f 100644 --- a/core/model/src/main/java/com/susu/core/model/Category.kt +++ b/core/model/src/main/java/com/susu/core/model/Category.kt @@ -1,8 +1,13 @@ package com.susu.core.model +import androidx.compose.runtime.Stable +import kotlinx.serialization.Serializable + +@Stable +@Serializable data class Category( - val id: Int, - val seq: Int, - val category: String, + val id: Int = 0, + val seq: Int = 0, + val name: String = "", val customCategory: String? = null, ) diff --git a/core/model/src/main/java/com/susu/core/model/Ledger.kt b/core/model/src/main/java/com/susu/core/model/Ledger.kt index fced05c1..5bd7b56b 100644 --- a/core/model/src/main/java/com/susu/core/model/Ledger.kt +++ b/core/model/src/main/java/com/susu/core/model/Ledger.kt @@ -1,15 +1,19 @@ package com.susu.core.model import androidx.compose.runtime.Stable -import java.time.LocalDateTime +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toKotlinLocalDateTime +import kotlinx.serialization.Serializable @Stable +@Serializable data class Ledger( - val id: Int, - val title: String, - val description: String, - val startAt: LocalDateTime, - val endAt: LocalDateTime, - val category: Category, - val totalAmounts: Int, + val id: Int = -1, + val title: String = "", + val description: String = "", + val startAt: LocalDateTime = java.time.LocalDateTime.now().toKotlinLocalDateTime(), + val endAt: LocalDateTime = java.time.LocalDateTime.now().toKotlinLocalDateTime(), + val category: Category = Category(), + val totalAmounts: Int = 0, + val totalCounts: Int = 0, ) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 944fbbe2..8b3390ab 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -2,8 +2,13 @@ plugins { alias(libs.plugins.susu.android.library) alias(libs.plugins.susu.android.library.compose) + alias(libs.plugins.kotlin.serialization) } android { namespace = "com.susu.core.ui" } + +dependencies { + implementation(libs.kotlinx.serialization.json) +} diff --git a/core/ui/src/main/java/com/susu/core/ui/extension/Json.kt b/core/ui/src/main/java/com/susu/core/ui/extension/Json.kt new file mode 100644 index 00000000..3c2ac87d --- /dev/null +++ b/core/ui/src/main/java/com/susu/core/ui/extension/Json.kt @@ -0,0 +1,13 @@ +package com.susu.core.ui.extension + +import android.net.Uri +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +inline fun Json.encodeToUri(value: T): String { + return Uri.encode(encodeToString(value)) +} + +inline fun Json.decodeFromUri(value: String): T { + return decodeFromString(Uri.decode(value)) +} diff --git a/core/ui/src/main/java/com/susu/core/ui/extension/LazyGridState.kt b/core/ui/src/main/java/com/susu/core/ui/extension/LazyGridState.kt new file mode 100644 index 00000000..0250605b --- /dev/null +++ b/core/ui/src/main/java/com/susu/core/ui/extension/LazyGridState.kt @@ -0,0 +1,38 @@ +package com.susu.core.ui.extension + +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.collectLatest + +// https://manavtamboli.medium.com/infinite-list-paged-list-in-jetpack-compose-b10fc7e74768 +@Composable +fun LazyGridState.OnBottomReached( + // tells how many items before we reach the bottom of the list + // to call onLoadMore function + buffer: Int = 0, + onLoadMore: () -> Unit, +) { + // Buffer must be positive. + // Or our list will never reach the bottom. + require(buffer >= 0) { "buffer cannot be negative, but was $buffer" } + + val shouldLoadMore = remember { + derivedStateOf { + val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf false + + lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - buffer + } + } + LaunchedEffect(shouldLoadMore) { + snapshotFlow { shouldLoadMore.value } + .collectLatest { + if (it) { + onLoadMore() + } + } + } +} diff --git a/core/ui/src/main/java/com/susu/core/ui/extension/LazyListState.kt b/core/ui/src/main/java/com/susu/core/ui/extension/LazyListState.kt new file mode 100644 index 00000000..9ae32f2f --- /dev/null +++ b/core/ui/src/main/java/com/susu/core/ui/extension/LazyListState.kt @@ -0,0 +1,39 @@ +package com.susu.core.ui.extension + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow + +// https://manavtamboli.medium.com/infinite-list-paged-list-in-jetpack-compose-b10fc7e74768 +@Composable +fun LazyListState.OnBottomReached( + // tells how many items before we reach the bottom of the list + // to call onLoadMore function + buffer: Int = 3, + onLoadMore: () -> Unit, +) { + // Buffer must be positive. + // Or our list will never reach the bottom. + require(buffer >= 0) { "buffer cannot be negative, but was $buffer" } + + val shouldLoadMore = remember { + derivedStateOf { + val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf false + + lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - buffer + } + } + LaunchedEffect(shouldLoadMore) { + snapshotFlow { shouldLoadMore.value } + .collect { + if (it) { + onLoadMore() + } + } + } +} + +fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 2d9826d0..a1bd3ed6 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ 편집 저장 삭제 + 취소 전체 %s원 총 %d개 더보기 아이콘 diff --git a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt index 34902eb2..a9dc5263 100644 --- a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt @@ -1,11 +1,13 @@ package com.susu.data.data.di +import com.susu.data.data.repository.CategoryConfigRepositoryImpl import com.susu.data.data.repository.LedgerRecentSearchRepositoryImpl import com.susu.data.data.repository.LedgerRepositoryImpl import com.susu.data.data.repository.LoginRepositoryImpl import com.susu.data.data.repository.SignUpRepositoryImpl import com.susu.data.data.repository.TermRepositoryImpl import com.susu.data.data.repository.TokenRepositoryImpl +import com.susu.domain.repository.CategoryConfigRepository import com.susu.domain.repository.LedgerRecentSearchRepository import com.susu.domain.repository.LedgerRepository import com.susu.domain.repository.LoginRepository @@ -50,4 +52,9 @@ abstract class RepositoryModule { abstract fun bindLedgerRepository( ledgerRepositoryImpl: LedgerRepositoryImpl, ): LedgerRepository + + @Binds + abstract fun bindCategoryConfigRepository( + categoryConfigRepositoryImpl: CategoryConfigRepositoryImpl, + ): CategoryConfigRepository } diff --git a/data/src/main/java/com/susu/data/data/repository/CategoryConfigRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/CategoryConfigRepositoryImpl.kt new file mode 100644 index 00000000..3fd1af85 --- /dev/null +++ b/data/src/main/java/com/susu/data/data/repository/CategoryConfigRepositoryImpl.kt @@ -0,0 +1,29 @@ +package com.susu.data.data.repository + +import com.susu.core.android.Dispatcher +import com.susu.core.android.SusuDispatchers +import com.susu.core.model.Category +import com.susu.data.local.dao.CategoryConfigDao +import com.susu.data.local.model.toEntity +import com.susu.data.local.model.toModel +import com.susu.data.remote.api.CategoryService +import com.susu.data.remote.model.response.toModel +import com.susu.domain.repository.CategoryConfigRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class CategoryConfigRepositoryImpl @Inject constructor( + private val dao: CategoryConfigDao, + private val api: CategoryService, + @Dispatcher(SusuDispatchers.IO) private val ioDispatcher: CoroutineDispatcher, +) : CategoryConfigRepository { + override suspend fun getCategoryConfig(): List = withContext(ioDispatcher) { + val localCategoryConfig = dao.getCategoryConfig().map { it.toModel() } + if (localCategoryConfig.isNotEmpty()) return@withContext localCategoryConfig + + val remoteCategoryConfig = api.getCategoryConfig().getOrThrow().map { it.toModel() } + dao.insert(remoteCategoryConfig.map { it.toEntity() }) + return@withContext remoteCategoryConfig + } +} diff --git a/data/src/main/java/com/susu/data/data/repository/LedgerRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/LedgerRepositoryImpl.kt index 4f0e75df..92aeae34 100644 --- a/data/src/main/java/com/susu/data/data/repository/LedgerRepositoryImpl.kt +++ b/data/src/main/java/com/susu/data/data/repository/LedgerRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.susu.data.data.repository import com.susu.core.model.Ledger import com.susu.data.remote.api.LedgerService +import com.susu.data.remote.model.request.toData import com.susu.data.remote.model.response.toModel import com.susu.domain.repository.LedgerRepository import kotlinx.datetime.toKotlinLocalDateTime @@ -18,14 +19,21 @@ class LedgerRepositoryImpl @Inject constructor( toEndAt: LocalDateTime, page: Int?, sort: String?, - ): List { - return ledgerService.getLedgerList( - title = title, - categoryId = categoryId, - fromStartAt = fromStartAt.toKotlinLocalDateTime(), - toEndAt = toEndAt.toKotlinLocalDateTime(), - page = page, - sort = sort, - ).getOrThrow().toModel() - } + ): List = ledgerService.getLedgerList( + title = title, + categoryId = categoryId, + fromStartAt = fromStartAt.toKotlinLocalDateTime(), + toEndAt = toEndAt.toKotlinLocalDateTime(), + page = page, + sort = sort, + ).getOrThrow().toModel() + + override suspend fun editLedger(ledger: Ledger): Ledger = ledgerService.editLedger( + id = ledger.id, + ledgerRequest = ledger.toData(), + ).getOrThrow().toModel() + + override suspend fun deleteLedger(id: Int) = ledgerService.deleteLedgerList( + listOf(id), + ).getOrThrow() } diff --git a/data/src/main/java/com/susu/data/local/RoomInMemoryDataBase.kt b/data/src/main/java/com/susu/data/local/RoomInMemoryDataBase.kt new file mode 100644 index 00000000..38259b36 --- /dev/null +++ b/data/src/main/java/com/susu/data/local/RoomInMemoryDataBase.kt @@ -0,0 +1,16 @@ +package com.susu.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.susu.data.local.dao.CategoryConfigDao +import com.susu.data.local.model.CategoryConfigEntity + +@Database( + entities = [ + CategoryConfigEntity::class, + ], + version = 1, +) +abstract class RoomInMemoryDataBase : RoomDatabase() { + abstract fun categoryConfigDao(): CategoryConfigDao +} diff --git a/data/src/main/java/com/susu/data/local/dao/CategoryConfigDao.kt b/data/src/main/java/com/susu/data/local/dao/CategoryConfigDao.kt new file mode 100644 index 00000000..92cfd09b --- /dev/null +++ b/data/src/main/java/com/susu/data/local/dao/CategoryConfigDao.kt @@ -0,0 +1,16 @@ +package com.susu.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import com.susu.data.local.model.CategoryConfigEntity +import com.susu.data.local.model.EntityTable + +@Dao +interface CategoryConfigDao { + @Query("SELECT * FROM ${EntityTable.CATEGORY_CONFIG}") + fun getCategoryConfig(): List + + @Insert + fun insert(categoryConfig: List) +} diff --git a/data/src/main/java/com/susu/data/local/di/DaoModule.kt b/data/src/main/java/com/susu/data/local/di/DaoModule.kt index c879b13c..e674d48d 100644 --- a/data/src/main/java/com/susu/data/local/di/DaoModule.kt +++ b/data/src/main/java/com/susu/data/local/di/DaoModule.kt @@ -1,6 +1,7 @@ package com.susu.data.local.di import com.susu.data.local.RoomDataBase +import com.susu.data.local.RoomInMemoryDataBase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,4 +15,8 @@ object DaoModule { @Singleton @Provides fun provideRecentSearchDao(db: RoomDataBase) = db.ledgerRecentSearchDao() + + @Singleton + @Provides + fun provideCategoryConfigDao(db: RoomInMemoryDataBase) = db.categoryConfigDao() } diff --git a/data/src/main/java/com/susu/data/local/di/RoomModule.kt b/data/src/main/java/com/susu/data/local/di/RoomModule.kt index 86e5f45c..cc278d53 100644 --- a/data/src/main/java/com/susu/data/local/di/RoomModule.kt +++ b/data/src/main/java/com/susu/data/local/di/RoomModule.kt @@ -3,6 +3,7 @@ package com.susu.data.local.di import android.content.Context import androidx.room.Room import com.susu.data.local.RoomDataBase +import com.susu.data.local.RoomInMemoryDataBase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -25,4 +26,13 @@ object RoomModule { RoomDataBase::class.java, DB_NAME, ).build() + + @Provides + @Singleton + fun provideRoomInMemoryDataBase( + @ApplicationContext context: Context, + ) = Room.inMemoryDatabaseBuilder( + context, + RoomInMemoryDataBase::class.java, + ).build() } diff --git a/data/src/main/java/com/susu/data/local/model/CategoryConfigEntity.kt b/data/src/main/java/com/susu/data/local/model/CategoryConfigEntity.kt new file mode 100644 index 00000000..0c84f2dd --- /dev/null +++ b/data/src/main/java/com/susu/data/local/model/CategoryConfigEntity.kt @@ -0,0 +1,25 @@ +package com.susu.data.local.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.susu.core.model.Category + +@Entity(tableName = EntityTable.CATEGORY_CONFIG) +data class CategoryConfigEntity( + @PrimaryKey + val id: Int, + val seq: Int, + val name: String, +) + +internal fun CategoryConfigEntity.toModel() = Category( + id = id, + seq = seq, + name = name, +) + +internal fun Category.toEntity() = CategoryConfigEntity( + id = id, + seq = seq, + name = name, +) diff --git a/data/src/main/java/com/susu/data/local/model/EntityTable.kt b/data/src/main/java/com/susu/data/local/model/EntityTable.kt index 9ffb15c3..5949b06e 100644 --- a/data/src/main/java/com/susu/data/local/model/EntityTable.kt +++ b/data/src/main/java/com/susu/data/local/model/EntityTable.kt @@ -3,4 +3,5 @@ package com.susu.data.local.model object EntityTable { const val LEDGER_RECENT_SEARCH = "LedgerRecentSearch" const val ENVELOPE_RECENT_SEARCH = "EnvelopeRecentSearch" + const val CATEGORY_CONFIG = "CategoryConfig" } diff --git a/data/src/main/java/com/susu/data/remote/api/CategoryService.kt b/data/src/main/java/com/susu/data/remote/api/CategoryService.kt new file mode 100644 index 00000000..88a8a938 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/api/CategoryService.kt @@ -0,0 +1,10 @@ +package com.susu.data.remote.api + +import com.susu.data.remote.model.response.CategoryConfigResponse +import com.susu.data.remote.retrofit.ApiResult +import retrofit2.http.GET + +interface CategoryService { + @GET("categories") + suspend fun getCategoryConfig(): ApiResult> +} diff --git a/data/src/main/java/com/susu/data/remote/api/LedgerService.kt b/data/src/main/java/com/susu/data/remote/api/LedgerService.kt index e24872b9..ae30275d 100644 --- a/data/src/main/java/com/susu/data/remote/api/LedgerService.kt +++ b/data/src/main/java/com/susu/data/remote/api/LedgerService.kt @@ -1,9 +1,15 @@ package com.susu.data.remote.api +import com.susu.data.remote.model.request.LedgerRequest import com.susu.data.remote.model.response.LedgerListResponse +import com.susu.data.remote.model.response.LedgerResponse import com.susu.data.remote.retrofit.ApiResult import kotlinx.datetime.LocalDateTime +import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.Path import retrofit2.http.Query interface LedgerService { @@ -16,4 +22,13 @@ interface LedgerService { @Query("page") page: Int?, @Query("sort") sort: String?, ): ApiResult + + @PATCH("ledgers/{id}") + suspend fun editLedger( + @Path("id") id: Int, + @Body ledgerRequest: LedgerRequest, + ): ApiResult + + @DELETE("ledgers") + suspend fun deleteLedgerList(@Query("ids") idList: List): ApiResult } diff --git a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt index aa41241f..945668fc 100644 --- a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt +++ b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt @@ -1,5 +1,6 @@ package com.susu.data.remote.di +import com.susu.data.remote.api.CategoryService import com.susu.data.remote.api.LedgerService import com.susu.data.remote.api.SignUpService import com.susu.data.remote.api.TermService @@ -46,4 +47,10 @@ object ApiServiceModule { fun providesLedgerService(retrofit: Retrofit): LedgerService { return retrofit.create(LedgerService::class.java) } + + @Singleton + @Provides + fun providesCategoryService(retrofit: Retrofit): CategoryService { + return retrofit.create(CategoryService::class.java) + } } diff --git a/data/src/main/java/com/susu/data/remote/model/request/LedgerRequest.kt b/data/src/main/java/com/susu/data/remote/model/request/LedgerRequest.kt new file mode 100644 index 00000000..6229e4bc --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/request/LedgerRequest.kt @@ -0,0 +1,23 @@ +package com.susu.data.remote.model.request + +import com.susu.core.model.Ledger +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.Serializable + +@Serializable +data class LedgerRequest( + val title: String, + val description: String = "", + val categoryId: Int, + val customCategory: String?, + val startAt: LocalDateTime, + val endAt: LocalDateTime, +) + +internal fun Ledger.toData() = LedgerRequest( + title = this.title, + categoryId = category.id, + customCategory = category.customCategory, + startAt = startAt, + endAt = endAt, +) diff --git a/data/src/main/java/com/susu/data/remote/model/response/CategoryConfigResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/CategoryConfigResponse.kt new file mode 100644 index 00000000..cc701b65 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/CategoryConfigResponse.kt @@ -0,0 +1,17 @@ +package com.susu.data.remote.model.response + +import com.susu.core.model.Category +import kotlinx.serialization.Serializable + +@Serializable +data class CategoryConfigResponse( + val id: Int, + val seq: Int, + val name: String, +) + +internal fun CategoryConfigResponse.toModel() = Category( + id = id, + seq = seq, + name = name, +) diff --git a/data/src/main/java/com/susu/data/remote/model/response/LedgerListResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/LedgerListResponse.kt index 71eeb782..8f5729f2 100644 --- a/data/src/main/java/com/susu/data/remote/model/response/LedgerListResponse.kt +++ b/data/src/main/java/com/susu/data/remote/model/response/LedgerListResponse.kt @@ -1,15 +1,12 @@ package com.susu.data.remote.model.response -import com.susu.core.model.Category -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.toJavaLocalDateTime import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class LedgerListResponse( @SerialName("data") - val ledgerList: List, + val ledgerList: List, val page: Int, val size: Int, val totalPage: Int, @@ -17,32 +14,6 @@ data class LedgerListResponse( val sort: SortInfo, ) -@Serializable -data class Ledger( - @SerialName("ledger") - val info: LedgerInfo, - val category: CategoryInfo, - val totalAmounts: Int, - val totalCounts: Int, -) - -@Serializable -data class LedgerInfo( - val id: Int, - val title: String, - val description: String, - val startAt: LocalDateTime, - val endAt: LocalDateTime, -) - -@Serializable -data class CategoryInfo( - val id: Int, - val seq: Int, - val category: String, - val customCategory: String? = null, -) - @Serializable data class SortInfo( val empty: Boolean, @@ -51,22 +22,5 @@ data class SortInfo( ) internal fun LedgerListResponse.toModel() = this.ledgerList.map { ledger -> - with(ledger) { - com.susu.core.model.Ledger( - id = info.id, - title = info.title, - description = info.description, - startAt = info.startAt.toJavaLocalDateTime(), - endAt = info.endAt.toJavaLocalDateTime(), - category = category.toModel(), - totalAmounts = totalAmounts, - ) - } + ledger.toModel() } - -internal fun CategoryInfo.toModel() = Category( - id = id, - seq = seq, - category = category, - customCategory = customCategory, -) diff --git a/data/src/main/java/com/susu/data/remote/model/response/LedgerResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/LedgerResponse.kt new file mode 100644 index 00000000..cf7d0450 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/LedgerResponse.kt @@ -0,0 +1,51 @@ +package com.susu.data.remote.model.response + +import com.susu.core.model.Category +import com.susu.core.model.Ledger +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LedgerResponse( + @SerialName("ledger") + val info: LedgerInfo, + val category: CategoryInfo, + val totalAmounts: Int = 0, + val totalCounts: Int = 0, +) + +@Serializable +data class LedgerInfo( + val id: Int, + val title: String, + val description: String = "", + val startAt: LocalDateTime, + val endAt: LocalDateTime, +) + +@Serializable +data class CategoryInfo( + val id: Int, + val seq: Int, + val category: String, + val customCategory: String? = null, +) + +internal fun LedgerResponse.toModel() = Ledger( + id = info.id, + title = info.title, + description = info.description, + startAt = info.startAt, + endAt = info.endAt, + category = category.toModel(), + totalAmounts = totalAmounts, + totalCounts = totalCounts, +) + +internal fun CategoryInfo.toModel() = Category( + id = id, + seq = seq, + name = category, + customCategory = customCategory, +) diff --git a/data/src/main/java/com/susu/data/remote/retrofit/ApiResult.kt b/data/src/main/java/com/susu/data/remote/retrofit/ApiResult.kt index d8e77de0..5dfb7419 100644 --- a/data/src/main/java/com/susu/data/remote/retrofit/ApiResult.kt +++ b/data/src/main/java/com/susu/data/remote/retrofit/ApiResult.kt @@ -30,7 +30,7 @@ sealed interface ApiResult { val isFailure: Boolean get() = this is Failure - fun getOrThrow(customHttpErrorHandler: (Failure.HttpError.() -> Exception)? = null): T { + fun getOrThrow(): T { throwFailure() return (this as Success).data } diff --git a/domain/src/main/java/com/susu/domain/repository/CategoryConfigRepository.kt b/domain/src/main/java/com/susu/domain/repository/CategoryConfigRepository.kt new file mode 100644 index 00000000..c606ee9a --- /dev/null +++ b/domain/src/main/java/com/susu/domain/repository/CategoryConfigRepository.kt @@ -0,0 +1,7 @@ +package com.susu.domain.repository + +import com.susu.core.model.Category + +interface CategoryConfigRepository { + suspend fun getCategoryConfig(): List +} diff --git a/domain/src/main/java/com/susu/domain/repository/LedgerRepository.kt b/domain/src/main/java/com/susu/domain/repository/LedgerRepository.kt index 97602be5..d98b0d7c 100644 --- a/domain/src/main/java/com/susu/domain/repository/LedgerRepository.kt +++ b/domain/src/main/java/com/susu/domain/repository/LedgerRepository.kt @@ -12,4 +12,12 @@ interface LedgerRepository { page: Int?, sort: String?, ): List + + suspend fun editLedger( + ledger: Ledger, + ): Ledger + + suspend fun deleteLedger( + id: Int, + ) } diff --git a/domain/src/main/java/com/susu/domain/usecase/categoryconfig/GetCategoryConfigUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/categoryconfig/GetCategoryConfigUseCase.kt new file mode 100644 index 00000000..f28af560 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/categoryconfig/GetCategoryConfigUseCase.kt @@ -0,0 +1,13 @@ +package com.susu.domain.usecase.categoryconfig + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.CategoryConfigRepository +import javax.inject.Inject + +class GetCategoryConfigUseCase @Inject constructor( + private val categoryConfigRepository: CategoryConfigRepository, +) { + suspend operator fun invoke() = runCatchingIgnoreCancelled { + categoryConfigRepository.getCategoryConfig() + } +} diff --git a/domain/src/main/java/com/susu/domain/usecase/ledger/DeleteLedgerUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/ledger/DeleteLedgerUseCase.kt new file mode 100644 index 00000000..43d143ef --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/ledger/DeleteLedgerUseCase.kt @@ -0,0 +1,13 @@ +package com.susu.domain.usecase.ledger + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.LedgerRepository +import javax.inject.Inject + +class DeleteLedgerUseCase @Inject constructor( + private val ledgerRepository: LedgerRepository, +) { + suspend operator fun invoke(id: Int) = runCatchingIgnoreCancelled { + ledgerRepository.deleteLedger(id) + } +} diff --git a/domain/src/main/java/com/susu/domain/usecase/ledger/EditLedgerUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/ledger/EditLedgerUseCase.kt new file mode 100644 index 00000000..550c38d1 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/ledger/EditLedgerUseCase.kt @@ -0,0 +1,14 @@ +package com.susu.domain.usecase.ledger + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.core.model.Ledger +import com.susu.domain.repository.LedgerRepository +import javax.inject.Inject + +class EditLedgerUseCase @Inject constructor( + private val ledgerRepository: LedgerRepository, +) { + suspend operator fun invoke(ledger: Ledger) = runCatchingIgnoreCancelled { + ledgerRepository.editLedger(ledger) + } +} diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainActivity.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainActivity.kt index adc141dc..8eb4e5ee 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainActivity.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainActivity.kt @@ -21,16 +21,16 @@ import dagger.hilt.android.AndroidEntryPoint class MainActivity : ComponentActivity() { private val viewModel: MainViewModel by viewModels() - private val isNavigating - get() = viewModel.uiState.value.isNavigating + private val uiState + get() = viewModel.uiState.value override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) - splashScreen.setKeepOnScreenCondition { isNavigating } + splashScreen.setKeepOnScreenCondition { uiState.showSplashScreen } - if (isNavigating) { + if (uiState.isNavigating) { KakaoLoginHelper.getAccessToken { accessToken -> viewModel.navigate( hasKakaoLoginHistory = KakaoLoginHelper.hasKakaoLoginHistory, @@ -39,6 +39,10 @@ class MainActivity : ComponentActivity() { } } + if (uiState.isInitializing) { + viewModel.initCategoryConfig() + } + WindowCompat.setDecorFitsSystemWindows(window, false) setContent { diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainContract.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainContract.kt index 8b68382d..9686cecc 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainContract.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainContract.kt @@ -6,15 +6,19 @@ import com.susu.core.ui.base.SideEffect import com.susu.core.ui.base.UiState data class MainState( + val isInitializing: Boolean = true, val isNavigating: Boolean = true, val snackbarToken: SnackbarToken = SnackbarToken(), val snackbarVisible: Boolean = false, val dialogToken: DialogToken = DialogToken(), val dialogVisible: Boolean = false, -) : UiState +) : UiState { + val showSplashScreen = isInitializing || isNavigating +} sealed interface MainSideEffect : SideEffect { data object NavigateLogin : MainSideEffect data object NavigateSignup : MainSideEffect data object NavigateSent : MainSideEffect + data class ShowNetworkErrorSnackbar(val retry: () -> Unit) : MainSideEffect } diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt index 1ca4d6ff..396ad16c 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainNavigator.kt @@ -4,12 +4,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.navigation.NavDestination -import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.Ledger import com.susu.feature.community.navigation.navigateCommunity import com.susu.feature.loginsignup.navigation.LoginSignupRoute import com.susu.feature.mypage.navigation.navigateMyPage @@ -57,7 +57,7 @@ internal class MainNavigator( fun navigate(tab: MainNavigationTab) { val navOptions = navOptions { - popUpTo(navController.graph.findStartDestination().id) { + popUpTo(SentRoute.route) { saveState = true } launchSingleTop = true @@ -109,16 +109,16 @@ internal class MainNavigator( } } - fun navigateLedgerDetail(id: Int) { - navController.navigateLedgerDetail(id) + fun navigateLedgerDetail(ledger: Ledger) { + navController.navigateLedgerDetail(ledger) } fun navigateLedgerSearch() { navController.navigateLedgerSearch() } - fun navigateLedgerEdit() { - navController.navigateLedgerEdit() + fun navigateLedgerEdit(ledger: Ledger) { + navController.navigateLedgerEdit(ledger) } fun navigateLedgerFilter() { diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt index efadbaf9..3b66e1fc 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt @@ -20,10 +20,12 @@ import com.susu.core.designsystem.component.dialog.SusuDialog import com.susu.core.designsystem.component.navigation.SusuNavigationBar import com.susu.core.designsystem.component.navigation.SusuNavigationItem import com.susu.core.designsystem.component.snackbar.SusuSnackbar +import com.susu.core.ui.SnackbarToken import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.community.navigation.communityNavGraph import com.susu.feature.loginsignup.navigation.loginSignupNavGraph import com.susu.feature.mypage.navigation.myPageNavGraph +import com.susu.feature.received.navigation.ReceivedRoute import com.susu.feature.received.navigation.receivedNavGraph import com.susu.feature.sent.navigation.sentNavGraph import com.susu.feature.statistics.navigation.statisticsNavGraph @@ -42,6 +44,16 @@ internal fun MainScreen( MainSideEffect.NavigateLogin -> navigator.navigateLogin() MainSideEffect.NavigateSent -> navigator.navigateSent() MainSideEffect.NavigateSignup -> navigator.navigateSignup() + is MainSideEffect.ShowNetworkErrorSnackbar -> { + viewModel.onShowSnackbar( + SnackbarToken( + message = "네트워크 오류가 발생했어요", + onClickActionButton = sideEffect.retry, + actionIcon = R.drawable.ic_refresh, + actionIconContentDescription = "새로고침 아이콘", + ), + ) + } } } @@ -60,7 +72,7 @@ internal fun MainScreen( navigateToReceived = navigator::navigateSent, navigateToLogin = navigator::navigateLogin, navigateToSignUp = navigator::navigateSignup, - onShowToast = viewModel::onShowToast, + onShowToast = viewModel::onShowSnackbar, padding = innerPadding, ) @@ -75,11 +87,28 @@ internal fun MainScreen( receivedNavGraph( padding = innerPadding, popBackStack = navigator::popBackStackIfNotHome, + popBackStackWithLedger = { ledger -> + navigator.navController.previousBackStackEntry?.savedStateHandle?.set( + ReceivedRoute.LEDGER_ARGUMENT_NAME, + ledger, + ) + navigator.popBackStackIfNotHome() + }, + popBackStackWithDeleteLedgerId = { ledgerId -> + navigator.navController.previousBackStackEntry?.savedStateHandle?.set( + ReceivedRoute.LEDGER_ID_ARGUMENT_NAME, + ledgerId, + ) + navigator.popBackStackIfNotHome() + }, navigateLedgerSearch = navigator::navigateLedgerSearch, navigateLedgerDetail = navigator::navigateLedgerDetail, navigateLedgerEdit = navigator::navigateLedgerEdit, navigateLedgerFilter = navigator::navigateLedgerFilter, navigateLedgerAdd = navigator::navigateLedgerAdd, + onShowSnackbar = viewModel::onShowSnackbar, + onShowDialog = viewModel::onShowDialog, + handleException = viewModel::handleException, ) statisticsNavGraph( diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainViewModel.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainViewModel.kt index e251a203..3df958b8 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainViewModel.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainViewModel.kt @@ -1,9 +1,12 @@ package com.susu.feature.navigator import androidx.lifecycle.viewModelScope +import com.susu.core.android.throwUnknownException +import com.susu.core.model.exception.NetworkException import com.susu.core.ui.DialogToken import com.susu.core.ui.SnackbarToken import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.categoryconfig.GetCategoryConfigUseCase import com.susu.domain.usecase.loginsignup.CheckCanRegisterUseCase import com.susu.domain.usecase.loginsignup.LoginUseCase import dagger.hilt.android.lifecycle.HiltViewModel @@ -11,12 +14,14 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import java.net.UnknownHostException import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( private val loginUseCase: LoginUseCase, private val checkCanRegisterUseCase: CheckCanRegisterUseCase, + private val getCategoryConfigUseCase: GetCategoryConfigUseCase, ) : BaseViewModel(MainState()) { companion object { private const val NAVIGATE_DELAY = 500L @@ -25,7 +30,7 @@ class MainViewModel @Inject constructor( private val mutex = Mutex() - fun onShowToast(snackbarToken: SnackbarToken) = viewModelScope.launch { + fun onShowSnackbar(snackbarToken: SnackbarToken) = viewModelScope.launch { mutex.withLock { intent { copy(snackbarToken = snackbarToken, snackbarVisible = true) } delay(SHOW_TOAST_LENGTH) @@ -41,6 +46,18 @@ class MainViewModel @Inject constructor( intent { copy(dialogVisible = false) } } + fun handleException(throwable: Throwable, retry: () -> Unit) = when (throwable) { + is NetworkException -> postSideEffect(MainSideEffect.ShowNetworkErrorSnackbar(retry)) + is UnknownHostException -> postSideEffect(MainSideEffect.ShowNetworkErrorSnackbar(retry)) + else -> throwUnknownException(throwable) + } + + fun initCategoryConfig() = viewModelScope.launch { + getCategoryConfigUseCase() + .onFailure { } + intent { copy(isInitializing = false) } + } + fun navigate(hasKakaoLoginHistory: Boolean, kakaoAccessToken: String?) = viewModelScope.launch { if (hasKakaoLoginHistory.not()) { intent { copy(isNavigating = false) } diff --git a/feature/navigator/src/main/res/drawable/ic_refresh.xml b/feature/navigator/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..e423446d --- /dev/null +++ b/feature/navigator/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/received/build.gradle.kts b/feature/received/build.gradle.kts index d1a06190..d93c8051 100644 --- a/feature/received/build.gradle.kts +++ b/feature/received/build.gradle.kts @@ -1,8 +1,13 @@ @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed plugins { alias(libs.plugins.susu.android.feature.compose) + alias(libs.plugins.kotlin.serialization) } android { namespace = "com.susu.feature.received" } + +dependencies { + implementation(libs.kotlinx.serialization.json) +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailContract.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailContract.kt index 521fc7be..744f7af1 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailContract.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailContract.kt @@ -1,10 +1,24 @@ package com.susu.feature.received.ledgerdetail +import com.susu.core.model.Ledger import com.susu.core.ui.base.SideEffect import com.susu.core.ui.base.UiState data class LedgerDetailState( - val id: Int = 0, + val name: String = "", + val money: Int = 0, + val count: Int = 0, + val category: String = "", + val startDate: String = "", + val endDate: String = "", ) : UiState -sealed interface LedgerDetailSideEffect : SideEffect +sealed interface LedgerDetailSideEffect : SideEffect { + data class NavigateLedgerEdit(val ledger: Ledger) : LedgerDetailSideEffect + data class PopBackStackWithLedger(val ledger: String) : LedgerDetailSideEffect + data class PopBackStackWithDeleteLedgerId(val ledgerId: Int) : LedgerDetailSideEffect + data class ShowDeleteDialog(val onConfirmRequest: () -> Unit) : LedgerDetailSideEffect + data object ShowDeleteSuccessSnackbar : LedgerDetailSideEffect + data class ShowSnackbar(val msg: String) : LedgerDetailSideEffect + data class HandleException(val throwable: Throwable, val retry: () -> Unit) : LedgerDetailSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt index 75472dbe..87010765 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailScreen.kt @@ -1,5 +1,6 @@ package com.susu.feature.received.ledgerdetail +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,13 +17,16 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.appbar.SusuDefaultAppBar import com.susu.core.designsystem.component.appbar.icon.BackIcon import com.susu.core.designsystem.component.appbar.icon.DeleteText @@ -34,24 +38,75 @@ import com.susu.core.designsystem.component.button.SusuGhostButton import com.susu.core.designsystem.theme.Gray25 import com.susu.core.designsystem.theme.Gray50 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.Ledger +import com.susu.core.ui.DialogToken import com.susu.core.ui.R +import com.susu.core.ui.SnackbarToken import com.susu.core.ui.alignList +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.received.ledgerdetail.component.LedgerDetailEnvelopeContainer import com.susu.feature.received.ledgerdetail.component.LedgerDetailOverviewColumn @Composable fun LedgerDetailRoute( - @Suppress("deteKt:UnusedParameter") viewModel: LedgerDetailViewModel = hiltViewModel(), - navigateLedgerEdit: () -> Unit, + ledger: String?, + navigateLedgerEdit: (Ledger) -> Unit, + popBackStackWithLedger: (String) -> Unit, + popBackStackWithDeleteLedgerId: (Int) -> Unit, + onShowSnackbar: (SnackbarToken) -> Unit, + onShowDialog: (DialogToken) -> Unit, + handleException: (Throwable, () -> Unit) -> Unit, ) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + val context = LocalContext.current + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is LedgerDetailSideEffect.NavigateLedgerEdit -> navigateLedgerEdit(sideEffect.ledger) + is LedgerDetailSideEffect.PopBackStackWithLedger -> popBackStackWithLedger(sideEffect.ledger) + is LedgerDetailSideEffect.ShowDeleteDialog -> { + onShowDialog( + DialogToken( + title = context.getString(com.susu.feature.received.R.string.ledger_detail_screen_dialog_title), + text = context.getString(com.susu.feature.received.R.string.ledger_detail_screen_dialog_description), + confirmText = context.getString(R.string.word_delete), + dismissText = context.getString(R.string.word_cancel), + onConfirmRequest = sideEffect.onConfirmRequest, + ), + ) + } + + LedgerDetailSideEffect.ShowDeleteSuccessSnackbar -> { + onShowSnackbar( + SnackbarToken( + message = context.getString(com.susu.feature.received.R.string.ledger_detail_screen_snackbar_message), + ), + ) + } + + is LedgerDetailSideEffect.PopBackStackWithDeleteLedgerId -> popBackStackWithDeleteLedgerId(sideEffect.ledgerId) + is LedgerDetailSideEffect.HandleException -> handleException(sideEffect.throwable, sideEffect.retry) + is LedgerDetailSideEffect.ShowSnackbar -> onShowSnackbar(SnackbarToken(message = sideEffect.msg)) + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.initData(ledger) + } + + BackHandler(onBack = viewModel::popBackStackWithLedger) + LedgerDetailScreen( - onClickEdit = navigateLedgerEdit, + uiState = uiState, + onClickEdit = viewModel::navigateLedgerEdit, + onClickDelete = viewModel::showDeleteDialog, + onClickBack = viewModel::popBackStackWithLedger, ) } @Composable fun LedgerDetailScreen( + uiState: LedgerDetailState = LedgerDetailState(), onClickBack: () -> Unit = {}, onClickEdit: () -> Unit = {}, onClickDelete: () -> Unit = {}, @@ -89,13 +144,15 @@ fun LedgerDetailScreen( ), ) { item { - LedgerDetailOverviewColumn( - money = 0, - count = 0, - eventCategory = "장례식", - eventName = "고모부 장례", - eventRange = "2023.05.12 - 2023.05.15", - ) + with(uiState) { + LedgerDetailOverviewColumn( + money = money, + count = count, + eventName = name, + eventCategory = category, + eventRange = "$startDate - $endDate", + ) + } } item { @@ -148,7 +205,9 @@ fun LedgerDetailScreen( if (showEmptyScreen) { item { Column( - modifier = Modifier.fillMaxWidth().padding(top = 104.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 104.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_m), ) { diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailViewModel.kt index 35aed4ee..e6f2ab24 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailViewModel.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgerdetail/LedgerDetailViewModel.kt @@ -1,17 +1,83 @@ package com.susu.feature.received.ledgerdetail import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.susu.core.model.Ledger +import com.susu.core.model.exception.NotFoundLedgerException import com.susu.core.ui.base.BaseViewModel +import com.susu.core.ui.extension.decodeFromUri +import com.susu.core.ui.extension.encodeToUri +import com.susu.core.ui.util.to_yyyy_dot_MM_dot_dd +import com.susu.domain.usecase.ledger.DeleteLedgerUseCase import com.susu.feature.received.navigation.ReceivedRoute import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.serialization.json.Json import javax.inject.Inject @HiltViewModel class LedgerDetailViewModel @Inject constructor( + private val deleteLedgerUseCase: DeleteLedgerUseCase, savedStateHandle: SavedStateHandle, ) : BaseViewModel( LedgerDetailState(), ) { - @Suppress("deteKt:UnusedPrivateProperty") - private val id = savedStateHandle.get(ReceivedRoute.LEDGER_DETAIL_ARGUMENT_NAME)!!.toInt() + private val argument = savedStateHandle.get(ReceivedRoute.LEDGER_ARGUMENT_NAME)!! + private var ledger = Ledger() + + fun initData(backStackEntryLedgerUri: String?) { + if (backStackEntryLedgerUri == null) { + updateLedgerInfo(Json.decodeFromUri(argument)) + return + } + + val backStackLedger = Json.decodeFromUri(backStackEntryLedgerUri) + if (backStackLedger == Ledger()) { + updateLedgerInfo(Json.decodeFromUri(argument)) + return + } + + updateLedgerInfo(backStackLedger) + } + + private fun updateLedgerInfo(ledger: Ledger) = intent { + this@LedgerDetailViewModel.ledger = ledger + ledger.let { ledger -> + val category = ledger.category + copy( + name = ledger.title, + money = ledger.totalAmounts, + count = ledger.totalCounts, + category = if (category.customCategory.isNullOrEmpty()) category.name else category.customCategory!!, + startDate = ledger.startAt.toJavaLocalDateTime().to_yyyy_dot_MM_dot_dd(), + endDate = ledger.endAt.toJavaLocalDateTime().to_yyyy_dot_MM_dot_dd(), + ) + } + } + + fun navigateLedgerEdit() = postSideEffect(LedgerDetailSideEffect.NavigateLedgerEdit(ledger)) + + fun popBackStackWithLedger() = postSideEffect(LedgerDetailSideEffect.PopBackStackWithLedger(Json.encodeToUri(ledger))) + fun showDeleteDialog() = postSideEffect( + LedgerDetailSideEffect.ShowDeleteDialog( + onConfirmRequest = ::deleteLedger, + ), + ) + + private fun deleteLedger() = viewModelScope.launch { + deleteLedgerUseCase(ledger.id) + .onSuccess { + postSideEffect( + LedgerDetailSideEffect.ShowDeleteSuccessSnackbar, + LedgerDetailSideEffect.PopBackStackWithDeleteLedgerId(ledger.id), + ) + } + .onFailure { throwable -> + when (throwable) { + is NotFoundLedgerException -> postSideEffect(LedgerDetailSideEffect.ShowSnackbar(throwable.message)) + else -> postSideEffect(LedgerDetailSideEffect.HandleException(throwable, ::deleteLedger)) + } + } + } } diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditContract.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditContract.kt new file mode 100644 index 00000000..5e5cf597 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditContract.kt @@ -0,0 +1,37 @@ +package com.susu.feature.received.ledgeredit + +import com.susu.core.model.Category +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class LedgerEditState( + val name: String = "", + val selectedCategoryId: Int = 0, + val customCategory: String = "", + val startYear: Int = 0, + val startMonth: Int = 0, + val startDay: Int = 0, + val endYear: Int = 0, + val endMonth: Int = 0, + val endDay: Int = 0, + val categoryConfigList: PersistentList = persistentListOf(Category()), + val showCustomCategoryButton: Boolean = false, + val isCustomCategoryChipSaved: Boolean = false, + val showStartDateBottomSheet: Boolean = false, + val showEndDateBottomSheet: Boolean = false, +) : UiState { + val isSelectedCustomCategory = selectedCategoryId == categoryConfigList.last().id + val saveButtonEnabled = when { + name.isEmpty() -> false + isSelectedCustomCategory && (customCategory.isEmpty() || isCustomCategoryChipSaved.not()) -> false + else -> true + } +} + +sealed interface LedgerEditSideEffect : SideEffect { + data object PopBackStack : LedgerEditSideEffect + data class PopBackStackWithLedger(val ledger: String) : LedgerEditSideEffect + data object FocusCustomCategory : LedgerEditSideEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditScreen.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditScreen.kt index f8c9f64d..fca24d9a 100644 --- a/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditScreen.kt @@ -12,41 +12,110 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Text +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.appbar.SusuDefaultAppBar import com.susu.core.designsystem.component.appbar.icon.BackIcon +import com.susu.core.designsystem.component.bottomsheet.datepicker.SusuLimitDatePickerBottomSheet import com.susu.core.designsystem.component.button.AddConditionButton import com.susu.core.designsystem.component.button.FilledButtonColor import com.susu.core.designsystem.component.button.MediumButtonStyle import com.susu.core.designsystem.component.button.SmallButtonStyle import com.susu.core.designsystem.component.button.SusuFilledButton +import com.susu.core.designsystem.component.textfield.SusuBasicTextField import com.susu.core.designsystem.component.textfieldbutton.SusuTextFieldWrapContentButton import com.susu.core.designsystem.component.textfieldbutton.TextFieldButtonColor import com.susu.core.designsystem.component.textfieldbutton.style.SmallTextFieldButtonStyle import com.susu.core.designsystem.theme.Gray80 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle +import com.susu.core.ui.extension.susuClickable import com.susu.core.ui.util.AnnotatedText import com.susu.feature.received.R import com.susu.feature.received.ledgeredit.component.LedgerEditContainer +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.launch @Composable fun LedgerEditRoute( - @Suppress("unused") + viewModel: LedgerEditViewModel = hiltViewModel(), popBackStack: () -> Unit, + popBackStackWithLedger: (String) -> Unit, ) { - LedgerEditScreen() + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + + val focusRequester = remember { FocusRequester() } + val scope = rememberCoroutineScope() + + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + LedgerEditSideEffect.PopBackStack -> popBackStack() + is LedgerEditSideEffect.PopBackStackWithLedger -> popBackStackWithLedger(sideEffect.ledger) + LedgerEditSideEffect.FocusCustomCategory -> scope.launch { + awaitFrame() + focusRequester.requestFocus() + } + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.initData() + } + + LedgerEditScreen( + uiState = uiState, + onClickBack = viewModel::popBackStack, + onClickCustomCategoryClearIcon = { viewModel.updateCustomCategory("") }, + onClickCustomCategoryCloseIcon = viewModel::hideCustomCategoryButton, + onClickCustomCategoryInnerButton = viewModel::toggleCustomCategorySaved, + onTextChangeCustomCategory = viewModel::updateCustomCategory, + onClickAddConditionButton = viewModel::showCustomCategoryButton, + onClickCategoryButton = viewModel::updateCategory, + onTextChangeName = viewModel::updateName, + focusRequester = focusRequester, + onStartDateItemSelected = viewModel::updateStartYear, + onClickStartDateText = viewModel::showStartDateBottomSheet, + onDismissStartDateBottomSheet = viewModel::hideStartDateBottomSheet, + onEndDateItemSelected = viewModel::updateEndYear, + onClickEndDateText = viewModel::showEndDateBottomSheet, + onDismissEndDateBottomSheet = viewModel::hideEndDateBottomSheet, + onClickSaveButton = viewModel::editLedger, + ) } -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable -fun LedgerEditScreen() { +fun LedgerEditScreen( + uiState: LedgerEditState = LedgerEditState(), + focusRequester: FocusRequester = remember { FocusRequester() }, + onClickBack: () -> Unit = {}, + onClickCustomCategoryClearIcon: () -> Unit = {}, + onClickCustomCategoryCloseIcon: () -> Unit = {}, + onClickCustomCategoryInnerButton: () -> Unit = {}, + onClickAddConditionButton: () -> Unit = {}, + onClickCategoryButton: (Int) -> Unit = {}, + onTextChangeName: (String) -> Unit = {}, + onTextChangeCustomCategory: (String) -> Unit = {}, + onStartDateItemSelected: (Int, Int, Int) -> Unit = { _, _, _ -> }, + onClickStartDateText: () -> Unit = {}, + onDismissStartDateBottomSheet: () -> Unit = {}, + onEndDateItemSelected: (Int, Int, Int) -> Unit = { _, _, _ -> }, + onClickEndDateText: () -> Unit = {}, + onDismissEndDateBottomSheet: () -> Unit = {}, + onClickSaveButton: () -> Unit = {}, +) { Box( modifier = Modifier .background(SusuTheme.colorScheme.background15) @@ -59,7 +128,7 @@ fun LedgerEditScreen() { ) { SusuDefaultAppBar( leftIcon = { - BackIcon() + BackIcon(onClickBack) }, ) @@ -73,9 +142,10 @@ fun LedgerEditScreen() { name = stringResource(id = com.susu.core.ui.R.string.word_event_name), verticalAlignment = Alignment.CenterVertically, content = { - Text( - text = "고모부 장례식", - style = SusuTheme.typography.title_m, + SusuBasicTextField( + text = uiState.name, + textStyle = SusuTheme.typography.title_m, + onTextChange = onTextChangeName, ) }, ) @@ -88,38 +158,33 @@ fun LedgerEditScreen() { verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), ) { - SusuFilledButton( - color = FilledButtonColor.Orange, - style = SmallButtonStyle.height32, - text = "결혼식", - ) - - SusuFilledButton( - color = FilledButtonColor.Orange, - style = SmallButtonStyle.height32, - text = "돌잔치", - ) - - SusuFilledButton( - color = FilledButtonColor.Orange, - style = SmallButtonStyle.height32, - text = "장례식", - ) - - SusuFilledButton( - color = FilledButtonColor.Orange, - style = SmallButtonStyle.height32, - text = "생일 기념일", - ) - - AddConditionButton(onClick = {}) + uiState.categoryConfigList.dropLast(1).forEach { categoryConfig -> + SusuFilledButton( + isActive = categoryConfig.id == uiState.selectedCategoryId, + color = FilledButtonColor.Orange, + style = SmallButtonStyle.height32, + text = categoryConfig.name, + onClick = { onClickCategoryButton(categoryConfig.id) }, + ) + } - SusuTextFieldWrapContentButton( - color = TextFieldButtonColor.Orange, - style = SmallTextFieldButtonStyle.height32, - text = "친척 장례식", - isSaved = true, - ) + if (uiState.showCustomCategoryButton) { + SusuTextFieldWrapContentButton( + focusRequester = focusRequester, + onTextChange = onTextChangeCustomCategory, + color = TextFieldButtonColor.Orange, + style = SmallTextFieldButtonStyle.height32, + text = uiState.customCategory, + isFocused = uiState.categoryConfigList.last().id == uiState.selectedCategoryId, + isSaved = uiState.isCustomCategoryChipSaved, + onClickClearIcon = onClickCustomCategoryClearIcon, + onClickCloseIcon = onClickCustomCategoryCloseIcon, + onClickFilledButton = onClickCustomCategoryInnerButton, + onClickButton = { onClickCategoryButton(uiState.categoryConfigList.last().id) }, + ) + } else { + AddConditionButton(onClick = onClickAddConditionButton) + } } }, ) @@ -130,7 +195,13 @@ fun LedgerEditScreen() { content = { Column { AnnotatedText( - originalText = stringResource(R.string.ledger_edit_screen_from_date, 2023, 11, 25), + modifier = Modifier.susuClickable(rippleEnabled = false, onClick = onClickStartDateText), + originalText = stringResource( + R.string.ledger_edit_screen_from_date, + uiState.startYear, + uiState.startMonth, + uiState.startDay, + ), targetTextList = listOf( stringResource(R.string.ledger_edit_screen_year), stringResource(R.string.ledger_edit_screen_month), @@ -140,7 +211,13 @@ fun LedgerEditScreen() { spanStyle = SusuTheme.typography.title_m.copy(Gray80).toSpanStyle(), ) AnnotatedText( - originalText = stringResource(R.string.ledger_edit_screen_until_date, 2023, 11, 25), + modifier = Modifier.susuClickable(rippleEnabled = false, onClick = onClickEndDateText), + originalText = stringResource( + R.string.ledger_edit_screen_until_date, + uiState.endYear, + uiState.endMonth, + uiState.endDay, + ), targetTextList = listOf( stringResource(R.string.ledger_edit_screen_year), stringResource(R.string.ledger_edit_screen_month), @@ -161,10 +238,43 @@ fun LedgerEditScreen() { .fillMaxWidth() .imePadding(), shape = RectangleShape, + isActive = uiState.saveButtonEnabled, + isClickable = uiState.saveButtonEnabled, color = FilledButtonColor.Black, style = MediumButtonStyle.height60, text = stringResource(id = com.susu.core.ui.R.string.word_save), + onClick = onClickSaveButton, ) + + if (uiState.showStartDateBottomSheet) { + SusuLimitDatePickerBottomSheet( + initialYear = uiState.startYear, + initialMonth = uiState.startMonth, + initialDay = uiState.startDay, + criteriaYear = uiState.endYear, + criteriaMonth = uiState.endMonth, + criteriaDay = uiState.endDay, + afterDate = false, + maximumContainerHeight = 346.dp, + onDismissRequest = { _, _, _ -> onDismissStartDateBottomSheet() }, + onItemSelected = onStartDateItemSelected, + ) + } + + if (uiState.showEndDateBottomSheet) { + SusuLimitDatePickerBottomSheet( + initialYear = uiState.endYear, + initialMonth = uiState.endMonth, + initialDay = uiState.endDay, + criteriaYear = uiState.startYear, + criteriaMonth = uiState.startMonth, + criteriaDay = uiState.startDay, + afterDate = true, + maximumContainerHeight = 346.dp, + onDismissRequest = { _, _, _ -> onDismissEndDateBottomSheet() }, + onItemSelected = onEndDateItemSelected, + ) + } } } diff --git a/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditViewModel.kt new file mode 100644 index 00000000..08359bad --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/ledgeredit/LedgerEditViewModel.kt @@ -0,0 +1,151 @@ +package com.susu.feature.received.ledgeredit + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.susu.core.model.Category +import com.susu.core.model.Ledger +import com.susu.core.ui.base.BaseViewModel +import com.susu.core.ui.extension.decodeFromUri +import com.susu.core.ui.extension.encodeToUri +import com.susu.domain.usecase.categoryconfig.GetCategoryConfigUseCase +import com.susu.domain.usecase.ledger.EditLedgerUseCase +import com.susu.feature.received.navigation.ReceivedRoute +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch +import kotlinx.datetime.toJavaLocalDateTime +import kotlinx.datetime.toKotlinLocalDateTime +import kotlinx.serialization.json.Json +import java.time.LocalDateTime +import javax.inject.Inject + +@HiltViewModel +class LedgerEditViewModel @Inject constructor( + private val getCategoryConfigUseCase: GetCategoryConfigUseCase, + private val editLedgerUseCase: EditLedgerUseCase, + savedStateHandle: SavedStateHandle, +) : BaseViewModel( + LedgerEditState(), +) { + private val argument = savedStateHandle.get(ReceivedRoute.LEDGER_ARGUMENT_NAME)!! + private var ledgerId = 0 + private val toEditLedger + get() = with(currentState) { + Ledger( + id = ledgerId, + title = name, + startAt = LocalDateTime.of(startYear, startMonth, startDay, 0, 0).toKotlinLocalDateTime(), + endAt = LocalDateTime.of(endYear, endMonth, endDay, 0, 0).toKotlinLocalDateTime(), + category = Category( + id = selectedCategoryId, + customCategory = customCategory.ifEmpty { null }, + ), + ) + } + + fun editLedger() = viewModelScope.launch { + editLedgerUseCase(toEditLedger) + .onSuccess { ledger -> + postSideEffect(LedgerEditSideEffect.PopBackStackWithLedger(Json.encodeToUri(ledger))) + } + .onFailure { + } + } + + fun initData() = viewModelScope.launch { + getCategoryConfigUseCase() + .onSuccess { + intent { copy(categoryConfigList = it.toPersistentList()) } + initLedger() + } + .onFailure { } + } + + private fun initLedger() { + val ledger = Json.decodeFromUri(argument) + val (startDate, endDate) = (ledger.startAt.toJavaLocalDateTime() to ledger.endAt.toJavaLocalDateTime()) + intent { + ledgerId = ledger.id + val customCategory = ledger.category.customCategory + copy( + name = ledger.title, + selectedCategoryId = ledger.category.id, + startYear = startDate.year, + startMonth = startDate.monthValue, + startDay = startDate.dayOfMonth, + endYear = endDate.year, + endMonth = endDate.monthValue, + endDay = endDate.dayOfMonth, + customCategory = customCategory ?: "", + isCustomCategoryChipSaved = customCategory.isNullOrEmpty().not(), + showCustomCategoryButton = ledger.category.customCategory != null, + ) + } + } + + fun updateCategory(categoryId: Int) = intent { + copy(selectedCategoryId = categoryId) + } + + fun updateName(name: String) = intent { copy(name = name) } + + fun toggleCustomCategorySaved() = intent { + copy( + isCustomCategoryChipSaved = !isCustomCategoryChipSaved, + ) + } + + fun updateCustomCategory(customCategory: String) = intent { + copy( + customCategory = customCategory, + ) + } + + fun showCustomCategoryButton() { + intent { + copy( + showCustomCategoryButton = true, + ) + } + updateCategory(currentState.categoryConfigList.last().id) + postSideEffect(LedgerEditSideEffect.FocusCustomCategory) + } + + fun hideCustomCategoryButton() { + intent { + copy( + showCustomCategoryButton = false, + isCustomCategoryChipSaved = false, + ) + } + updateCustomCategory("") + if (currentState.isSelectedCustomCategory) { + updateCategory(currentState.categoryConfigList.first().id) + } + } + + fun updateStartYear(year: Int, month: Int, day: Int) { + intent { + copy( + startYear = year, + startMonth = month, + startDay = day, + ) + } + } + + fun updateEndYear(year: Int, month: Int, day: Int) = intent { + copy( + endYear = year, + endMonth = month, + endDay = day, + ) + } + + fun showStartDateBottomSheet() = intent { copy(showStartDateBottomSheet = true) } + fun hideStartDateBottomSheet() = intent { copy(showStartDateBottomSheet = false) } + fun showEndDateBottomSheet() = intent { copy(showEndDateBottomSheet = true) } + fun hideEndDateBottomSheet() = intent { copy(showEndDateBottomSheet = false) } + + fun popBackStack() = postSideEffect(LedgerEditSideEffect.PopBackStack) +} diff --git a/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt b/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt index 867246b8..91efea55 100644 --- a/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt +++ b/feature/received/src/main/java/com/susu/feature/received/navigation/ReceivedNavigation.kt @@ -7,27 +7,32 @@ import androidx.navigation.NavOptions import androidx.navigation.NavType import androidx.navigation.compose.composable import androidx.navigation.navArgument +import com.susu.core.model.Ledger +import com.susu.core.ui.DialogToken +import com.susu.core.ui.SnackbarToken +import com.susu.core.ui.extension.encodeToUri import com.susu.feature.received.ledgeradd.LedgerAddRoute import com.susu.feature.received.ledgerdetail.LedgerDetailRoute import com.susu.feature.received.ledgeredit.LedgerEditRoute import com.susu.feature.received.ledgerfilter.LedgerFilterRoute import com.susu.feature.received.received.ReceivedRoute import com.susu.feature.received.search.LedgerSearchRoute +import kotlinx.serialization.json.Json fun NavController.navigateReceived(navOptions: NavOptions) { navigate(ReceivedRoute.route, navOptions) } -fun NavController.navigateLedgerDetail(id: Int) { - navigate(ReceivedRoute.ledgerDetailRoute(id.toString())) +fun NavController.navigateLedgerDetail(ledger: Ledger) { + navigate(ReceivedRoute.ledgerDetailRoute(Json.encodeToUri(ledger))) } fun NavController.navigateLedgerSearch() { navigate(ReceivedRoute.ledgerSearchRoute) } -fun NavController.navigateLedgerEdit() { - navigate(ReceivedRoute.ledgerEditRoute) +fun NavController.navigateLedgerEdit(ledger: Ledger) { + navigate(ReceivedRoute.ledgerEditRoute(Json.encodeToUri(ledger))) } fun NavController.navigateLedgerFilter() { @@ -40,15 +45,24 @@ fun NavController.navigateLedgerAdd() { fun NavGraphBuilder.receivedNavGraph( padding: PaddingValues, - navigateLedgerDetail: (Int) -> Unit, + navigateLedgerDetail: (Ledger) -> Unit, popBackStack: () -> Unit, + popBackStackWithLedger: (String) -> Unit, + popBackStackWithDeleteLedgerId: (Int) -> Unit, navigateLedgerSearch: () -> Unit, - navigateLedgerEdit: () -> Unit, + navigateLedgerEdit: (Ledger) -> Unit, navigateLedgerFilter: () -> Unit, navigateLedgerAdd: () -> Unit, + onShowSnackbar: (SnackbarToken) -> Unit, + onShowDialog: (DialogToken) -> Unit, + handleException: (Throwable, () -> Unit) -> Unit, ) { - composable(route = ReceivedRoute.route) { + composable(route = ReceivedRoute.route) { navBackStackEntry -> + val ledger = navBackStackEntry.savedStateHandle.get(ReceivedRoute.LEDGER_ARGUMENT_NAME) + val toDeleteLedgerId = navBackStackEntry.savedStateHandle.get(ReceivedRoute.LEDGER_ID_ARGUMENT_NAME) ?: -1 ReceivedRoute( + ledger = ledger, + toDeleteLedgerId = toDeleteLedgerId, padding = padding, navigateLedgerDetail = navigateLedgerDetail, navigateLedgerSearch = navigateLedgerSearch, @@ -58,16 +72,22 @@ fun NavGraphBuilder.receivedNavGraph( } composable( - route = ReceivedRoute.ledgerDetailRoute("{${ReceivedRoute.LEDGER_DETAIL_ARGUMENT_NAME}}"), + route = ReceivedRoute.ledgerDetailRoute("{${ReceivedRoute.LEDGER_ARGUMENT_NAME}}"), arguments = listOf( - navArgument(ReceivedRoute.LEDGER_DETAIL_ARGUMENT_NAME) { + navArgument(ReceivedRoute.LEDGER_ARGUMENT_NAME) { type = NavType.StringType - defaultValue = "0" }, ), - ) { + ) { navBackStackEntry -> + val ledger = navBackStackEntry.savedStateHandle.get(ReceivedRoute.LEDGER_ARGUMENT_NAME) LedgerDetailRoute( + ledger = ledger, navigateLedgerEdit = navigateLedgerEdit, + popBackStackWithLedger = popBackStackWithLedger, + popBackStackWithDeleteLedgerId = popBackStackWithDeleteLedgerId, + onShowSnackbar = onShowSnackbar, + onShowDialog = onShowDialog, + handleException = handleException, ) } @@ -78,9 +98,12 @@ fun NavGraphBuilder.receivedNavGraph( ) } composable( - route = ReceivedRoute.ledgerEditRoute, + route = ReceivedRoute.ledgerEditRoute("{${ReceivedRoute.LEDGER_ARGUMENT_NAME}}"), ) { - LedgerEditRoute(popBackStack = popBackStack) + LedgerEditRoute( + popBackStack = popBackStack, + popBackStackWithLedger = popBackStackWithLedger, + ) } composable( @@ -100,11 +123,12 @@ fun NavGraphBuilder.receivedNavGraph( object ReceivedRoute { const val route = "received" - const val LEDGER_DETAIL_ARGUMENT_NAME = "ledgerDetailId" - fun ledgerDetailRoute(id: String = "0") = "ledger-detail/$id" + const val LEDGER_ARGUMENT_NAME = "ledger" + const val LEDGER_ID_ARGUMENT_NAME = "ledger-id" + fun ledgerDetailRoute(ledger: String) = "ledger-detail/$ledger" + fun ledgerEditRoute(ledger: String) = "ledger-edit/$ledger" const val ledgerSearchRoute = "ledger-search" - const val ledgerEditRoute = "ledger-edit" // TODO 파라미터 넘기는 방식으로 수정해야함. const val ledgerAddRoute = "ledger-add" // TODO 파라미터 넘기는 방식으로 수정해야함. const val ledgerFilterRoute = "ledger-filter" // TODO 파라미터 넘기는 방식으로 수정해야함. } diff --git a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedContract.kt b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedContract.kt new file mode 100644 index 00000000..c7d18f99 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedContract.kt @@ -0,0 +1,21 @@ +package com.susu.feature.received.received + +import com.susu.core.model.Ledger +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +data class ReceivedState( + val isLoading: Boolean = false, + val showAlignBottomSheet: Boolean = false, + val ledgerList: PersistentList = persistentListOf(), + val showEmptyLedger: Boolean = false, +) : UiState + +sealed interface ReceivedEffect : SideEffect { + data class NavigateLedgerDetail(val ledger: Ledger) : ReceivedEffect + data object NavigateLedgerAdd : ReceivedEffect + data object NavigateLedgerSearch : ReceivedEffect + data object NavigateLedgerFilter : ReceivedEffect +} diff --git a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt index 16e1fe88..91a39a6e 100644 --- a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedScreen.kt @@ -10,21 +10,24 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.susu.core.designsystem.component.appbar.SusuDefaultAppBar import com.susu.core.designsystem.component.appbar.icon.LogoIcon import com.susu.core.designsystem.component.appbar.icon.NotificationIcon @@ -36,7 +39,10 @@ import com.susu.core.designsystem.component.button.SusuFloatingButton import com.susu.core.designsystem.component.button.SusuGhostButton import com.susu.core.designsystem.theme.Gray50 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.Ledger import com.susu.core.ui.alignList +import com.susu.core.ui.extension.OnBottomReached +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.received.R import com.susu.feature.received.received.component.LedgerAddCard import com.susu.feature.received.received.component.LedgerCard @@ -44,33 +50,62 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun ReceivedRoute( + viewModel: ReceivedViewModel = hiltViewModel(), + ledger: String?, + toDeleteLedgerId: Int, padding: PaddingValues, - navigateLedgerDetail: (Int) -> Unit, + navigateLedgerDetail: (Ledger) -> Unit, navigateLedgerSearch: () -> Unit, navigateLedgerFilter: () -> Unit, navigateLedgerAdd: () -> Unit, ) { + val uiState = viewModel.uiState.collectAsStateWithLifecycle().value + val ledgerListState = rememberLazyGridState() + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + ReceivedEffect.NavigateLedgerAdd -> navigateLedgerAdd() + is ReceivedEffect.NavigateLedgerDetail -> navigateLedgerDetail(sideEffect.ledger) + ReceivedEffect.NavigateLedgerFilter -> navigateLedgerFilter() + ReceivedEffect.NavigateLedgerSearch -> navigateLedgerSearch() + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.updateLedgerIfNeed(ledger = ledger, toDeleteLedgerId = toDeleteLedgerId) + } + + ledgerListState.OnBottomReached { + viewModel.getLedgerList() + } + ReceiveScreen( + uiState = uiState, + ledgerListState = ledgerListState, padding = padding, - onClickLedgerCard = navigateLedgerDetail, - onClickLedgerAddCard = navigateLedgerAdd, - onClickSearchIcon = navigateLedgerSearch, // TODO SideEffect로 변경 - onClickFloatingAddButton = navigateLedgerAdd, - onClickFilterButton = navigateLedgerFilter, + onClickLedgerCard = viewModel::navigateLedgerDetail, + onClickLedgerAddCard = viewModel::navigateLedgerAdd, + onClickSearchIcon = viewModel::navigateLedgerSearch, + onClickFloatingAddButton = viewModel::navigateLedgerAdd, + onClickFilterButton = viewModel::navigateLedgerFilter, + onClickAlignButton = viewModel::showAlignBottomSheet, + onDismissAlignBottomSheet = viewModel::hideAlignBottomSheet, ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReceiveScreen( + uiState: ReceivedState = ReceivedState(), + ledgerListState: LazyGridState = rememberLazyGridState(), padding: PaddingValues, onClickSearchIcon: () -> Unit = {}, onClickNotificationIcon: () -> Unit = {}, onClickAlignButton: () -> Unit = {}, onClickFilterButton: () -> Unit = {}, onClickLedgerAddCard: () -> Unit = {}, - onClickLedgerCard: (Int) -> Unit = {}, + onClickLedgerCard: (Ledger) -> Unit = {}, onClickFloatingAddButton: () -> Unit = {}, + onDismissAlignBottomSheet: () -> Unit = {}, ) { Box( modifier = Modifier @@ -93,82 +128,81 @@ fun ReceiveScreen( }, ) - Column( - modifier = Modifier.padding(SusuTheme.spacing.spacing_m), - verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_m), + LazyVerticalGrid( + modifier = Modifier + .fillMaxSize(), + state = ledgerListState, + contentPadding = PaddingValues( + SusuTheme.spacing.spacing_m, + ), + columns = GridCells.Fixed(2), + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), ) { - Row( - horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + item( + span = { GridItemSpan(2) }, ) { - SusuGhostButton( - color = GhostButtonColor.Black, - style = SmallButtonStyle.height32, - text = alignList[0], // TODO State 변환 - leftIcon = { - Icon( - painter = painterResource(id = com.susu.core.ui.R.drawable.ic_align), - contentDescription = stringResource(R.string.content_description_align_icon), - ) - }, - onClick = onClickAlignButton, - ) - SusuGhostButton( - color = GhostButtonColor.Black, - style = SmallButtonStyle.height32, - text = stringResource(com.susu.core.ui.R.string.word_filter), - leftIcon = { - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(id = com.susu.core.ui.R.drawable.ic_filter), - contentDescription = stringResource(R.string.content_description_filter_icon), - ) - }, - onClick = onClickFilterButton, - ) - } - - LazyVerticalGrid( - modifier = Modifier - .fillMaxSize(), - columns = GridCells.Fixed(2), - verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), - horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), - ) { - item { - LedgerCard( - ledgerType = "결혼식", - title = "나의 결혼식", - money = 4335000, - count = 164, - onClick = { onClickLedgerCard(1) }, + Row( + modifier = Modifier.padding(bottom = SusuTheme.spacing.spacing_xxs), + horizontalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + SusuGhostButton( + color = GhostButtonColor.Black, + style = SmallButtonStyle.height32, + text = alignList[0], // TODO State 변환 + leftIcon = { + Icon( + painter = painterResource(id = com.susu.core.ui.R.drawable.ic_align), + contentDescription = stringResource(R.string.content_description_align_icon), + ) + }, + onClick = onClickAlignButton, ) - } - - item { - LedgerCard( - ledgerType = "결혼식", - title = "나의 결혼식", - money = 4335000, - count = 164, - onClick = { onClickLedgerCard(1) }, + SusuGhostButton( + color = GhostButtonColor.Black, + style = SmallButtonStyle.height32, + text = stringResource(com.susu.core.ui.R.string.word_filter), + leftIcon = { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = com.susu.core.ui.R.drawable.ic_filter), + contentDescription = stringResource(R.string.content_description_filter_icon), + ) + }, + onClick = onClickFilterButton, ) } + } - item { - LedgerAddCard( - onClick = onClickLedgerAddCard, - ) - } + items( + items = uiState.ledgerList, + key = { it.id }, + ) { ledger -> + LedgerCard( + ledgerType = ledger.category.name, + title = ledger.title, + money = ledger.totalAmounts, + count = ledger.totalCounts, + onClick = { onClickLedgerCard(ledger) }, + ) + } + + item { + LedgerAddCard( + onClick = onClickLedgerAddCard, + ) } } } - Text( - modifier = Modifier.align(Alignment.Center), - text = stringResource(R.string.received_screen_empty_ledger), - style = SusuTheme.typography.text_s, - color = Gray50, - ) + if (uiState.showEmptyLedger) { + Text( + modifier = Modifier.align(Alignment.Center), + text = stringResource(R.string.received_screen_empty_ledger), + style = SusuTheme.typography.text_s, + color = Gray50, + ) + } SusuFloatingButton( modifier = Modifier @@ -177,14 +211,9 @@ fun ReceiveScreen( onClick = onClickFloatingAddButton, ) - // TODO State 변환 - var isSheetOpen by remember { - mutableStateOf(true) - } - - if (isSheetOpen) { + if (uiState.showAlignBottomSheet) { SusuSelectionBottomSheet( - onDismissRequest = { isSheetOpen = false }, + onDismissRequest = onDismissAlignBottomSheet, containerHeight = 250.dp, items = alignList.toImmutableList(), selectedItemPosition = 0, // TODO State 변환 diff --git a/feature/received/src/main/java/com/susu/feature/received/received/ReceivedViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedViewModel.kt new file mode 100644 index 00000000..6d7d25d5 --- /dev/null +++ b/feature/received/src/main/java/com/susu/feature/received/received/ReceivedViewModel.kt @@ -0,0 +1,62 @@ +package com.susu.feature.received.received + +import androidx.lifecycle.viewModelScope +import com.susu.core.model.Ledger +import com.susu.core.ui.base.BaseViewModel +import com.susu.core.ui.extension.decodeFromUri +import com.susu.domain.usecase.ledger.GetLedgerListUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import javax.inject.Inject + +@HiltViewModel +class ReceivedViewModel @Inject constructor( + private val getLedgerListUseCase: GetLedgerListUseCase, +) : BaseViewModel( + ReceivedState(), +) { + private var page = 0 + private var isLast = false + + fun getLedgerList() = viewModelScope.launch { + if (isLast) return@launch + + getLedgerListUseCase( + GetLedgerListUseCase.Param( + page = page, + ), + ).onSuccess { ledgerList -> + isLast = ledgerList.isEmpty() + page++ + val newLedgerList = currentState.ledgerList.plus(ledgerList).toPersistentList() + intent { + copy( + ledgerList = newLedgerList, + showEmptyLedger = newLedgerList.isEmpty(), + ) + } + } + } + + fun updateLedgerIfNeed(ledger: String?, toDeleteLedgerId: Int) { + val toUpdateLedger = ledger?.let { + Json.decodeFromUri(ledger) + } ?: Ledger() + val newList = currentState + .ledgerList + .filter { it.id != toDeleteLedgerId } + .map { if (it.id == toUpdateLedger.id) toUpdateLedger else it } + .toPersistentList() + + intent { copy(ledgerList = newList) } + } + + fun showAlignBottomSheet() = intent { copy(showAlignBottomSheet = true) } + fun hideAlignBottomSheet() = intent { copy(showAlignBottomSheet = false) } + fun navigateLedgerDetail(ledger: Ledger) = postSideEffect(ReceivedEffect.NavigateLedgerDetail(ledger)) + fun navigateLedgerAdd() = postSideEffect(ReceivedEffect.NavigateLedgerAdd) + fun navigateLedgerSearch() = postSideEffect(ReceivedEffect.NavigateLedgerSearch) + fun navigateLedgerFilter() = postSideEffect(ReceivedEffect.NavigateLedgerFilter) +} diff --git a/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchContract.kt b/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchContract.kt index f7a2e55d..fe257169 100644 --- a/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchContract.kt +++ b/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchContract.kt @@ -16,5 +16,5 @@ data class LedgerSearchState( sealed interface LedgerSearchSideEffect : SideEffect { data object PopBackStack : LedgerSearchSideEffect - data class NavigateLedgerDetail(val id: Int) : LedgerSearchSideEffect + data class NavigateLedgerDetail(val ledger: Ledger) : LedgerSearchSideEffect } diff --git a/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchScreen.kt b/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchScreen.kt index 2d968a6e..17083f17 100644 --- a/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchScreen.kt +++ b/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchScreen.kt @@ -39,13 +39,13 @@ import kotlinx.coroutines.flow.debounce fun LedgerSearchRoute( viewModel: LedgerSearchViewModel = hiltViewModel(), popBackStack: () -> Unit, - navigateLedgerDetail: (Int) -> Unit, + navigateLedgerDetail: (Ledger) -> Unit, ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle().value viewModel.sideEffect.collectWithLifecycle { sideEffect -> when (sideEffect) { LedgerSearchSideEffect.PopBackStack -> popBackStack() - is LedgerSearchSideEffect.NavigateLedgerDetail -> navigateLedgerDetail(sideEffect.id) + is LedgerSearchSideEffect.NavigateLedgerDetail -> navigateLedgerDetail(sideEffect.ledger) } } @@ -69,9 +69,9 @@ fun LedgerSearchRoute( viewModel.upsertLedgerRecentSearch(search) }, onClickRecentSearchContainerCloseIcon = viewModel::deleteLedgerRecentSearch, - onClickSearchResultContainer = { - viewModel.upsertLedgerRecentSearch(it.title) - viewModel.navigateLedgerDetail(it.id) + onClickSearchResultContainer = { ledger -> + viewModel.upsertLedgerRecentSearch(ledger.title) + viewModel.navigateLedgerDetail(ledger) }, ) } diff --git a/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchViewModel.kt b/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchViewModel.kt index d8210429..4f1372d3 100644 --- a/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchViewModel.kt +++ b/feature/received/src/main/java/com/susu/feature/received/search/LedgerSearchViewModel.kt @@ -1,6 +1,7 @@ package com.susu.feature.received.search import androidx.lifecycle.viewModelScope +import com.susu.core.model.Ledger import com.susu.core.ui.base.BaseViewModel import com.susu.domain.usecase.ledger.GetLedgerListUseCase import com.susu.domain.usecase.ledgerrecentsearch.DeleteLedgerRecentSearchUseCase @@ -20,7 +21,7 @@ class LedgerSearchViewModel @Inject constructor( ) : BaseViewModel( LedgerSearchState(), ) { - fun navigateLedgerDetail(id: Int) = postSideEffect(LedgerSearchSideEffect.NavigateLedgerDetail(id)) + fun navigateLedgerDetail(ledger: Ledger) = postSideEffect(LedgerSearchSideEffect.NavigateLedgerDetail(ledger)) fun getLedgerRecentSearchList() = viewModelScope.launch { getLedgerRecentSearchListUseCase() @@ -44,7 +45,14 @@ class LedgerSearchViewModel @Inject constructor( fun getLedgerList(search: String) = viewModelScope.launch { getLedgerListUseCase(GetLedgerListUseCase.Param(title = search)) - .onSuccess { intent { copy(ledgerList = it.toPersistentList()) } } + .onSuccess { + intent { + copy( + ledgerList = it.toPersistentList(), + showSearchResultEmpty = it.isEmpty(), + ) + } + } } fun popBackStack() = postSideEffect(LedgerSearchSideEffect.PopBackStack) @@ -52,7 +60,6 @@ class LedgerSearchViewModel @Inject constructor( private fun updateRecentSearchList(searchList: List) = intent { copy( recentSearchKeywordList = searchList.toPersistentList(), - showSearchResultEmpty = searchList.isEmpty(), ) } } diff --git a/feature/received/src/main/res/values/strings.xml b/feature/received/src/main/res/values/strings.xml index cfdcf105..f9cee99c 100644 --- a/feature/received/src/main/res/values/strings.xml +++ b/feature/received/src/main/res/values/strings.xml @@ -25,4 +25,7 @@ %d년 %d월 %d일 까지 부터 + 장부를 삭제할까요? + 삭제한 장부와 봉투는 다시 복구할 수 없어요 + 장부가 삭제되었어요