From dacf8e076b6d022841b830ec1c47c0588f2f4359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EB=B3=91=ED=97=8C?= <64366488+Heonbyeong@users.noreply.github.com> Date: Tue, 21 Oct 2025 23:15:23 +0900 Subject: [PATCH 01/12] =?UTF-8?q?[ID-44]=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20UI=20=EA=B0=9C=EB=B0=9C=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: network module domain dependency * feat: outline tag component to design system * feat: category bottomsheet to ledger manual * feat: create category bottom sheet & interaction * feat: category input handling * feat: category row list to ledger manual screen * refactor: tag horizontal spacedBy padding * refactor: category bottomsheet header text color * refactor: bottomsheet dismiss request apply invokeOnCompletion * refactor: remove semicolon with bottomsheet type enum class * refactor: spacing between title and textfield in create bottomsheet * refactor: remove spacing below the textfield in create bottomsheet * refactor: change tag value text style from Body4 to Body3 * refactor: remove hide event from bottomsheet back handler lambda --- .../design_system/component/tag/Tag.kt | 67 +++- .../ledgermanual/LedgerManualScreen.kt | 60 +++- .../ledgermanual/LedgerManualState.kt | 11 +- .../ledgermanual/LedgerManualViewModel.kt | 12 + .../view/LedgerManualBottomSheetType.kt | 6 + .../view/LedgerManualCategoryBottomSheet.kt | 298 ++++++++++++++++++ 6 files changed, 451 insertions(+), 3 deletions(-) create mode 100644 feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualBottomSheetType.kt create mode 100644 feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt diff --git a/core/design-system/src/main/java/com/moneymong/moneymong/design_system/component/tag/Tag.kt b/core/design-system/src/main/java/com/moneymong/moneymong/design_system/component/tag/Tag.kt index 2f60719a..935b6e12 100644 --- a/core/design-system/src/main/java/com/moneymong/moneymong/design_system/component/tag/Tag.kt +++ b/core/design-system/src/main/java/com/moneymong/moneymong/design_system/component/tag/Tag.kt @@ -2,6 +2,7 @@ package com.moneymong.moneymong.design_system.component.tag import androidx.annotation.DrawableRes import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -18,7 +19,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.moneymong.moneymong.design_system.theme.Blue04 import com.moneymong.moneymong.design_system.theme.Body2 +import com.moneymong.moneymong.design_system.theme.Body3 +import com.moneymong.moneymong.design_system.theme.Gray03 +import com.moneymong.moneymong.design_system.theme.Gray05 +import com.moneymong.moneymong.design_system.theme.Gray06 import com.moneymong.moneymong.design_system.theme.White +import com.moneymong.moneymong.ui.noRippleClickable @Composable fun MDSTag( @@ -54,6 +60,46 @@ fun MDSTag( } } +@Composable +fun MDSOutlineTag( + modifier: Modifier = Modifier, + text: String, + @DrawableRes iconResource: Int? = null, + onClick: () -> Unit, +) { + Row( + modifier = modifier + .border( + width = 1.4.dp, + color = Gray03, + shape = RoundedCornerShape(size = Int.MAX_VALUE.dp) + ) + .background( + color = White, + shape = RoundedCornerShape(size = Int.MAX_VALUE.dp) + ) + .padding(horizontal = 12.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + color = Gray06, + style = Body3, + ) + if (iconResource != null) { + Icon( + modifier = Modifier + .size(18.dp) + .noRippleClickable(onClick), + painter = painterResource(id = iconResource), + contentDescription = "Tag icon", + tint = Gray05 + ) + } + } +} + @Preview(showBackground = true) @Composable fun MDSTagPreview() { @@ -73,4 +119,23 @@ fun MDSTagPreview() { iconResource = com.moneymong.moneymong.design_system.R.drawable.ic_pencil ) } -} \ No newline at end of file +} + +@Preview(showBackground = true) +@Composable +fun MDSOutlineTagPreview() { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + MDSOutlineTag( + text = "tag", + onClick = {}, + ) + MDSOutlineTag( + text = "tag", + iconResource = com.moneymong.moneymong.design_system.R.drawable.ic_close_default, + onClick = {}, + ) + } +} diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt index 067b91a7..288df93f 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -26,13 +28,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -53,6 +59,7 @@ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.moneymong.moneymong.android.util.base64ToFile import com.moneymong.moneymong.android.util.encodingBase64 +import com.moneymong.moneymong.design_system.R import com.moneymong.moneymong.ui.noRippleClickable import com.moneymong.moneymong.design_system.R.drawable import com.moneymong.moneymong.design_system.component.button.MDSButton @@ -60,6 +67,7 @@ import com.moneymong.moneymong.design_system.component.button.MDSButtonSize import com.moneymong.moneymong.design_system.component.button.MDSButtonType import com.moneymong.moneymong.design_system.component.modal.MDSModal import com.moneymong.moneymong.design_system.component.selection.MDSSelection +import com.moneymong.moneymong.design_system.component.tag.MDSOutlineTag import com.moneymong.moneymong.design_system.component.textfield.MDSTextField import com.moneymong.moneymong.design_system.component.textfield.util.MDSTextFieldIcons import com.moneymong.moneymong.design_system.component.textfield.util.withRequiredMark @@ -68,18 +76,23 @@ import com.moneymong.moneymong.design_system.component.textfield.visualtransform import com.moneymong.moneymong.design_system.component.textfield.visualtransformation.TimeVisualTransformation import com.moneymong.moneymong.design_system.error.ErrorDialog import com.moneymong.moneymong.design_system.theme.Blue03 +import com.moneymong.moneymong.design_system.theme.Blue04 import com.moneymong.moneymong.design_system.theme.Body2 import com.moneymong.moneymong.design_system.theme.Body3 import com.moneymong.moneymong.design_system.theme.Gray06 import com.moneymong.moneymong.design_system.theme.Gray10 import com.moneymong.moneymong.design_system.theme.MMHorizontalSpacing import com.moneymong.moneymong.design_system.theme.White +import com.moneymong.moneymong.ledgermanual.view.LedgerManualCategoryBottomSheet import com.moneymong.moneymong.ledgermanual.view.LedgerManualTopbarView import com.moneymong.moneymong.model.ledger.FundType +import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -@OptIn(ExperimentalGlideComposeApi::class) +@OptIn(ExperimentalGlideComposeApi::class, ExperimentalMaterial3Api::class, + ExperimentalLayoutApi::class +) @Composable fun LedgerManualScreen( modifier: Modifier = Modifier, @@ -99,6 +112,8 @@ fun LedgerManualScreen( } } ) + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() viewModel.collectSideEffect { when (it) { @@ -168,6 +183,21 @@ fun LedgerManualScreen( ) } + if (state.showBottomSheet) { + LedgerManualCategoryBottomSheet( + sheetState = sheetState, + categories = emptyList(), + categoryValue = state.categoryValue, + isSystemCategoryError = state.isSystemCategoryError, + onDismissRequest = { + scope.launch { + sheetState.hide() + }.invokeOnCompletion { viewModel.onDismissBottomSheet() } + }, + onChangeCategoryValue = viewModel::onChangeCategoryValue + ) + } + Scaffold( topBar = { LedgerManualTopbarView( @@ -311,6 +341,34 @@ fun LedgerManualScreen( keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) ) Spacer(modifier = Modifier.height(24.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "카테고리", + style = Body2, + color = Gray06, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + modifier = Modifier.noRippleClickable(viewModel::onClickCategoryEdit), + text = "수정", + style = Body2, + color = Blue04, + ) + } + Spacer(modifier = Modifier.height(8.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + MDSOutlineTag( + text = "Test", // TODO + iconResource = drawable.ic_close_default, + onClick = {}, + ) + } + Spacer(modifier = Modifier.height(24.dp)) Text( text = "사진 첨부 (최대 12장)", style = Body2, diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt index 294d825a..1a3a2eea 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt @@ -25,7 +25,9 @@ data class LedgerManualState( val isMemoError: Boolean = false, val showPopBackStackModal: Boolean = false, val showErrorDialog: Boolean = false, - val errorMessage: String = "" + val errorMessage: String = "", + val showBottomSheet: Boolean = false, + val categoryValue: TextFieldValue = TextFieldValue(), ) : State { val enabled: Boolean @@ -53,4 +55,11 @@ data class LedgerManualState( val formattedTime = timeFormat.format(timeFormat.parse(paymentTimeValue.text)) return "$formattedDate $formattedTime".toZonedDateTime("yyyyMMdd HHmmss") } + + val isSystemCategoryError: Boolean + get() = categoryValue.text == SYSTEM_CATEGORY + + companion object { + private const val SYSTEM_CATEGORY = "카테고리 없음" + } } diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt index ac10bc8a..89579c21 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt @@ -177,6 +177,18 @@ class LedgerManualViewModel @Inject constructor( fun onClickErrorDialogConfirm() = eventEmit(LedgerManualSideEffect.LedgerManualHideErrorDialog) + fun onClickCategoryEdit() = intent { reduce { state.copy(showBottomSheet = true) } } + + fun onDismissBottomSheet() = intent { reduce { state.copy(showBottomSheet = false) } } + + fun onChangeCategoryValue(value: TextFieldValue) = blockingIntent { + val validate = value.text.validateValue(length = 10) + + if (validate) { + reduce { state.copy(categoryValue = value) } + } + } + private fun trimStartWithZero(value: TextFieldValue) = if (value.text.isNotEmpty() && value.text.all { it == '0' }) { value.copy(text = "0") diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualBottomSheetType.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualBottomSheetType.kt new file mode 100644 index 00000000..cc25f684 --- /dev/null +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualBottomSheetType.kt @@ -0,0 +1,6 @@ +package com.moneymong.moneymong.ledgermanual.view + +enum class LedgerManualBottomSheetType { + LIST, + CREATE, +} diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt new file mode 100644 index 00000000..ffd36754 --- /dev/null +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt @@ -0,0 +1,298 @@ +package com.moneymong.moneymong.ledgermanual.view + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.moneymong.moneymong.design_system.theme.MMHorizontalSpacing +import com.moneymong.moneymong.design_system.R +import com.moneymong.moneymong.design_system.component.bottomSheet.MDSBottomSheet +import com.moneymong.moneymong.design_system.component.button.MDSButton +import com.moneymong.moneymong.design_system.component.button.MDSButtonSize +import com.moneymong.moneymong.design_system.component.button.MDSButtonType +import com.moneymong.moneymong.design_system.component.tag.MDSOutlineTag +import com.moneymong.moneymong.design_system.component.textfield.MDSTextField +import com.moneymong.moneymong.design_system.component.textfield.util.MDSTextFieldIcons +import com.moneymong.moneymong.design_system.theme.Black +import com.moneymong.moneymong.design_system.theme.Blue04 +import com.moneymong.moneymong.design_system.theme.Body2 +import com.moneymong.moneymong.design_system.theme.Body3 +import com.moneymong.moneymong.design_system.theme.Gray05 +import com.moneymong.moneymong.design_system.theme.Gray07 +import com.moneymong.moneymong.design_system.theme.Heading1 +import com.moneymong.moneymong.design_system.theme.Heading4 +import com.moneymong.moneymong.design_system.theme.White +import com.moneymong.moneymong.ui.noRippleClickable + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LedgerManualCategoryBottomSheet( + modifier: Modifier = Modifier, + sheetState: SheetState, + categories: List, // TODO API response + categoryValue: TextFieldValue, + isSystemCategoryError: Boolean, + onDismissRequest: () -> Unit, + onChangeCategoryValue: (TextFieldValue) -> Unit, +) { + var sheetType by remember { mutableStateOf(LedgerManualBottomSheetType.LIST) } + + MDSBottomSheet( + modifier = modifier, + sheetState = sheetState, + onDismissRequest = onDismissRequest, + ) { + AnimatedContent( + targetState = sheetType, + transitionSpec = { + when (targetState) { + LedgerManualBottomSheetType.CREATE -> { + slideInHorizontally { fullWidth -> fullWidth } + .togetherWith(slideOutHorizontally { fullWidth -> fullWidth / -3 }) + } + + LedgerManualBottomSheetType.LIST -> { + slideInHorizontally { fullWidth -> -fullWidth } + .togetherWith(slideOutHorizontally { fullWidth -> fullWidth / 3 }) + } + } + } + ) { targetState -> + when (targetState) { + LedgerManualBottomSheetType.LIST -> { + LedgerManualCategoryBottomSheetContent( + categories = categories, + onDismissRequest = onDismissRequest, + onClickCreate = { sheetType = LedgerManualBottomSheetType.CREATE } + ) + } + + LedgerManualBottomSheetType.CREATE -> { + LedgerManualCategoryCreateBottomSheetContent( + textFieldValue = categoryValue, + isSystemCategoryError = isSystemCategoryError, + categories = categories, + onValueChange = onChangeCategoryValue, + onClickRegister = {}, + onPrev = { sheetType = LedgerManualBottomSheetType.LIST } + ) + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun LedgerManualCategoryBottomSheetContent( + modifier: Modifier = Modifier, + categories: List, + onDismissRequest: () -> Unit, + onClickCreate: () -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .height(448.dp) + .background(White) + .padding(horizontal = MMHorizontalSpacing, vertical = 20.dp), + ) { + Icon( + modifier = Modifier + .align(alignment = Alignment.End) + .noRippleClickable(onDismissRequest), + painter = painterResource(R.drawable.ic_close_default), + contentDescription = null + ) + Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + style = Heading4, + color = Black, + text = "카테고리", + ) + Text( + modifier = Modifier.noRippleClickable(onClickCreate), + style = Body3, + color = Blue04, + text = "추가", + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + style = Body2, + color = Gray05, + text = "원하는 카테고리를 마음대로 만들 수 있어요", + ) + Spacer(modifier = Modifier.height(16.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + categories.forEach { + MDSOutlineTag( + text = it, + iconResource = R.drawable.ic_close_default, + onClick = {}, + ) + } + } + } +} + +@Composable +fun LedgerManualCategoryCreateBottomSheetContent( + modifier: Modifier = Modifier, + textFieldValue: TextFieldValue, + isSystemCategoryError: Boolean, + categories: List, // TODO API response + onValueChange: (TextFieldValue) -> Unit, + onClickRegister: () -> Unit, + onPrev: () -> Unit, +) { + val maxCount = 10 + var isFilled by remember { mutableStateOf(false) } + val focusRequester = remember { FocusRequester() } + val keyboard = LocalSoftwareKeyboardController.current + val isExists = categories.contains(textFieldValue.text) + val helperText by remember(isSystemCategoryError, isExists) { + derivedStateOf { + when { + isSystemCategoryError -> "사용할 수 없는 카테고리 이름이에요" + isExists -> "이미 있는 카테고리에요" + else -> "" + } + } + } + + BackHandler { + onPrev() + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Column( + modifier = modifier + .fillMaxWidth() + .background(White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding(horizontal = MMHorizontalSpacing, vertical = 20.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .size(24.dp) + .noRippleClickable { + keyboard?.hide() + onPrev() + }, + painter = painterResource(R.drawable.ic_chevron_left), + contentDescription = null, + tint = Gray07, + ) + Text( + text = "카테고리 생성", + style = Heading1, + color = Black, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + MDSTextField( + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { isFilled = !it.isFocused } + .focusRequester(focusRequester), + placeholder = "카테고리를 입력해주세요", + value = textFieldValue, + onValueChange = onValueChange, + title = "", + isFilled = isFilled, + isError = isSystemCategoryError || isExists, + singleLine = true, + helperText = helperText, + icon = MDSTextFieldIcons.Clear, + onIconClick = { onValueChange(TextFieldValue("")) }, + maxCount = maxCount + ) + } + val enabled = textFieldValue.text.isNotBlank() && (!isSystemCategoryError && !isExists) + MDSButton( + modifier = Modifier.fillMaxWidth(), + text = "등록", + type = MDSButtonType.PRIMARY, + size = MDSButtonSize.LARGE, + cornerShape = 0.dp, + enabled = enabled, + onClick = onClickRegister, + ) + } +} + +@Preview(showBackground = true) +@Composable +fun LedgerManualCategoryBottomSheetContentPreview() { + val categories = listOf("testTooLongTextOverFlow", "test") + + LedgerManualCategoryBottomSheetContent( + categories = categories, + onDismissRequest = {}, + ) {} +} + +@Preview(showBackground = true) +@Composable +fun LedgerManualCategoryCreateBottomSheetContentPreview() { + LedgerManualCategoryCreateBottomSheetContent( + textFieldValue = TextFieldValue(), + isSystemCategoryError = false, + categories = emptyList(), + onValueChange = {}, + onClickRegister = {} + ) {} +} \ No newline at end of file From 3c2089d2c02e70171d67c8fd3f27df5e5ae44fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EB=B3=91=ED=97=8C?= <64366488+Heonbyeong@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:03:58 +0900 Subject: [PATCH 02/12] =?UTF-8?q?[ID-46]=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=93=B1=EB=A1=9D=20API=20=EC=97=B0=EB=8F=99=20(#6?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: create category api interface * feat: create category data source * feat: create category repository * feat: create category usecase * feat: call create category api * feat: create category ui handle * refactor: get agency id from state --- .../model/agency/CategoryCreateRequest.kt | 6 ++++ .../model/agency/CategoryCreateResponse.kt | 6 ++++ .../model/agency/CategoryReadResponse.kt | 9 ++++++ .../moneymong/network/api/AgencyApi.kt | 7 +++++ .../agency/AgencyRemoteDataSource.kt | 3 ++ .../agency/AgencyRemoteDataSourceImpl.kt | 6 ++++ .../agency/AgencyRemoteDataSourceMock.kt | 6 ++++ .../repository/agency/AgencyRepositoryImpl.kt | 5 ++++ .../repository/agency/AgencyRepository.kt | 3 ++ .../usecase/agency/CreateCategoryUseCase.kt | 13 +++++++++ .../ledgermanual/LedgerManualScreen.kt | 6 ++-- .../ledgermanual/LedgerManualState.kt | 2 ++ .../ledgermanual/LedgerManualViewModel.kt | 28 ++++++++++++++++++- .../view/LedgerManualCategoryBottomSheet.kt | 18 ++++++------ 14 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryCreateRequest.kt create mode 100644 core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryCreateResponse.kt create mode 100644 core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt create mode 100644 domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/CreateCategoryUseCase.kt diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryCreateRequest.kt b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryCreateRequest.kt new file mode 100644 index 00000000..c7c1d059 --- /dev/null +++ b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryCreateRequest.kt @@ -0,0 +1,6 @@ +package com.moneymong.moneymong.model.agency + +data class CategoryCreateRequest( + val agencyId: Long, + val name: String, +) diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryCreateResponse.kt b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryCreateResponse.kt new file mode 100644 index 00000000..cbf5d8bc --- /dev/null +++ b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryCreateResponse.kt @@ -0,0 +1,6 @@ +package com.moneymong.moneymong.model.agency + +data class CategoryCreateResponse( + val agencyId: Long, + val name: String, +) diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt new file mode 100644 index 00000000..a54fa491 --- /dev/null +++ b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt @@ -0,0 +1,9 @@ +package com.moneymong.moneymong.model.agency + +data class CategoryReadResponse( + val categories: List, +) + +data class CategoryResponse( + val name: String, +) diff --git a/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt b/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt index bda1058b..7c666c20 100644 --- a/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt +++ b/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt @@ -5,6 +5,8 @@ import com.moneymong.moneymong.model.agency.AgencyGetResponse import com.moneymong.moneymong.model.agency.AgencyJoinRequest import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest +import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryCreateResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import com.moneymong.moneymong.model.member.InvitationCodeResponse @@ -49,6 +51,11 @@ interface AgencyApi { @Body request: AgencyRegisterRequest ): Result + @POST("api/v1/agencies/categories") + suspend fun createCategory( + @Body request: CategoryCreateRequest + ): Result + // PATCH @PATCH("api/v1/agencies/{agencyId}/invitation-code") suspend fun reInvitationCode( diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt index 3bd7fe51..23637a70 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt @@ -5,6 +5,8 @@ import com.moneymong.moneymong.model.agency.AgencyGetResponse import com.moneymong.moneymong.model.agency.AgencyJoinRequest import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest +import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryCreateResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse @@ -14,4 +16,5 @@ interface AgencyRemoteDataSource { suspend fun fetchMyAgencyList(): Result> suspend fun fetchAgencyByName(agencyName: String): Result> suspend fun agencyCodeNumbers(data: AgencyJoinRequest): Result + suspend fun createCategory(request: CategoryCreateRequest): Result } \ No newline at end of file diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt index 2495a6d4..8af2e0e7 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt @@ -5,6 +5,8 @@ import com.moneymong.moneymong.model.agency.AgencyGetResponse import com.moneymong.moneymong.model.agency.AgencyJoinRequest import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest +import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryCreateResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import com.moneymong.moneymong.network.api.AgencyApi @@ -35,4 +37,8 @@ class AgencyRemoteDataSourceImpl @Inject constructor( ): Result { return agencyApi.agencyCodeNumbers(body = data) } + + override suspend fun createCategory(request: CategoryCreateRequest): Result { + return agencyApi.createCategory(request = request) + } } \ No newline at end of file diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt index 2aceeedd..9ef13a1d 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt @@ -5,6 +5,8 @@ import com.moneymong.moneymong.model.agency.AgencyGetResponse import com.moneymong.moneymong.model.agency.AgencyJoinRequest import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest +import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryCreateResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import kotlinx.coroutines.delay @@ -36,6 +38,10 @@ class AgencyRemoteDataSourceMock : AgencyRemoteDataSource { ) } + override suspend fun createCategory(request: CategoryCreateRequest): Result { + return Result.success(CategoryCreateResponse(agencyId = 1L, name = "category")) + } + private companion object { val agenciesMockOfSuccess = listOf( Result.success( diff --git a/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt b/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt index 459a807c..4c15c851 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt @@ -11,6 +11,8 @@ import com.moneymong.moneymong.model.agency.AgencyGetResponse import com.moneymong.moneymong.model.agency.AgencyJoinRequest import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest +import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryCreateResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import kotlinx.coroutines.flow.Flow @@ -49,4 +51,7 @@ class AgencyRepositoryImpl @Inject constructor( override suspend fun fetchAgencyId(): Int = agencyLocalDataSource.fetchAgencyId() + + override suspend fun createCategory(request: CategoryCreateRequest): Result = + agencyRemoteDataSource.createCategory(request = request) } \ No newline at end of file diff --git a/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt b/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt index 94762027..f7d686c5 100644 --- a/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt +++ b/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt @@ -5,6 +5,8 @@ import com.moneymong.moneymong.model.agency.AgencyGetResponse import com.moneymong.moneymong.model.agency.AgencyJoinRequest import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest +import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryCreateResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import kotlinx.coroutines.flow.Flow @@ -18,4 +20,5 @@ interface AgencyRepository { suspend fun saveAgencyId(agencyId: Int) suspend fun fetchAgencyId(): Int + suspend fun createCategory(request: CategoryCreateRequest): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/CreateCategoryUseCase.kt b/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/CreateCategoryUseCase.kt new file mode 100644 index 00000000..21594d71 --- /dev/null +++ b/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/CreateCategoryUseCase.kt @@ -0,0 +1,13 @@ +package com.moneymong.moneymong.domain.usecase.agency + +import com.moneymong.moneymong.domain.repository.agency.AgencyRepository +import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import javax.inject.Inject + +class CreateCategoryUseCase @Inject constructor( + private val agencyRepository: AgencyRepository, +) { + suspend operator fun invoke(request: CategoryCreateRequest): Result = + agencyRepository.createCategory(request = request) +} \ No newline at end of file diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt index 288df93f..7a8d7a31 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt @@ -59,7 +59,6 @@ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.moneymong.moneymong.android.util.base64ToFile import com.moneymong.moneymong.android.util.encodingBase64 -import com.moneymong.moneymong.design_system.R import com.moneymong.moneymong.ui.noRippleClickable import com.moneymong.moneymong.design_system.R.drawable import com.moneymong.moneymong.design_system.component.button.MDSButton @@ -186,7 +185,7 @@ fun LedgerManualScreen( if (state.showBottomSheet) { LedgerManualCategoryBottomSheet( sheetState = sheetState, - categories = emptyList(), + categories = state.categories, categoryValue = state.categoryValue, isSystemCategoryError = state.isSystemCategoryError, onDismissRequest = { @@ -194,7 +193,8 @@ fun LedgerManualScreen( sheetState.hide() }.invokeOnCompletion { viewModel.onDismissBottomSheet() } }, - onChangeCategoryValue = viewModel::onChangeCategoryValue + onChangeCategoryValue = viewModel::onChangeCategoryValue, + onCategoryCreate = viewModel::createCategory, ) } diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt index 1a3a2eea..24617e6f 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.text.input.TextFieldValue import com.moneymong.moneymong.android.State import com.moneymong.moneymong.common.util.toZonedDateTime import com.moneymong.moneymong.design_system.component.textfield.util.PriceType +import com.moneymong.moneymong.model.agency.CategoryResponse import com.moneymong.moneymong.model.ledger.FundType import java.text.SimpleDateFormat @@ -28,6 +29,7 @@ data class LedgerManualState( val errorMessage: String = "", val showBottomSheet: Boolean = false, val categoryValue: TextFieldValue = TextFieldValue(), + val categories: List? = null, ) : State { val enabled: Boolean diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt index 89579c21..d028d303 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt @@ -3,6 +3,8 @@ package com.moneymong.moneymong.ledgermanual import androidx.compose.ui.text.input.TextFieldValue import com.moneymong.moneymong.android.BaseViewModel import com.moneymong.moneymong.android.util.toMultipart +import com.moneymong.moneymong.common.error.MoneyMongError +import com.moneymong.moneymong.domain.usecase.agency.CreateCategoryUseCase import com.moneymong.moneymong.ui.isValidPaymentDate import com.moneymong.moneymong.ui.isValidPaymentTime import com.moneymong.moneymong.ui.validateValue @@ -10,6 +12,7 @@ import com.moneymong.moneymong.domain.usecase.agency.FetchAgencyIdUseCase import com.moneymong.moneymong.domain.usecase.ledger.PostLedgerTransactionUseCase import com.moneymong.moneymong.domain.usecase.ocr.PostFileUploadUseCase import com.moneymong.moneymong.domain.usecase.user.FetchUserNicknameUseCase +import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.ledger.FundType import com.moneymong.moneymong.model.ledger.LedgerTransactionRequest import com.moneymong.moneymong.model.ocr.FileUploadRequest @@ -27,7 +30,8 @@ class LedgerManualViewModel @Inject constructor( private val postLedgerTransactionUseCase: PostLedgerTransactionUseCase, private val postFileUploadUseCase: PostFileUploadUseCase, private val fetchAgencyIdUseCase: FetchAgencyIdUseCase, - private val fetchUserNicknameUseCase: FetchUserNicknameUseCase + private val fetchUserNicknameUseCase: FetchUserNicknameUseCase, + private val createCategoryUseCase: CreateCategoryUseCase, ) : BaseViewModel(LedgerManualState()) { init { @@ -92,6 +96,28 @@ class LedgerManualViewModel @Inject constructor( } } + fun createCategory() = intent { + val request = + CategoryCreateRequest(agencyId = state.agencyId.toLong(), name = state.categoryValue.text) + + createCategoryUseCase(request) + .onSuccess { + reduce { + state.copy( + showBottomSheet = false, + categoryValue = TextFieldValue(), + ) + } + }.onFailure { + reduce { + state.copy( + showErrorDialog = true, + errorMessage = it.message ?: MoneyMongError.UnExpectedError.message + ) + } + } + } + fun onChangeStoreNameValue(value: TextFieldValue) = blockingIntent { val validate = value.text.validateValue(length = 20) if (!validate) { diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt index ffd36754..d473d583 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt @@ -55,6 +55,7 @@ import com.moneymong.moneymong.design_system.theme.Gray07 import com.moneymong.moneymong.design_system.theme.Heading1 import com.moneymong.moneymong.design_system.theme.Heading4 import com.moneymong.moneymong.design_system.theme.White +import com.moneymong.moneymong.model.agency.CategoryResponse import com.moneymong.moneymong.ui.noRippleClickable @OptIn(ExperimentalMaterial3Api::class) @@ -62,11 +63,12 @@ import com.moneymong.moneymong.ui.noRippleClickable fun LedgerManualCategoryBottomSheet( modifier: Modifier = Modifier, sheetState: SheetState, - categories: List, // TODO API response + categories: List?, categoryValue: TextFieldValue, isSystemCategoryError: Boolean, onDismissRequest: () -> Unit, onChangeCategoryValue: (TextFieldValue) -> Unit, + onCategoryCreate: () -> Unit, ) { var sheetType by remember { mutableStateOf(LedgerManualBottomSheetType.LIST) } @@ -106,7 +108,7 @@ fun LedgerManualCategoryBottomSheet( isSystemCategoryError = isSystemCategoryError, categories = categories, onValueChange = onChangeCategoryValue, - onClickRegister = {}, + onClickRegister = onCategoryCreate, onPrev = { sheetType = LedgerManualBottomSheetType.LIST } ) } @@ -119,7 +121,7 @@ fun LedgerManualCategoryBottomSheet( @Composable fun LedgerManualCategoryBottomSheetContent( modifier: Modifier = Modifier, - categories: List, + categories: List?, onDismissRequest: () -> Unit, onClickCreate: () -> Unit, ) { @@ -165,9 +167,9 @@ fun LedgerManualCategoryBottomSheetContent( FlowRow( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - categories.forEach { + categories?.forEach { MDSOutlineTag( - text = it, + text = it.name, iconResource = R.drawable.ic_close_default, onClick = {}, ) @@ -181,7 +183,7 @@ fun LedgerManualCategoryCreateBottomSheetContent( modifier: Modifier = Modifier, textFieldValue: TextFieldValue, isSystemCategoryError: Boolean, - categories: List, // TODO API response + categories: List?, onValueChange: (TextFieldValue) -> Unit, onClickRegister: () -> Unit, onPrev: () -> Unit, @@ -190,7 +192,7 @@ fun LedgerManualCategoryCreateBottomSheetContent( var isFilled by remember { mutableStateOf(false) } val focusRequester = remember { FocusRequester() } val keyboard = LocalSoftwareKeyboardController.current - val isExists = categories.contains(textFieldValue.text) + val isExists = categories?.any { it.name == textFieldValue.text } ?: false val helperText by remember(isSystemCategoryError, isExists) { derivedStateOf { when { @@ -277,7 +279,7 @@ fun LedgerManualCategoryCreateBottomSheetContent( @Preview(showBackground = true) @Composable fun LedgerManualCategoryBottomSheetContentPreview() { - val categories = listOf("testTooLongTextOverFlow", "test") + val categories = listOf(CategoryResponse("testTooLongTextOverFlow"), CategoryResponse("test")) LedgerManualCategoryBottomSheetContent( categories = categories, From 27e6f72ba140711cabfc685feeb652096e70e4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EB=B3=91=ED=97=8C?= <64366488+Heonbyeong@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:49:19 +0900 Subject: [PATCH 03/12] =?UTF-8?q?[ID-50]=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=A1=B0=ED=9A=8C=20API=20=EC=97=B0=EB=8F=99=20(#6?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: fetch categories api interface from network layer * feat: fetch categories api datasource * feat: fetch categories repository * refactor: path variables to query parameters * feat: fetch categories usecase * feat: fetch categories viewmodel method * feat: binding categories * feat: ledger manual category click event handle * feat: category state clean up * refactor: category click event change multiple to single --- .../design_system/component/tag/Tag.kt | 52 ++++++++++++++----- .../model/agency/CategoryReadResponse.kt | 1 + .../moneymong/network/api/AgencyApi.kt | 6 +++ .../agency/AgencyRemoteDataSource.kt | 2 + .../agency/AgencyRemoteDataSourceImpl.kt | 5 ++ .../agency/AgencyRemoteDataSourceMock.kt | 5 ++ .../repository/agency/AgencyRepositoryImpl.kt | 4 ++ .../repository/agency/AgencyRepository.kt | 2 + .../usecase/agency/FetchCategoriesUseCase.kt | 12 +++++ .../ledgermanual/LedgerManualScreen.kt | 25 +++++---- .../ledgermanual/LedgerManualState.kt | 3 +- .../ledgermanual/LedgerManualViewModel.kt | 30 ++++++++--- .../view/LedgerManualCategoryBottomSheet.kt | 11 +++- 13 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/FetchCategoriesUseCase.kt diff --git a/core/design-system/src/main/java/com/moneymong/moneymong/design_system/component/tag/Tag.kt b/core/design-system/src/main/java/com/moneymong/moneymong/design_system/component/tag/Tag.kt index 935b6e12..5929fafb 100644 --- a/core/design-system/src/main/java/com/moneymong/moneymong/design_system/component/tag/Tag.kt +++ b/core/design-system/src/main/java/com/moneymong/moneymong/design_system/component/tag/Tag.kt @@ -3,6 +3,7 @@ package com.moneymong.moneymong.design_system.component.tag import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -13,16 +14,19 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.moneymong.moneymong.design_system.R import com.moneymong.moneymong.design_system.theme.Blue04 import com.moneymong.moneymong.design_system.theme.Body2 import com.moneymong.moneymong.design_system.theme.Body3 import com.moneymong.moneymong.design_system.theme.Gray03 import com.moneymong.moneymong.design_system.theme.Gray05 import com.moneymong.moneymong.design_system.theme.Gray06 +import com.moneymong.moneymong.design_system.theme.Gray08 import com.moneymong.moneymong.design_system.theme.White import com.moneymong.moneymong.ui.noRippleClickable @@ -64,38 +68,56 @@ fun MDSTag( fun MDSOutlineTag( modifier: Modifier = Modifier, text: String, + selected: Boolean = false, @DrawableRes iconResource: Int? = null, - onClick: () -> Unit, + onClick: () -> Unit = {}, ) { Row( modifier = modifier .border( width = 1.4.dp, - color = Gray03, + color = if (selected) Blue04 else Gray03, shape = RoundedCornerShape(size = Int.MAX_VALUE.dp) ) + .clip(RoundedCornerShape(size = Int.MAX_VALUE.dp)) .background( color = White, shape = RoundedCornerShape(size = Int.MAX_VALUE.dp) ) + .clickable( + enabled = iconResource == null, + onClick = onClick + ) .padding(horizontal = 12.dp, vertical = 6.dp), horizontalArrangement = Arrangement.spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = text, - color = Gray06, + color = if (selected) Gray08 else Gray06, style = Body3, ) - if (iconResource != null) { - Icon( - modifier = Modifier - .size(18.dp) - .noRippleClickable(onClick), - painter = painterResource(id = iconResource), - contentDescription = "Tag icon", - tint = Gray05 - ) + when { + !selected && iconResource != null -> { + Icon( + modifier = Modifier + .size(18.dp) + .noRippleClickable(onClick), + painter = painterResource(id = iconResource), + contentDescription = "Tag icon", + tint = Gray05 + ) + } + + selected -> { + Icon( + modifier = Modifier + .size(18.dp), + painter = painterResource(id = R.drawable.ic_check), + contentDescription = "Tag icon", + tint = Blue04 + ) + } } } } @@ -116,7 +138,7 @@ fun MDSTagPreview() { text = "tag", backgroundColor = Blue04, contentColor = White, - iconResource = com.moneymong.moneymong.design_system.R.drawable.ic_pencil + iconResource = R.drawable.ic_pencil ) } } @@ -130,11 +152,13 @@ fun MDSOutlineTagPreview() { ) { MDSOutlineTag( text = "tag", + selected = false, onClick = {}, ) MDSOutlineTag( text = "tag", - iconResource = com.moneymong.moneymong.design_system.R.drawable.ic_close_default, + selected = true, + iconResource = R.drawable.ic_close_default, onClick = {}, ) } diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt index a54fa491..1625d09b 100644 --- a/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt +++ b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt @@ -1,6 +1,7 @@ package com.moneymong.moneymong.model.agency data class CategoryReadResponse( + val agencyId: Long, val categories: List, ) diff --git a/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt b/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt index 7c666c20..f46e9446 100644 --- a/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt +++ b/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import com.moneymong.moneymong.model.member.InvitationCodeResponse @@ -40,6 +41,11 @@ interface AgencyApi { @Query("keyword") name: String ): Result> + @GET("api/v1/agencies/categories") + suspend fun fetchCategories( + @Query("agencyId") agencyId: Long + ): Result + // POST @POST("/api/v2/agencies/invitation-code") suspend fun agencyCodeNumbers( diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt index 23637a70..b437c7d6 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse @@ -17,4 +18,5 @@ interface AgencyRemoteDataSource { suspend fun fetchAgencyByName(agencyName: String): Result> suspend fun agencyCodeNumbers(data: AgencyJoinRequest): Result suspend fun createCategory(request: CategoryCreateRequest): Result + suspend fun fetchCategories(agencyId: Long): Result } \ No newline at end of file diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt index 8af2e0e7..fc2e9e8c 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import com.moneymong.moneymong.network.api.AgencyApi @@ -41,4 +42,8 @@ class AgencyRemoteDataSourceImpl @Inject constructor( override suspend fun createCategory(request: CategoryCreateRequest): Result { return agencyApi.createCategory(request = request) } + + override suspend fun fetchCategories(agencyId: Long): Result { + return agencyApi.fetchCategories(agencyId = agencyId) + } } \ No newline at end of file diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt index 9ef13a1d..b1ebc95b 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import kotlinx.coroutines.delay @@ -42,6 +43,10 @@ class AgencyRemoteDataSourceMock : AgencyRemoteDataSource { return Result.success(CategoryCreateResponse(agencyId = 1L, name = "category")) } + override suspend fun fetchCategories(agencyId: Long): Result { + return Result.success(CategoryReadResponse(agencyId = agencyId, categories = emptyList())) + } + private companion object { val agenciesMockOfSuccess = listOf( Result.success( diff --git a/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt b/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt index 4c15c851..ffb7591d 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt @@ -13,6 +13,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import kotlinx.coroutines.flow.Flow @@ -54,4 +55,7 @@ class AgencyRepositoryImpl @Inject constructor( override suspend fun createCategory(request: CategoryCreateRequest): Result = agencyRemoteDataSource.createCategory(request = request) + + override suspend fun fetchCategories(agencyId: Long): Result = + agencyRemoteDataSource.fetchCategories(agencyId = agencyId) } \ No newline at end of file diff --git a/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt b/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt index f7d686c5..79a75d63 100644 --- a/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt +++ b/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse import kotlinx.coroutines.flow.Flow @@ -21,4 +22,5 @@ interface AgencyRepository { suspend fun saveAgencyId(agencyId: Int) suspend fun fetchAgencyId(): Int suspend fun createCategory(request: CategoryCreateRequest): Result + suspend fun fetchCategories(agencyId: Long): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/FetchCategoriesUseCase.kt b/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/FetchCategoriesUseCase.kt new file mode 100644 index 00000000..41501a56 --- /dev/null +++ b/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/FetchCategoriesUseCase.kt @@ -0,0 +1,12 @@ +package com.moneymong.moneymong.domain.usecase.agency + +import com.moneymong.moneymong.domain.repository.agency.AgencyRepository +import com.moneymong.moneymong.model.agency.CategoryReadResponse +import javax.inject.Inject + +class FetchCategoriesUseCase @Inject constructor( + private val agencyRepository: AgencyRepository +) { + suspend operator fun invoke(agencyId: Long): Result = + agencyRepository.fetchCategories(agencyId = agencyId) +} \ No newline at end of file diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt index 7a8d7a31..f18169da 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -89,7 +88,8 @@ import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -@OptIn(ExperimentalGlideComposeApi::class, ExperimentalMaterial3Api::class, +@OptIn( + ExperimentalGlideComposeApi::class, ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class ) @Composable @@ -191,7 +191,10 @@ fun LedgerManualScreen( onDismissRequest = { scope.launch { sheetState.hide() - }.invokeOnCompletion { viewModel.onDismissBottomSheet() } + }.invokeOnCompletion { + viewModel.onDismissBottomSheet() + viewModel.onChangeCategoryValue(TextFieldValue()) + } }, onChangeCategoryValue = viewModel::onChangeCategoryValue, onCategoryCreate = viewModel::createCategory, @@ -360,13 +363,17 @@ fun LedgerManualScreen( } Spacer(modifier = Modifier.height(8.dp)) FlowRow( - horizontalArrangement = Arrangement.spacedBy(10.dp) + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), ) { - MDSOutlineTag( - text = "Test", // TODO - iconResource = drawable.ic_close_default, - onClick = {}, - ) + state.categories.forEach { category -> + val isSelected = category == state.selectedCategory + MDSOutlineTag( + text = category.name, + selected = isSelected, + onClick = { viewModel.onClickCategory(category) }, + ) + } } Spacer(modifier = Modifier.height(24.dp)) Text( diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt index 24617e6f..c8001f71 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualState.kt @@ -29,7 +29,8 @@ data class LedgerManualState( val errorMessage: String = "", val showBottomSheet: Boolean = false, val categoryValue: TextFieldValue = TextFieldValue(), - val categories: List? = null, + val categories: List = emptyList(), + val selectedCategory: CategoryResponse? = null ) : State { val enabled: Boolean diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt index d028d303..a8b7ce5e 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt @@ -9,10 +9,12 @@ import com.moneymong.moneymong.ui.isValidPaymentDate import com.moneymong.moneymong.ui.isValidPaymentTime import com.moneymong.moneymong.ui.validateValue import com.moneymong.moneymong.domain.usecase.agency.FetchAgencyIdUseCase +import com.moneymong.moneymong.domain.usecase.agency.FetchCategoriesUseCase import com.moneymong.moneymong.domain.usecase.ledger.PostLedgerTransactionUseCase import com.moneymong.moneymong.domain.usecase.ocr.PostFileUploadUseCase import com.moneymong.moneymong.domain.usecase.user.FetchUserNicknameUseCase import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryResponse import com.moneymong.moneymong.model.ledger.FundType import com.moneymong.moneymong.model.ledger.LedgerTransactionRequest import com.moneymong.moneymong.model.ocr.FileUploadRequest @@ -32,10 +34,12 @@ class LedgerManualViewModel @Inject constructor( private val fetchAgencyIdUseCase: FetchAgencyIdUseCase, private val fetchUserNicknameUseCase: FetchUserNicknameUseCase, private val createCategoryUseCase: CreateCategoryUseCase, + private val fetchCategoriesUseCase: FetchCategoriesUseCase, ) : BaseViewModel(LedgerManualState()) { init { fetchUserInfo() + fetchCategories() } @OptIn(OrbitExperimental::class) @@ -98,16 +102,15 @@ class LedgerManualViewModel @Inject constructor( fun createCategory() = intent { val request = - CategoryCreateRequest(agencyId = state.agencyId.toLong(), name = state.categoryValue.text) + CategoryCreateRequest( + agencyId = state.agencyId.toLong(), + name = state.categoryValue.text + ) createCategoryUseCase(request) .onSuccess { - reduce { - state.copy( - showBottomSheet = false, - categoryValue = TextFieldValue(), - ) - } + fetchCategories() + reduce { state.copy(showBottomSheet = false) } }.onFailure { reduce { state.copy( @@ -115,6 +118,15 @@ class LedgerManualViewModel @Inject constructor( errorMessage = it.message ?: MoneyMongError.UnExpectedError.message ) } + }.also { onChangeCategoryValue(TextFieldValue()) } + } + + fun fetchCategories() = intent { + fetchCategoriesUseCase(agencyId = state.agencyId.toLong()) + .onSuccess { + reduce { + state.copy(categories = it.categories) + } } } @@ -215,6 +227,10 @@ class LedgerManualViewModel @Inject constructor( } } + fun onClickCategory(category: CategoryResponse) = intent { + reduce { state.copy(selectedCategory = category) } + } + private fun trimStartWithZero(value: TextFieldValue) = if (value.text.isNotEmpty() && value.text.all { it == '0' }) { value.copy(text = "0") diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt index d473d583..9364348c 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt @@ -16,6 +16,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material3.ExperimentalMaterial3Api @@ -109,7 +111,10 @@ fun LedgerManualCategoryBottomSheet( categories = categories, onValueChange = onChangeCategoryValue, onClickRegister = onCategoryCreate, - onPrev = { sheetType = LedgerManualBottomSheetType.LIST } + onPrev = { + sheetType = LedgerManualBottomSheetType.LIST + onChangeCategoryValue(TextFieldValue()) + } ) } } @@ -125,6 +130,7 @@ fun LedgerManualCategoryBottomSheetContent( onDismissRequest: () -> Unit, onClickCreate: () -> Unit, ) { + val scrollState = rememberScrollState() Column( modifier = modifier .fillMaxWidth() @@ -165,13 +171,14 @@ fun LedgerManualCategoryBottomSheetContent( ) Spacer(modifier = Modifier.height(16.dp)) FlowRow( + modifier = Modifier.verticalScroll(scrollState), horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { categories?.forEach { MDSOutlineTag( text = it.name, iconResource = R.drawable.ic_close_default, - onClick = {}, ) } } From c34f7ba7396e4587cc1b1fa9f4fd997a7a05eaf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EB=B3=91=ED=97=8C?= <64366488+Heonbyeong@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:49:18 +0900 Subject: [PATCH 04/12] =?UTF-8?q?[ID-52]=20=EC=9E=A5=EB=B6=80=20API=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80=20(#6?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add category when create ledger transaction * feat: add category section when fetch ledger detail * fix: staff status snapshot * refactor: bottom bar edit buttom * refactor: remove print --- .../model/ledger/LedgerTransactionRequest.kt | 3 +- .../LedgerTransactionDetailResponse.kt | 3 +- .../moneymong/ledger/LedgerScreen.kt | 11 +-- .../moneymong/ledger/LedgerSideEffect.kt | 2 +- .../ledgerdetail/LedgerDetailScreen.kt | 75 ++++++++++++------- .../ledgerdetail/LedgerDetailViewModel.kt | 2 +- .../ledgermanual/LedgerManualViewModel.kt | 1 + 7 files changed, 57 insertions(+), 40 deletions(-) diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/ledger/LedgerTransactionRequest.kt b/core/model/src/main/java/com/moneymong/moneymong/model/ledger/LedgerTransactionRequest.kt index 7436c0a6..0241b086 100644 --- a/core/model/src/main/java/com/moneymong/moneymong/model/ledger/LedgerTransactionRequest.kt +++ b/core/model/src/main/java/com/moneymong/moneymong/model/ledger/LedgerTransactionRequest.kt @@ -6,5 +6,6 @@ data class LedgerTransactionRequest( val amount: Int, val description: String, val paymentDate: String, - val documentImageUrls: List = emptyList() + val documentImageUrls: List = emptyList(), + val category: String? = null, ) diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/ledgerdetail/LedgerTransactionDetailResponse.kt b/core/model/src/main/java/com/moneymong/moneymong/model/ledgerdetail/LedgerTransactionDetailResponse.kt index 7c5ce8fa..fe9bae05 100644 --- a/core/model/src/main/java/com/moneymong/moneymong/model/ledgerdetail/LedgerTransactionDetailResponse.kt +++ b/core/model/src/main/java/com/moneymong/moneymong/model/ledgerdetail/LedgerTransactionDetailResponse.kt @@ -12,5 +12,6 @@ data class LedgerTransactionDetailResponse( val paymentDate: String, val receiptImageUrls: List, val documentImageUrls: List, - val authorName: String + val authorName: String, + val category: String?, ) \ No newline at end of file diff --git a/feature/ledger/src/main/java/com/moneymong/moneymong/ledger/LedgerScreen.kt b/feature/ledger/src/main/java/com/moneymong/moneymong/ledger/LedgerScreen.kt index 2571ac87..3836dc84 100644 --- a/feature/ledger/src/main/java/com/moneymong/moneymong/ledger/LedgerScreen.kt +++ b/feature/ledger/src/main/java/com/moneymong/moneymong/ledger/LedgerScreen.kt @@ -85,16 +85,10 @@ fun LedgerScreen( ) val analyticsTracker = LocalAnalyticsTracker.current - LaunchedEffect(Unit) { - viewModel.fetchMyAgencyList() - viewModel.fetchAgencyMemberList() - viewModel.fetchAgencyExistLedger() - } - viewModel.collectSideEffect { when (it) { is LedgerSideEffect.LedgerNavigateToLedgerDetail -> { - navigateToLedgerDetail(null, it.id, state.isStaff) + navigateToLedgerDetail(null, it.id, it.isStaff) } is LedgerSideEffect.LedgerNavigateToLedgerManual -> { @@ -236,7 +230,8 @@ fun LedgerScreen( onClickTransactionItem = { viewModel.eventEmit( LedgerSideEffect.LedgerNavigateToLedgerDetail( - it + id = it, + isStaff = state.isStaff, ) ) }, diff --git a/feature/ledger/src/main/java/com/moneymong/moneymong/ledger/LedgerSideEffect.kt b/feature/ledger/src/main/java/com/moneymong/moneymong/ledger/LedgerSideEffect.kt index 8dda7faf..a9d9e012 100644 --- a/feature/ledger/src/main/java/com/moneymong/moneymong/ledger/LedgerSideEffect.kt +++ b/feature/ledger/src/main/java/com/moneymong/moneymong/ledger/LedgerSideEffect.kt @@ -7,7 +7,7 @@ sealed class LedgerSideEffect : SideEffect { data object LedgerCloseSheet : LedgerSideEffect() data object LedgerNavigateToLedgerManual : LedgerSideEffect() data object LedgerFetchRetry : LedgerSideEffect() - data class LedgerNavigateToLedgerDetail(val id: Int): LedgerSideEffect() + data class LedgerNavigateToLedgerDetail(val id: Int, val isStaff: Boolean): LedgerSideEffect() data class LedgerSelectedAgencyChange(val agencyId: Int): LedgerSideEffect() data class LedgerVisibleSnackbar(val message: String, val withDismissAction: Boolean): LedgerSideEffect() } diff --git a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailScreen.kt b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailScreen.kt index 8153008d..3334e78a 100644 --- a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailScreen.kt +++ b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailScreen.kt @@ -53,6 +53,7 @@ import com.moneymong.moneymong.design_system.component.button.MDSButtonSize import com.moneymong.moneymong.design_system.component.button.MDSButtonType import com.moneymong.moneymong.design_system.component.indicator.LoadingScreen import com.moneymong.moneymong.design_system.component.modal.MDSModal +import com.moneymong.moneymong.design_system.component.tag.MDSOutlineTag import com.moneymong.moneymong.design_system.component.textfield.MDSTextField import com.moneymong.moneymong.design_system.component.textfield.util.MDSTextFieldIcons import com.moneymong.moneymong.design_system.component.textfield.util.withRequiredMark @@ -177,6 +178,36 @@ fun LedgerDetailScreen( onClickDelete = { viewModel.onChangeVisibleConfirmModal(true) }, onClickDone = viewModel::onClickEditButton ) + }, + bottomBar = { + if (state.isStaff) { + DisposableEffect(key1 = Unit) { + SystemBarColorController.setNavigationBarColor(color = White) + + onDispose { + SystemBarColorController.initialSystemBarColors() + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding(horizontal = MMHorizontalSpacing), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(20.dp)) + MDSButton( + modifier = Modifier.fillMaxWidth(), + text = "수정하기", + enabled = state.enabledEdit, + size = MDSButtonSize.LARGE, + type = MDSButtonType.PRIMARY, + onClick = viewModel::onClickEditButton + ) + Spacer(modifier = Modifier.height(12.dp)) + } + } } ) { Column( @@ -384,6 +415,22 @@ fun LedgerDetailScreen( .height(1.dp) .background(Gray03, shape = DottedShape(8.dp)) ) + Text( + text = "카테고리", + style = Body2, + color = Gray06 + ) + Spacer(modifier = Modifier.height(8.dp)) + state.ledgerTransactionDetail?.category?.let { + MDSOutlineTag(text = it) + } + Box( + modifier = Modifier + .padding(vertical = 20.dp) + .fillMaxWidth() + .height(1.dp) + .background(Gray03, shape = DottedShape(8.dp)) + ) Text( text = "사진 첨부 (최대12장)", style = Body2, @@ -467,34 +514,6 @@ fun LedgerDetailScreen( } } Spacer(modifier = Modifier.height(20.dp)) - if (state.isStaff) { - DisposableEffect(key1 = Unit) { - SystemBarColorController.setNavigationBarColor(color = White) - - onDispose { - SystemBarColorController.initialSystemBarColors() - } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(White) - .padding(horizontal = MMHorizontalSpacing), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(20.dp)) - MDSButton( - modifier = Modifier.fillMaxWidth(), - text = "수정하기", - enabled = state.enabledEdit, - size = MDSButtonSize.LARGE, - type = MDSButtonType.PRIMARY, - onClick = viewModel::onClickEditButton - ) - Spacer(modifier = Modifier.height(12.dp)) - } - } } if (state.isLoading) { LoadingScreen(modifier = Modifier.fillMaxSize()) diff --git a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailViewModel.kt b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailViewModel.kt index d9dc9c93..a9b30693 100644 --- a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailViewModel.kt +++ b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailViewModel.kt @@ -272,7 +272,7 @@ class LedgerDetailViewModel @Inject constructor( reduce { state.copy(isStaff = isStaff) } } - private fun showErrorDialog(message: String?) = intent { + private fun showErrorDialog(message: String?) = blockingIntent { reduce { state.copy( showErrorDialog = true, diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt index a8b7ce5e..99b91eff 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt @@ -64,6 +64,7 @@ class LedgerManualViewModel @Inject constructor( description = state.memoValue.text.ifEmpty { "내용 없음" }, paymentDate = state.postPaymentDate, documentImageUrls = state.documentList, + category = state.selectedCategory?.name, ) postLedgerTransactionUseCase(state.agencyId, ledgerTransactionRequest) .onSuccess { From a6f73a0aa1588c92e3ffa9525705dd74a45e2421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EB=B3=91=ED=97=8C?= <64366488+Heonbyeong@users.noreply.github.com> Date: Tue, 3 Feb 2026 00:12:11 +0900 Subject: [PATCH 05/12] =?UTF-8?q?[ID-53]=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=82=AD=EC=A0=9C=20API=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: category delete api interface * feat: category delete method in datasource * feat: category delete in domain layer * feat: category delete api call * refactor: update state field when category delete failure --- .../model/agency/CategoryDeleteRequest.kt | 5 +++++ .../model/agency/CategoryReadResponse.kt | 1 + .../moneymong/network/api/AgencyApi.kt | 7 +++++++ .../agency/AgencyRemoteDataSource.kt | 2 ++ .../agency/AgencyRemoteDataSourceImpl.kt | 5 +++++ .../agency/AgencyRemoteDataSourceMock.kt | 5 +++++ .../repository/agency/AgencyRepositoryImpl.kt | 4 ++++ .../repository/agency/AgencyRepository.kt | 3 ++- .../usecase/agency/DeleteCategoryUseCase.kt | 12 ++++++++++++ .../ledgermanual/LedgerManualScreen.kt | 1 + .../ledgermanual/LedgerManualViewModel.kt | 18 ++++++++++++++++++ .../view/LedgerManualCategoryBottomSheet.kt | 10 ++++++++-- 12 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryDeleteRequest.kt create mode 100644 domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/DeleteCategoryUseCase.kt diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryDeleteRequest.kt b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryDeleteRequest.kt new file mode 100644 index 00000000..85bf4897 --- /dev/null +++ b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryDeleteRequest.kt @@ -0,0 +1,5 @@ +package com.moneymong.moneymong.model.agency + +data class CategoryDeleteRequest( + val categoryId: Long, +) diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt index 1625d09b..50a4a317 100644 --- a/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt +++ b/core/model/src/main/java/com/moneymong/moneymong/model/agency/CategoryReadResponse.kt @@ -6,5 +6,6 @@ data class CategoryReadResponse( ) data class CategoryResponse( + val id: Long, val name: String, ) diff --git a/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt b/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt index f46e9446..4c4dd5a9 100644 --- a/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt +++ b/core/network/src/main/java/com/moneymong/moneymong/network/api/AgencyApi.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse @@ -14,6 +15,7 @@ import com.moneymong.moneymong.model.member.InvitationCodeResponse import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.HTTP import retrofit2.http.PATCH import retrofit2.http.POST import retrofit2.http.Path @@ -73,4 +75,9 @@ interface AgencyApi { suspend fun deleteAgency( @Path("agencyId") agencyId: Int ): Result + + @HTTP(method = "DELETE", path = "api/v1/agencies/categories", hasBody = true) + suspend fun deleteCategory( + @Body request: CategoryDeleteRequest + ): Result } \ No newline at end of file diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt index b437c7d6..a7e602fb 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSource.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse @@ -19,4 +20,5 @@ interface AgencyRemoteDataSource { suspend fun agencyCodeNumbers(data: AgencyJoinRequest): Result suspend fun createCategory(request: CategoryCreateRequest): Result suspend fun fetchCategories(agencyId: Long): Result + suspend fun deleteCategory(request: CategoryDeleteRequest): Result } \ No newline at end of file diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt index fc2e9e8c..77e88ced 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceImpl.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse @@ -46,4 +47,8 @@ class AgencyRemoteDataSourceImpl @Inject constructor( override suspend fun fetchCategories(agencyId: Long): Result { return agencyApi.fetchCategories(agencyId = agencyId) } + + override suspend fun deleteCategory(request: CategoryDeleteRequest): Result { + return agencyApi.deleteCategory(request = request) + } } \ No newline at end of file diff --git a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt index b1ebc95b..bd1a6963 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/datasource/agency/AgencyRemoteDataSourceMock.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse @@ -47,6 +48,10 @@ class AgencyRemoteDataSourceMock : AgencyRemoteDataSource { return Result.success(CategoryReadResponse(agencyId = agencyId, categories = emptyList())) } + override suspend fun deleteCategory(request: CategoryDeleteRequest): Result { + return Result.success(Unit) + } + private companion object { val agenciesMockOfSuccess = listOf( Result.success( diff --git a/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt b/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt index ffb7591d..bbbef68a 100644 --- a/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt +++ b/data/src/main/java/com/moneymong/moneymong/data/repository/agency/AgencyRepositoryImpl.kt @@ -13,6 +13,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse @@ -58,4 +59,7 @@ class AgencyRepositoryImpl @Inject constructor( override suspend fun fetchCategories(agencyId: Long): Result = agencyRemoteDataSource.fetchCategories(agencyId = agencyId) + + override suspend fun deleteCategory(request: CategoryDeleteRequest): Result = + agencyRemoteDataSource.deleteCategory(request = request) } \ No newline at end of file diff --git a/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt b/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt index 79a75d63..6932cc01 100644 --- a/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt +++ b/domain/src/main/java/com/moneymong/moneymong/domain/repository/agency/AgencyRepository.kt @@ -7,6 +7,7 @@ import com.moneymong.moneymong.model.agency.AgencyJoinResponse import com.moneymong.moneymong.model.agency.AgencyRegisterRequest import com.moneymong.moneymong.model.agency.CategoryCreateRequest import com.moneymong.moneymong.model.agency.CategoryCreateResponse +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest import com.moneymong.moneymong.model.agency.CategoryReadResponse import com.moneymong.moneymong.model.agency.MyAgencyResponse import com.moneymong.moneymong.model.agency.RegisterAgencyResponse @@ -18,9 +19,9 @@ interface AgencyRepository { suspend fun fetchMyAgencyList(): Result> suspend fun fetchAgencyByName(agencyName: String): Result> suspend fun agencyCodeNumbers(data: AgencyJoinRequest): Result - suspend fun saveAgencyId(agencyId: Int) suspend fun fetchAgencyId(): Int suspend fun createCategory(request: CategoryCreateRequest): Result suspend fun fetchCategories(agencyId: Long): Result + suspend fun deleteCategory(request: CategoryDeleteRequest): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/DeleteCategoryUseCase.kt b/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/DeleteCategoryUseCase.kt new file mode 100644 index 00000000..40594360 --- /dev/null +++ b/domain/src/main/java/com/moneymong/moneymong/domain/usecase/agency/DeleteCategoryUseCase.kt @@ -0,0 +1,12 @@ +package com.moneymong.moneymong.domain.usecase.agency + +import com.moneymong.moneymong.domain.repository.agency.AgencyRepository +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest +import javax.inject.Inject + +class DeleteCategoryUseCase @Inject constructor( + private val agencyRepository: AgencyRepository, +){ + suspend operator fun invoke(request: CategoryDeleteRequest): Result = + agencyRepository.deleteCategory(request = request) +} \ No newline at end of file diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt index f18169da..2cd743ff 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt @@ -198,6 +198,7 @@ fun LedgerManualScreen( }, onChangeCategoryValue = viewModel::onChangeCategoryValue, onCategoryCreate = viewModel::createCategory, + onCategoryDelete = viewModel::deleteCategory, ) } diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt index 99b91eff..65104842 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt @@ -5,6 +5,7 @@ import com.moneymong.moneymong.android.BaseViewModel import com.moneymong.moneymong.android.util.toMultipart import com.moneymong.moneymong.common.error.MoneyMongError import com.moneymong.moneymong.domain.usecase.agency.CreateCategoryUseCase +import com.moneymong.moneymong.domain.usecase.agency.DeleteCategoryUseCase import com.moneymong.moneymong.ui.isValidPaymentDate import com.moneymong.moneymong.ui.isValidPaymentTime import com.moneymong.moneymong.ui.validateValue @@ -14,6 +15,7 @@ import com.moneymong.moneymong.domain.usecase.ledger.PostLedgerTransactionUseCas import com.moneymong.moneymong.domain.usecase.ocr.PostFileUploadUseCase import com.moneymong.moneymong.domain.usecase.user.FetchUserNicknameUseCase import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest import com.moneymong.moneymong.model.agency.CategoryResponse import com.moneymong.moneymong.model.ledger.FundType import com.moneymong.moneymong.model.ledger.LedgerTransactionRequest @@ -35,6 +37,7 @@ class LedgerManualViewModel @Inject constructor( private val fetchUserNicknameUseCase: FetchUserNicknameUseCase, private val createCategoryUseCase: CreateCategoryUseCase, private val fetchCategoriesUseCase: FetchCategoriesUseCase, + private val deleteCategoryUseCase: DeleteCategoryUseCase, ) : BaseViewModel(LedgerManualState()) { init { @@ -131,6 +134,21 @@ class LedgerManualViewModel @Inject constructor( } } + fun deleteCategory(category: CategoryResponse) = intent { + val request = CategoryDeleteRequest(categoryId = category.id) + + deleteCategoryUseCase(request = request) + .onSuccess { fetchCategories() } + .onFailure { + reduce { + state.copy( + showErrorDialog = true, + errorMessage = it.message ?: MoneyMongError.UnExpectedError.message + ) + } + } + } + fun onChangeStoreNameValue(value: TextFieldValue) = blockingIntent { val validate = value.text.validateValue(length = 20) if (!validate) { diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt index 9364348c..83c9742f 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/view/LedgerManualCategoryBottomSheet.kt @@ -71,6 +71,7 @@ fun LedgerManualCategoryBottomSheet( onDismissRequest: () -> Unit, onChangeCategoryValue: (TextFieldValue) -> Unit, onCategoryCreate: () -> Unit, + onCategoryDelete: (CategoryResponse) -> Unit, ) { var sheetType by remember { mutableStateOf(LedgerManualBottomSheetType.LIST) } @@ -100,7 +101,8 @@ fun LedgerManualCategoryBottomSheet( LedgerManualCategoryBottomSheetContent( categories = categories, onDismissRequest = onDismissRequest, - onClickCreate = { sheetType = LedgerManualBottomSheetType.CREATE } + onClickCreate = { sheetType = LedgerManualBottomSheetType.CREATE }, + onClickDelete = onCategoryDelete, ) } @@ -129,6 +131,7 @@ fun LedgerManualCategoryBottomSheetContent( categories: List?, onDismissRequest: () -> Unit, onClickCreate: () -> Unit, + onClickDelete: (CategoryResponse) -> Unit, ) { val scrollState = rememberScrollState() Column( @@ -179,6 +182,7 @@ fun LedgerManualCategoryBottomSheetContent( MDSOutlineTag( text = it.name, iconResource = R.drawable.ic_close_default, + onClick = { onClickDelete(it) } ) } } @@ -286,11 +290,13 @@ fun LedgerManualCategoryCreateBottomSheetContent( @Preview(showBackground = true) @Composable fun LedgerManualCategoryBottomSheetContentPreview() { - val categories = listOf(CategoryResponse("testTooLongTextOverFlow"), CategoryResponse("test")) + val categories = + listOf(CategoryResponse(1L, "testTooLongTextOverFlow"), CategoryResponse(1L, "test")) LedgerManualCategoryBottomSheetContent( categories = categories, onDismissRequest = {}, + onClickCreate = {}, ) {} } From 2f73ba0d8e2408046fbd300787a83e7072e0f4e9 Mon Sep 17 00:00:00 2001 From: Heon Date: Tue, 3 Feb 2026 00:32:16 +0900 Subject: [PATCH 06/12] chore: version name --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b690974d..b39b8fa2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { applicationId = "com.moneymong.moneymong" minSdk = 24 targetSdk = 35 - versionCode = 32 - versionName = "2.0.2" + versionCode = 31 + versionName = "2.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { From 5365b9ee1d16f5e76edfbee90858697202df6cf7 Mon Sep 17 00:00:00 2001 From: Heon Date: Tue, 3 Feb 2026 23:56:44 +0900 Subject: [PATCH 07/12] refactor: ci workflow --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b39b8fa2..df72720b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { applicationId = "com.moneymong.moneymong" minSdk = 24 targetSdk = 35 - versionCode = 31 + versionCode = 32 versionName = "2.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From f83bceb6dcc5943bb26e65b187a17da98f0ee6e0 Mon Sep 17 00:00:00 2001 From: Heon Date: Tue, 3 Feb 2026 23:59:35 +0900 Subject: [PATCH 08/12] chore: version code --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df72720b..329b3975 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { applicationId = "com.moneymong.moneymong" minSdk = 24 targetSdk = 35 - versionCode = 32 + versionCode = 33 versionName = "2.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 8b33757c59fdb7633fc3b359fe5a86b200191e01 Mon Sep 17 00:00:00 2001 From: Heon Date: Wed, 4 Feb 2026 00:04:35 +0900 Subject: [PATCH 09/12] chore: version code --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 329b3975..df72720b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { applicationId = "com.moneymong.moneymong" minSdk = 24 targetSdk = 35 - versionCode = 33 + versionCode = 32 versionName = "2.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" From 921b161905eaa30b767fad96da5bbdbbebe1740a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=88=EB=B3=91=ED=97=8C?= <64366488+Heonbyeong@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:13:54 +0900 Subject: [PATCH 10/12] Fix/category (#72) * fix: add category bottomsheet in ledger detail * fix: onClick category item event * fix: category value race condition * fix: change ledger detail api v1 to v2 --- .../LedgerTransactionDetailRequest.kt | 3 +- .../moneymong/network/api/LedgerDetailApi.kt | 2 +- .../ledgerdetail/LedgerDetailScreen.kt | 81 ++++- .../ledgerdetail/LedgerDetailState.kt | 18 +- .../ledgerdetail/LedgerDetailViewModel.kt | 81 ++++- .../view/LedgerDetailCategoryBottomSheet.kt | 292 ++++++++++++++++++ .../ledgermanual/LedgerManualScreen.kt | 1 - .../ledgermanual/LedgerManualViewModel.kt | 16 +- 8 files changed, 472 insertions(+), 22 deletions(-) create mode 100644 feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/view/LedgerDetailCategoryBottomSheet.kt diff --git a/core/model/src/main/java/com/moneymong/moneymong/model/ledgerdetail/LedgerTransactionDetailRequest.kt b/core/model/src/main/java/com/moneymong/moneymong/model/ledgerdetail/LedgerTransactionDetailRequest.kt index 2e729457..ee27c20e 100644 --- a/core/model/src/main/java/com/moneymong/moneymong/model/ledgerdetail/LedgerTransactionDetailRequest.kt +++ b/core/model/src/main/java/com/moneymong/moneymong/model/ledgerdetail/LedgerTransactionDetailRequest.kt @@ -4,5 +4,6 @@ data class LedgerTransactionDetailRequest( val storeInfo: String, val amount: Int, val description: String, - val paymentDate: String + val paymentDate: String, + val category: String? = null, ) \ No newline at end of file diff --git a/core/network/src/main/java/com/moneymong/moneymong/network/api/LedgerDetailApi.kt b/core/network/src/main/java/com/moneymong/moneymong/network/api/LedgerDetailApi.kt index 5b785738..3a70ac6c 100644 --- a/core/network/src/main/java/com/moneymong/moneymong/network/api/LedgerDetailApi.kt +++ b/core/network/src/main/java/com/moneymong/moneymong/network/api/LedgerDetailApi.kt @@ -33,7 +33,7 @@ interface LedgerDetailApi { ): Result // PUT - @PUT("api/v1/ledger/ledger-detail/{detailId}") + @PUT("api/v2/ledger/ledger-detail/{detailId}") suspend fun updateLedgerTransactionDetail( @Path("detailId") detailId: Int, @Body body: LedgerTransactionDetailRequest diff --git a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailScreen.kt b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailScreen.kt index 3334e78a..8cfa8688 100644 --- a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailScreen.kt +++ b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailScreen.kt @@ -9,6 +9,9 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -26,9 +29,12 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -75,10 +81,12 @@ import com.moneymong.moneymong.design_system.theme.White import com.moneymong.moneymong.ledgerdetail.view.LedgerDetailTopbarView import com.moneymong.moneymong.ui.DottedShape import com.moneymong.moneymong.ui.noRippleClickable +import com.moneymong.moneymong.ledgerdetail.view.LedgerDetailCategoryBottomSheet +import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect -@OptIn(ExperimentalGlideComposeApi::class) +@OptIn(ExperimentalGlideComposeApi::class, ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun LedgerDetailScreen( modifier: Modifier = Modifier, @@ -90,6 +98,8 @@ fun LedgerDetailScreen( val state = viewModel.collectAsState().value val verticalScrollState = rememberScrollState() val focusManager = LocalFocusManager.current + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scope = rememberCoroutineScope() val singlePhotoPickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), @@ -157,6 +167,25 @@ fun LedgerDetailScreen( } } + if (state.showBottomSheet) { + LedgerDetailCategoryBottomSheet( + sheetState = sheetState, + categories = state.categories, + categoryValue = state.categoryValue, + isSystemCategoryError = state.isSystemCategoryError, + onDismissRequest = { + scope.launch { + sheetState.hide() + }.invokeOnCompletion { + viewModel.onDismissBottomSheet() + } + }, + onChangeCategoryValue = viewModel::onChangeCategoryValue, + onCategoryCreate = viewModel::createCategory, + onCategoryDelete = viewModel::deleteCategory, + ) + } + if (state.showConfirmModal) { MDSModal( icon = R.drawable.ic_warning_filled, @@ -415,14 +444,48 @@ fun LedgerDetailScreen( .height(1.dp) .background(Gray03, shape = DottedShape(8.dp)) ) - Text( - text = "카테고리", - style = Body2, - color = Gray06 - ) - Spacer(modifier = Modifier.height(8.dp)) - state.ledgerTransactionDetail?.category?.let { - MDSOutlineTag(text = it) + if (state.useEditMode) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = "카테고리", + style = Body2, + color = Gray06, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + modifier = Modifier.noRippleClickable(viewModel::onClickCategoryEdit), + text = "수정", + style = Body2, + color = Blue04, + ) + } + Spacer(modifier = Modifier.height(8.dp)) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + state.categories.forEach { category -> + val isSelected = category == state.selectedCategory + MDSOutlineTag( + text = category.name, + selected = isSelected, + onClick = { viewModel.onClickCategory(category) }, + ) + } + } + } else { + Text( + text = "카테고리", + style = Body2, + color = Gray06 + ) + Spacer(modifier = Modifier.height(8.dp)) + state.ledgerTransactionDetail?.category?.let { + MDSOutlineTag(text = it) + } } Box( modifier = Modifier diff --git a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailState.kt b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailState.kt index fb6dccf3..dae2141a 100644 --- a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailState.kt +++ b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailState.kt @@ -5,6 +5,7 @@ import com.moneymong.moneymong.android.State import com.moneymong.moneymong.common.util.toDateFormat import com.moneymong.moneymong.ui.toWonFormat import com.moneymong.moneymong.design_system.component.textfield.util.PriceType +import com.moneymong.moneymong.model.agency.CategoryResponse import com.moneymong.moneymong.model.ledger.FundType import com.moneymong.moneymong.model.ledgerdetail.LedgerTransactionDetailResponse import java.time.LocalDateTime @@ -32,9 +33,17 @@ data class LedgerDetailState( val showConfirmModal: Boolean = false, val showErrorDialog: Boolean = false, val errorMessage: String = "", - val isStaff: Boolean = false + val isStaff: Boolean = false, + val agencyId: Int = 0, + val categories: List = emptyList(), + val selectedCategory: CategoryResponse? = null, + val showBottomSheet: Boolean = false, + val categoryValue: TextFieldValue = TextFieldValue() ) : State { + val isSystemCategoryError: Boolean + get() = categoryValue.text == SYSTEM_CATEGORY + val fundTypeText: String get() = ledgerTransactionDetail?.fundType?.let { if (it == FundType.INCOME.name) "수입" else "지출" @@ -73,4 +82,9 @@ data class LedgerDetailState( val hasPaymentDate = !isPaymentDateError && paymentDateValue.text.isNotEmpty() val hasPaymentTime = !isPaymentTimeError && paymentTimeValue.text.isNotEmpty() return hasStoreName && hasTotalPrice && hasPaymentDate && hasPaymentTime && !isMemoError - }} + } + + companion object { + private const val SYSTEM_CATEGORY = "카테고리 없음" + } +} diff --git a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailViewModel.kt b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailViewModel.kt index a9b30693..e656b2a6 100644 --- a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailViewModel.kt +++ b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/LedgerDetailViewModel.kt @@ -9,6 +9,10 @@ import com.moneymong.moneymong.android.util.toMultipart import com.moneymong.moneymong.ui.isValidPaymentDate import com.moneymong.moneymong.ui.isValidPaymentTime import com.moneymong.moneymong.ui.validateValue +import com.moneymong.moneymong.domain.usecase.agency.CreateCategoryUseCase +import com.moneymong.moneymong.domain.usecase.agency.DeleteCategoryUseCase +import com.moneymong.moneymong.domain.usecase.agency.FetchAgencyIdUseCase +import com.moneymong.moneymong.domain.usecase.agency.FetchCategoriesUseCase import com.moneymong.moneymong.domain.usecase.ledgerdetail.FetchLedgerTransactionDetailUseCase import com.moneymong.moneymong.domain.usecase.ledgerdetail.DeleteLedgerDetailUseCase import com.moneymong.moneymong.domain.usecase.ledgerdetail.DeleteLedgerDocumentTransactionUseCase @@ -16,6 +20,9 @@ import com.moneymong.moneymong.domain.usecase.ledgerdetail.PostLedgerDocumentTra import com.moneymong.moneymong.domain.usecase.ledgerdetail.UpdateLedgerTransactionDetailUseCase import com.moneymong.moneymong.domain.usecase.ocr.PostFileUploadUseCase import com.moneymong.moneymong.ledgerdetail.navigation.LedgerDetailArgs +import com.moneymong.moneymong.model.agency.CategoryCreateRequest +import com.moneymong.moneymong.model.agency.CategoryDeleteRequest +import com.moneymong.moneymong.model.agency.CategoryResponse import com.moneymong.moneymong.model.ledgerdetail.LedgerDocumentRequest import com.moneymong.moneymong.model.ledgerdetail.LedgerTransactionDetailRequest import com.moneymong.moneymong.model.ledgerdetail.LedgerTransactionDetailResponse @@ -40,11 +47,16 @@ class LedgerDetailViewModel @Inject constructor( private val postLedgerDocumentTransactionUseCase: PostLedgerDocumentTransactionUseCase, private val deleteLedgerDocumentTransactionUseCase: DeleteLedgerDocumentTransactionUseCase, private val postFileUploadUseCase: PostFileUploadUseCase, - private val deleteLedgerDetailUseCase: DeleteLedgerDetailUseCase + private val deleteLedgerDetailUseCase: DeleteLedgerDetailUseCase, + private val fetchAgencyIdUseCase: FetchAgencyIdUseCase, + private val fetchCategoriesUseCase: FetchCategoriesUseCase, + private val createCategoryUseCase: CreateCategoryUseCase, + private val deleteCategoryUseCase: DeleteCategoryUseCase, ) : BaseViewModel(LedgerDetailState()) { init { onChangeStaffStatus(isStaff = LedgerDetailArgs(savedStateHandle).isStaff) + fetchAgencyId() } fun ledgerTransactionEdit(detailId: Int) = intent { @@ -78,7 +90,8 @@ class LedgerDetailViewModel @Inject constructor( storeInfo = state.storeNameValue.text, amount = state.totalPriceValue.text.toInt(), description = state.memoValue.text, - paymentDate = state.formattedPaymentDate + paymentDate = state.formattedPaymentDate, + category = state.selectedCategory?.name, ) updateLedgerTransactionDetailUseCase(detailId, request) .onSuccess { @@ -191,7 +204,8 @@ class LedgerDetailViewModel @Inject constructor( ), memoValue = state.memoValue.copy(text = ledgerTransactionDetail.description), documentList = ledgerTransactionDetail.documentImageUrls.map { it.documentImageUrl }, - documentIdList = emptyList() + documentIdList = emptyList(), + selectedCategory = state.categories.find { it.name == ledgerTransactionDetail.category } ) } } @@ -268,6 +282,67 @@ class LedgerDetailViewModel @Inject constructor( reduce { state.copy(showErrorDialog = visible) } } + private fun fetchAgencyId() = blockingIntent { + val agencyId = fetchAgencyIdUseCase() + reduce { state.copy(agencyId = agencyId) } + fetchCategories() + } + + fun fetchCategories() = intent { + fetchCategoriesUseCase(agencyId = state.agencyId.toLong()) + .onSuccess { + reduce { state.copy(categories = it.categories) } + } + } + + fun createCategory() = intent { + val request = CategoryCreateRequest( + agencyId = state.agencyId.toLong(), + name = state.categoryValue.text + ) + createCategoryUseCase(request) + .onSuccess { + fetchCategories() + reduce { state.copy(showBottomSheet = false, categoryValue = TextFieldValue()) } + }.onFailure { + showErrorDialog(it.message) + } + } + + fun deleteCategory(category: CategoryResponse) = intent { + val request = CategoryDeleteRequest(categoryId = category.id) + deleteCategoryUseCase(request = request) + .onSuccess { fetchCategories() } + .onFailure { + showErrorDialog(it.message) + } + } + + fun onClickCategory(category: CategoryResponse) = intent { + reduce { + if (state.selectedCategory == category) { + state.copy(selectedCategory = null) + } else { + state.copy(selectedCategory = category) + } + } + } + + fun onClickCategoryEdit() = intent { + reduce { state.copy(showBottomSheet = true, categoryValue = TextFieldValue()) } + } + + fun onDismissBottomSheet() = intent { + reduce { state.copy(showBottomSheet = false, categoryValue = TextFieldValue()) } + } + + fun onChangeCategoryValue(value: TextFieldValue) = blockingIntent { + val validate = value.text.validateValue(length = 10) + if (validate) { + reduce { state.copy(categoryValue = value) } + } + } + private fun onChangeStaffStatus(isStaff: Boolean) = intent { reduce { state.copy(isStaff = isStaff) } } diff --git a/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/view/LedgerDetailCategoryBottomSheet.kt b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/view/LedgerDetailCategoryBottomSheet.kt new file mode 100644 index 00000000..b0274fa0 --- /dev/null +++ b/feature/ledgerdetail/src/main/java/com/moneymong/moneymong/ledgerdetail/view/LedgerDetailCategoryBottomSheet.kt @@ -0,0 +1,292 @@ +package com.moneymong.moneymong.ledgerdetail.view + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import com.moneymong.moneymong.design_system.R +import com.moneymong.moneymong.design_system.component.bottomSheet.MDSBottomSheet +import com.moneymong.moneymong.design_system.component.button.MDSButton +import com.moneymong.moneymong.design_system.component.button.MDSButtonSize +import com.moneymong.moneymong.design_system.component.button.MDSButtonType +import com.moneymong.moneymong.design_system.component.tag.MDSOutlineTag +import com.moneymong.moneymong.design_system.component.textfield.MDSTextField +import com.moneymong.moneymong.design_system.component.textfield.util.MDSTextFieldIcons +import com.moneymong.moneymong.design_system.theme.Black +import com.moneymong.moneymong.design_system.theme.Blue04 +import com.moneymong.moneymong.design_system.theme.Body2 +import com.moneymong.moneymong.design_system.theme.Body3 +import com.moneymong.moneymong.design_system.theme.Gray05 +import com.moneymong.moneymong.design_system.theme.Gray07 +import com.moneymong.moneymong.design_system.theme.Heading1 +import com.moneymong.moneymong.design_system.theme.Heading4 +import com.moneymong.moneymong.design_system.theme.MMHorizontalSpacing +import com.moneymong.moneymong.design_system.theme.White +import com.moneymong.moneymong.model.agency.CategoryResponse +import com.moneymong.moneymong.ui.noRippleClickable + +private enum class BottomSheetType { + LIST, + CREATE, +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LedgerDetailCategoryBottomSheet( + modifier: Modifier = Modifier, + sheetState: SheetState, + categories: List?, + categoryValue: TextFieldValue, + isSystemCategoryError: Boolean, + onDismissRequest: () -> Unit, + onChangeCategoryValue: (TextFieldValue) -> Unit, + onCategoryCreate: () -> Unit, + onCategoryDelete: (CategoryResponse) -> Unit, +) { + var sheetType by remember { mutableStateOf(BottomSheetType.LIST) } + + MDSBottomSheet( + modifier = modifier, + sheetState = sheetState, + onDismissRequest = onDismissRequest, + ) { + AnimatedContent( + targetState = sheetType, + transitionSpec = { + when (targetState) { + BottomSheetType.CREATE -> { + slideInHorizontally { fullWidth -> fullWidth } + .togetherWith(slideOutHorizontally { fullWidth -> fullWidth / -3 }) + } + + BottomSheetType.LIST -> { + slideInHorizontally { fullWidth -> -fullWidth } + .togetherWith(slideOutHorizontally { fullWidth -> fullWidth / 3 }) + } + } + } + ) { targetState -> + when (targetState) { + BottomSheetType.LIST -> { + CategoryListContent( + categories = categories, + onDismissRequest = onDismissRequest, + onClickCreate = { sheetType = BottomSheetType.CREATE }, + onClickDelete = onCategoryDelete, + ) + } + + BottomSheetType.CREATE -> { + CategoryCreateContent( + textFieldValue = categoryValue, + isSystemCategoryError = isSystemCategoryError, + categories = categories, + onValueChange = onChangeCategoryValue, + onClickRegister = onCategoryCreate, + onPrev = { + sheetType = BottomSheetType.LIST + onChangeCategoryValue(TextFieldValue()) + } + ) + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun CategoryListContent( + modifier: Modifier = Modifier, + categories: List?, + onDismissRequest: () -> Unit, + onClickCreate: () -> Unit, + onClickDelete: (CategoryResponse) -> Unit, +) { + val scrollState = rememberScrollState() + Column( + modifier = modifier + .fillMaxWidth() + .height(448.dp) + .background(White) + .padding(horizontal = MMHorizontalSpacing, vertical = 20.dp), + ) { + Icon( + modifier = Modifier + .align(alignment = Alignment.End) + .noRippleClickable(onDismissRequest), + painter = painterResource(R.drawable.ic_close_default), + contentDescription = null + ) + Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + style = Heading4, + color = Black, + text = "카테고리", + ) + Text( + modifier = Modifier.noRippleClickable(onClickCreate), + style = Body3, + color = Blue04, + text = "추가", + ) + } + Spacer(modifier = Modifier.height(4.dp)) + Text( + style = Body2, + color = Gray05, + text = "원하는 카테고리를 마음대로 만들 수 있어요", + ) + Spacer(modifier = Modifier.height(16.dp)) + FlowRow( + modifier = Modifier.verticalScroll(scrollState), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + categories?.forEach { + MDSOutlineTag( + text = it.name, + iconResource = R.drawable.ic_close_default, + onClick = { onClickDelete(it) } + ) + } + } + } +} + +@Composable +private fun CategoryCreateContent( + modifier: Modifier = Modifier, + textFieldValue: TextFieldValue, + isSystemCategoryError: Boolean, + categories: List?, + onValueChange: (TextFieldValue) -> Unit, + onClickRegister: () -> Unit, + onPrev: () -> Unit, +) { + val maxCount = 10 + var isFilled by remember { mutableStateOf(false) } + val focusRequester = remember { FocusRequester() } + val keyboard = LocalSoftwareKeyboardController.current + val isExists = categories?.any { it.name == textFieldValue.text } ?: false + val helperText by remember(isSystemCategoryError, isExists) { + derivedStateOf { + when { + isSystemCategoryError -> "사용할 수 없는 카테고리 이름이에요" + isExists -> "이미 있는 카테고리에요" + else -> "" + } + } + } + + BackHandler { + onPrev() + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Column( + modifier = modifier + .fillMaxWidth() + .background(White) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(White) + .padding(horizontal = MMHorizontalSpacing, vertical = 20.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .size(24.dp) + .noRippleClickable { + keyboard?.hide() + onPrev() + }, + painter = painterResource(R.drawable.ic_chevron_left), + contentDescription = null, + tint = Gray07, + ) + Text( + text = "카테고리 생성", + style = Heading1, + color = Black, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + MDSTextField( + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { isFilled = !it.isFocused } + .focusRequester(focusRequester), + placeholder = "카테고리를 입력해주세요", + value = textFieldValue, + onValueChange = onValueChange, + title = "", + isFilled = isFilled, + isError = isSystemCategoryError || isExists, + singleLine = true, + helperText = helperText, + icon = MDSTextFieldIcons.Clear, + onIconClick = { onValueChange(TextFieldValue("")) }, + maxCount = maxCount + ) + } + val enabled = textFieldValue.text.isNotBlank() && (!isSystemCategoryError && !isExists) + MDSButton( + modifier = Modifier.fillMaxWidth(), + text = "등록", + type = MDSButtonType.PRIMARY, + size = MDSButtonSize.LARGE, + cornerShape = 0.dp, + enabled = enabled, + onClick = onClickRegister, + ) + } +} diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt index 2cd743ff..3a9af890 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualScreen.kt @@ -193,7 +193,6 @@ fun LedgerManualScreen( sheetState.hide() }.invokeOnCompletion { viewModel.onDismissBottomSheet() - viewModel.onChangeCategoryValue(TextFieldValue()) } }, onChangeCategoryValue = viewModel::onChangeCategoryValue, diff --git a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt index 65104842..01bf977f 100644 --- a/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt +++ b/feature/ledgermanual/src/main/java/com/moneymong/moneymong/ledgermanual/LedgerManualViewModel.kt @@ -114,7 +114,7 @@ class LedgerManualViewModel @Inject constructor( createCategoryUseCase(request) .onSuccess { fetchCategories() - reduce { state.copy(showBottomSheet = false) } + reduce { state.copy(showBottomSheet = false, categoryValue = TextFieldValue()) } }.onFailure { reduce { state.copy( @@ -122,7 +122,7 @@ class LedgerManualViewModel @Inject constructor( errorMessage = it.message ?: MoneyMongError.UnExpectedError.message ) } - }.also { onChangeCategoryValue(TextFieldValue()) } + } } fun fetchCategories() = intent { @@ -234,9 +234,9 @@ class LedgerManualViewModel @Inject constructor( fun onClickErrorDialogConfirm() = eventEmit(LedgerManualSideEffect.LedgerManualHideErrorDialog) - fun onClickCategoryEdit() = intent { reduce { state.copy(showBottomSheet = true) } } + fun onClickCategoryEdit() = intent { reduce { state.copy(showBottomSheet = true, categoryValue = TextFieldValue()) } } - fun onDismissBottomSheet() = intent { reduce { state.copy(showBottomSheet = false) } } + fun onDismissBottomSheet() = intent { reduce { state.copy(showBottomSheet = false, categoryValue = TextFieldValue()) } } fun onChangeCategoryValue(value: TextFieldValue) = blockingIntent { val validate = value.text.validateValue(length = 10) @@ -247,7 +247,13 @@ class LedgerManualViewModel @Inject constructor( } fun onClickCategory(category: CategoryResponse) = intent { - reduce { state.copy(selectedCategory = category) } + reduce { + if (state.selectedCategory == category) { + state.copy(selectedCategory = null) + } else { + state.copy(selectedCategory = category) + } + } } private fun trimStartWithZero(value: TextFieldValue) = From 7736a740d8e20af85f256b5b6a251add9ffd0151 Mon Sep 17 00:00:00 2001 From: Heon Date: Sun, 8 Mar 2026 17:36:16 +0900 Subject: [PATCH 11/12] chore: gradle --- settings.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index f064f25c..8af5f564 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,8 +11,8 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven(url = "https://devrepo.kakao.com/nexus/content/groups/public/") maven(url = "https://jitpack.io") - maven (url = "https://devrepo.kakao.com/nexus/content/groups/public/" ) } } From 880f18fb10c708f7ef71b9429fa86b51838ecb1b Mon Sep 17 00:00:00 2001 From: Heon Date: Sun, 8 Mar 2026 17:54:22 +0900 Subject: [PATCH 12/12] chore: version code --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df72720b..329b3975 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { applicationId = "com.moneymong.moneymong" minSdk = 24 targetSdk = 35 - versionCode = 32 + versionCode = 33 versionName = "2.1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"