diff --git a/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModRoute.kt b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModRoute.kt new file mode 100644 index 00000000..760ce9b4 --- /dev/null +++ b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModRoute.kt @@ -0,0 +1,273 @@ +package com.hankki.feature.storedetail.editbottomsheet.edit.mod + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hankki.core.common.extension.noRippleClickable +import com.hankki.core.designsystem.component.button.HankkiExpandedButton +import com.hankki.core.designsystem.component.button.HankkiMediumButton +import com.hankki.core.designsystem.component.dialog.DoubleButtonDialog +import com.hankki.core.designsystem.component.topappbar.HankkiTopBar +import com.hankki.core.designsystem.theme.Gray400 +import com.hankki.core.designsystem.theme.Gray700 +import com.hankki.core.designsystem.theme.Gray900 +import com.hankki.core.designsystem.theme.HankkiTheme +import com.hankki.core.designsystem.theme.Red400 +import com.hankki.core.designsystem.theme.Red500 +import com.hankki.core.designsystem.theme.White +import com.hankki.feature.storedetail.component.HankkiModMenuField +import com.hankki.feature.storedetail.component.HankkiModPriceField +import com.hankki.feature.storedetail.component.PriceWarningMessage +import com.hankki.feature.storedetail.component.RollbackButton +import com.hankki.feature.storedetail.editbottomsheet.edit.ModViewModel +import kotlinx.coroutines.launch + +@Composable +fun EditModRoute( + storeId: Long, + menuId: Long, + menuName: String, + price: String, + viewModel: ModViewModel = hiltViewModel(), + onNavigateUp: () -> Unit, + onNavigateToEditSuccess: (Long) -> Unit, + onNavigateToDeleteSuccess: (Long) -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val sideEffectFlow = viewModel.sideEffect + val coroutineScope = rememberCoroutineScope() + val dialogState by viewModel.dialogState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.initialize(storeId, menuId, menuName, price) + } + + LaunchedEffect(sideEffectFlow) { + sideEffectFlow.collect { sideEffect -> + when (sideEffect) { + is ModSideEffect.NavigateToEditSuccess -> onNavigateToEditSuccess(sideEffect.storeId) + is ModSideEffect.NavigateToDeleteSuccess -> onNavigateToDeleteSuccess(sideEffect.storeId) + ModSideEffect.NavigateUp -> onNavigateUp() + is ModSideEffect.MenuAddFailure -> { + // Handle error + } + } + } + } + + ModifyMenuScreen( + uiState = uiState, + menuName = menuName, + price = price, + onNavigateUp = onNavigateUp, + onMenuNameChanged = viewModel::updateMenuName, + onPriceChanged = viewModel::updatePrice, + onSubmit = { + coroutineScope.launch { + viewModel.submitMenu() + } + }, + onShowDeleteDialog = viewModel::showDeleteDialog + ) + + when (dialogState) { + ModDialogState.DELETE -> { + DoubleButtonDialog( + title = "삭제하시면 되돌릴 수 없어요\n그래도 삭제하시겠어요?", + negativeButtonTitle = "취소", + positiveButtonTitle = "삭제하기", + onNegativeButtonClicked = { + viewModel.closeDialog() + }, + onPositiveButtonClicked = { + viewModel.deleteMenu() + viewModel.closeDialog() + } + ) + } + else -> {} + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ModifyMenuScreen( + uiState: ModState, + menuName: String, + price: String, + onNavigateUp: () -> Unit, + onMenuNameChanged: (TextFieldValue) -> Unit, + onPriceChanged: (TextFieldValue) -> Unit, + onSubmit: () -> Unit, + onShowDeleteDialog: () -> Unit +) { + val focusManager = LocalFocusManager.current + val isVisibleIme = WindowInsets.isImeVisible + val isSubmitEnabled = uiState.menuNameFieldValue.text.isNotBlank() && + uiState.priceFieldValue.text.isNotBlank() && + uiState.isPriceValid + + Box( + modifier = Modifier + .fillMaxSize() + .background(White) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .navigationBarsPadding(), + ) { + HankkiTopBar( + leadingIcon = { + Icon( + painter = painterResource(id = com.hankki.core.designsystem.R.drawable.ic_arrow_left), + contentDescription = "뒤로가기", + modifier = Modifier + .offset(x = 6.dp, y = 2.dp) + .noRippleClickable(onClick = onNavigateUp), + tint = Gray700 + ) + } + ) + + Spacer(modifier = Modifier.padding(top = 18.dp)) + Row { + Text( + text = menuName, + style = HankkiTheme.typography.suitH2, + color = Red500, + modifier = Modifier.padding(start = 22.dp) + ) + Text( + text = " 메뉴를", + style = HankkiTheme.typography.suitH2, + color = Gray900 + ) + } + Text( + text = "수정할게요", + style = HankkiTheme.typography.suitH2, + color = Gray900, + modifier = Modifier.padding(start = 22.dp) + ) + + Spacer(modifier = Modifier.height(34.dp)) + HankkiModMenuField( + modifier = Modifier.fillMaxWidth(), + label = "메뉴 이름", + value = uiState.menuNameFieldValue, + onValueChange = onMenuNameChanged, + placeholder = "새로운 메뉴 이름", + isFocused = false, + onMenuFocused = { /* Focus 상태가 변경될 때 호출 */ }, + clearText = { onMenuNameChanged(TextFieldValue("")) } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + HankkiModPriceField( + modifier = Modifier.fillMaxWidth(), + label = "가격", + value = uiState.priceFieldValue, + onValueChange = onPriceChanged, + isError = !uiState.isPriceValid, + errorMessage = "가격은 8000원 이하만 제보가능해요", + isFocused = false, + onPriceFocused = { /* Focus 상태가 변경될 때 호출 */ }, + clearText = { onPriceChanged(TextFieldValue("")) } + ) + + Spacer(modifier = Modifier.weight(1f)) + + if (isVisibleIme) { + if (uiState.menuNameFieldValue.text != menuName && uiState.showRestoreMenuNameButton) { + RollbackButton( + text = "기존 메뉴이름 입력", + onClick = { onMenuNameChanged(TextFieldValue(menuName)) } + ) + Spacer(modifier = Modifier.padding(top = 16.dp)) + } + + if (uiState.priceFieldValue.text != price && uiState.showRestorePriceButton) { + RollbackButton( + text = "기존 메뉴가격 입력", + onClick = { onPriceChanged(TextFieldValue(price)) } + ) + Spacer(modifier = Modifier.padding(top = 16.dp)) + } + + if (uiState.isOverPriceLimit) { + PriceWarningMessage( + onDeleteClick = onShowDeleteDialog + ) + } + + HankkiExpandedButton( + modifier = Modifier + .fillMaxWidth() + .imePadding(), + text = "적용", + onClick = { + if (isSubmitEnabled) { + focusManager.clearFocus() + onSubmit() + } + }, + enabled = isSubmitEnabled, + textStyle = HankkiTheme.typography.sub3, + backgroundColor = if (isSubmitEnabled) Red500 else Red400 + ) + } else { + Text( + text = "모두에게 보여지는 정보이니 신중하게 수정 부탁드려요", + style = HankkiTheme.typography.suitBody3, + color = Gray400, + modifier = Modifier + .padding(start = 36.dp, end = 35.dp, bottom = 12.dp) + ) + + HankkiMediumButton( + modifier = Modifier + .padding(horizontal = 22.dp) + .fillMaxWidth() + .navigationBarsPadding() + .padding(bottom = 15.dp), + text = "수정 완료", + onClick = onSubmit, + enabled = isSubmitEnabled, + textStyle = HankkiTheme.typography.sub3, + backgroundColor = if (isSubmitEnabled) Red500 else Red400 + ) + } + } + } +} diff --git a/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModSideEffect.kt b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModSideEffect.kt new file mode 100644 index 00000000..9fcd12b3 --- /dev/null +++ b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModSideEffect.kt @@ -0,0 +1,8 @@ +package com.hankki.feature.storedetail.editbottomsheet.edit.mod + +sealed class ModSideEffect { + data object NavigateUp : ModSideEffect() + data class NavigateToEditSuccess(val storeId: Long) : ModSideEffect() + data class NavigateToDeleteSuccess(val storeId: Long) : ModSideEffect() + data class MenuAddFailure(val message: String) : ModSideEffect() +} \ No newline at end of file diff --git a/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModState.kt b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModState.kt new file mode 100644 index 00000000..fe0fd504 --- /dev/null +++ b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModState.kt @@ -0,0 +1,12 @@ +package com.hankki.feature.storedetail.editbottomsheet.edit.mod + +import androidx.compose.ui.text.input.TextFieldValue + +data class ModState( + val menuNameFieldValue: TextFieldValue = TextFieldValue(), + val priceFieldValue: TextFieldValue = TextFieldValue(), + val isPriceValid: Boolean = true, + val isOverPriceLimit: Boolean = false, + val showRestoreMenuNameButton: Boolean = false, + val showRestorePriceButton: Boolean = false +) \ No newline at end of file diff --git a/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModSucceedRoute.kt b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModSucceedRoute.kt new file mode 100644 index 00000000..776bebcf --- /dev/null +++ b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModSucceedRoute.kt @@ -0,0 +1,134 @@ +package com.hankki.feature.storedetail.editbottomsheet.edit.mod + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.hankki.core.common.extension.noRippleClickable +import com.hankki.core.designsystem.component.button.HankkiMediumButton +import com.hankki.core.designsystem.component.topappbar.HankkiTopBar +import com.hankki.core.designsystem.theme.Gray700 +import com.hankki.core.designsystem.theme.Gray850 +import com.hankki.core.designsystem.theme.HankkiTheme +import com.hankki.core.designsystem.theme.Red500 +import com.hankki.core.designsystem.theme.White +import com.hankki.feature.storedetail.R +import com.hankki.feature.storedetail.StoreDetailViewModel + +@Composable +fun ModSucceedRoute( + viewModel: StoreDetailViewModel = hiltViewModel(), + onNavigateToEditMenu: () -> Unit, + onNavigateToStoreDetailRoute: () -> Unit, + onNavigateUp: () -> Unit +) { + ModSucceedScreen( + viewModel = viewModel, + onNavigateToEditMenu = onNavigateToEditMenu, + onNavigateToStoreDetailRoute = onNavigateToStoreDetailRoute, + onNavigateUp = onNavigateUp + ) +} + +@Composable +fun ModSucceedScreen( + viewModel: StoreDetailViewModel, + onNavigateToEditMenu: () -> Unit, + onNavigateToStoreDetailRoute: () -> Unit, + onNavigateUp: () -> Unit +) { + val storeState by viewModel.storeState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.fetchNickname() + } + + Column( + modifier = Modifier + .fillMaxSize() + ) { + Spacer(modifier = Modifier.statusBarsPadding()) + HankkiTopBar( + leadingIcon = { + Icon( + painter = painterResource(com.hankki.core.designsystem.R.drawable.ic_arrow_left), + contentDescription = "뒤로가기", + modifier = Modifier + .offset(x = 6.dp, y = 2.dp) + .noRippleClickable(onClick = onNavigateUp), + tint = Gray700 + ) + } + ) + + Spacer(modifier = Modifier.height(18.dp)) + Box { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.img_edit_completed), + contentDescription = "Success", + modifier = Modifier + .fillMaxSize() + ) + + Text( + text = "${storeState.nickname}님이 말씀해주신대로\n메뉴 정보를 수정했어요!", + style = HankkiTheme.typography.suitH2, + color = Gray850, + modifier = Modifier.padding(start = 22.dp) + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + + HankkiMediumButton( + modifier = Modifier.fillMaxWidth(), + text = "다른 메뉴도 수정하기", + onClick = { + onNavigateToEditMenu() + }, + enabled = true, + backgroundColor = White, + textStyle = HankkiTheme.typography.sub3, + textColor = Red500, + borderColor = Red500 + ) + Spacer(modifier = Modifier.height(16.dp)) + HankkiMediumButton( + modifier = Modifier.fillMaxWidth(), + text = "완료", + onClick = onNavigateToStoreDetailRoute, + enabled = true, + textStyle = HankkiTheme.typography.sub3 + ) + Spacer( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = 15.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModViewModel.kt b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModViewModel.kt new file mode 100644 index 00000000..c7e4edde --- /dev/null +++ b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModViewModel.kt @@ -0,0 +1,106 @@ +package com.hankki.feature.storedetail.editbottomsheet.edit.mod + +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.hankki.domain.storedetail.entity.MenuUpdateRequestEntity +import com.hankki.domain.storedetail.repository.StoreDetailRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ModViewModel @Inject constructor( + private val storeDetailRepository: StoreDetailRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(ModState()) + val uiState: StateFlow = _uiState + + private val _dialogState = MutableStateFlow(ModDialogState.CLOSED) + val dialogState: StateFlow = _dialogState + + private val _sideEffect = MutableSharedFlow() + val sideEffect: SharedFlow = _sideEffect + + var storeId: Long = -1 + private var menuId: Long = -1 + + fun initialize(storeId: Long, menuId: Long, menuName: String, price: String) { + this.storeId = storeId + this.menuId = menuId + _uiState.value = ModState( + menuNameFieldValue = TextFieldValue(menuName, selection = TextRange(menuName.length)), + priceFieldValue = TextFieldValue(price, selection = TextRange(price.length)), + isPriceValid = price.toIntOrNull()?.let { it < 8000 } ?: false, + isOverPriceLimit = price.toIntOrNull()?.let { it >= 8000 } ?: false + ) + } + + fun updateMenuName(newValue: TextFieldValue) { + _uiState.value = _uiState.value.copy( + menuNameFieldValue = newValue, + showRestoreMenuNameButton = newValue.text != _uiState.value.menuNameFieldValue.text + ) + } + + fun updatePrice(newValue: TextFieldValue) { + val priceInt = newValue.text.toIntOrNull() + _uiState.value = _uiState.value.copy( + priceFieldValue = newValue, + isOverPriceLimit = priceInt?.let { it >= 8000 } == true, + isPriceValid = priceInt != null && priceInt < 8000, + showRestorePriceButton = newValue.text != _uiState.value.priceFieldValue.text + ) + } + + suspend fun submitMenu() { + val uiState = _uiState.value + val parsedPrice = uiState.priceFieldValue.text.toIntOrNull() + + if (parsedPrice != null && parsedPrice < 8000 && uiState.isPriceValid) { + viewModelScope.launch { + val menuUpdateRequest = MenuUpdateRequestEntity( + name = uiState.menuNameFieldValue.text, + price = parsedPrice + ) + storeDetailRepository.putUpdateMenu(storeId, menuId, menuUpdateRequest) + .onSuccess { + _sideEffect.emit(ModSideEffect.NavigateToEditSuccess(storeId)) + } + .onFailure { error -> + _sideEffect.emit(ModSideEffect.MenuAddFailure(error.message ?: "Unknown error")) + } + } + } else { + _sideEffect.emit(ModSideEffect.MenuAddFailure("Price must be below 8000")) + } + } + + fun deleteMenu() { + viewModelScope.launch { + storeDetailRepository.deleteMenuItem(storeId, menuId) + .onSuccess { + _sideEffect.emit(ModSideEffect.NavigateToDeleteSuccess(storeId)) + } + .onFailure { error -> + _sideEffect.emit(ModSideEffect.MenuAddFailure(error.message ?: "Unknown error")) + } + } + } + + fun closeDialog() { + _dialogState.value = ModDialogState.CLOSED + } + + fun showDeleteDialog() { + _dialogState.value = ModDialogState.DELETE + } + + +} diff --git a/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModlDialogState.kt b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModlDialogState.kt new file mode 100644 index 00000000..d47e94d1 --- /dev/null +++ b/feature/storedetail/src/main/java/com/hankki/feature/storedetail/editbottomsheet/edit/mod/ModlDialogState.kt @@ -0,0 +1,6 @@ +package com.hankki.feature.storedetail.editbottomsheet.edit.mod + +enum class ModDialogState { + CLOSED, + DELETE +} \ No newline at end of file