From a9d03cae6800967371a8bd48c3c1e198b6b32063 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:11:42 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EB=A9=94=EB=89=B4=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=EC=82=AD=EC=A0=9C=20API=20=EC=9E=91=EC=84=B1=20#74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuit/ourmenu/data/repository/MenuFolderRepository.kt | 6 ++++++ .../java/com/kuit/ourmenu/data/service/MenuFolderService.kt | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt index 0f9fe24..5bbd007 100644 --- a/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt +++ b/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt @@ -40,4 +40,10 @@ class MenuFolderRepository @Inject constructor( sortOrder = sortOrder ).handleBaseResponse().getOrThrow() } + + suspend fun deleteMenuFolder( + menuFolderId: Long + ) = runCatching { + menuFolderService.deleteMenuFolder(menuFolderId).handleBaseResponse().getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt index 91d8c6a..57873d3 100644 --- a/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt +++ b/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt @@ -4,6 +4,7 @@ import com.kuit.ourmenu.data.model.base.BaseResponse import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderAllResponse import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderDetailResponse import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderResponse +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Path import retrofit2.http.Query @@ -27,4 +28,9 @@ interface MenuFolderService { @Query("size") size: Int, @Query("sortOrder") sortOrder: String, ): BaseResponse> + + @DELETE("api/menu-folders/{menuFolderId}") + suspend fun deleteMenuFolder( + @Path("menuFolderId") menuFolderId: Long + ): BaseResponse } \ No newline at end of file From f15ca3f6fe9737429968e26323d133b1ca832381 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Fri, 1 Aug 2025 16:45:39 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EB=A9=94=EB=89=B4=ED=8C=90?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A9=94=EB=89=B4=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/menuFolder/screen/MenuFolderScreen.kt | 3 +++ .../viewmodel/MenuFolderViewModel.kt | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt index c8e2581..bce53a3 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt @@ -102,6 +102,9 @@ fun MenuFolderScreen( onReset = { if (swipedIndex == index) swipedIndex = -1 }, onButtonClick = { onNavigateToDetail(folder.menuFolderId) + }, + onDeleteClick = { + viewModel.deleteMenuFolder(folder.menuFolderId) } ) } diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt index 97f3428..6999117 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt @@ -52,4 +52,23 @@ class MenuFolderViewModel @Inject constructor( _isLoading.value = false } } + + fun deleteMenuFolder(menuFolderId: Int) { + viewModelScope.launch { + _isLoading.value = true + _error.value = null + + menuFolderRepository.deleteMenuFolder(menuFolderId.toLong()) + .fold( + onSuccess = { + getMenuFolders() // Refresh the list after deletion + }, + onFailure = { throwable -> + _error.value = throwable.message ?: "메뉴 폴더 삭제 중 오류가 발생했습니다." + } + ) + + _isLoading.value = false + } + } } \ No newline at end of file From be4df6edb41a5950f2e77264748b7df1b73cba3f Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:29:01 +0900 Subject: [PATCH 3/8] =?UTF-8?q?refactor:=20=EB=A9=94=EB=89=B4=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=EC=8A=A4=EC=99=80=EC=9D=B4=ED=94=84=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20#74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../menuFolder/component/MenuFolderButton.kt | 75 ++++++++++++++----- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt index afd1351..db569fd 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt @@ -1,5 +1,6 @@ package com.kuit.ourmenu.ui.menuFolder.component +import androidx.compose.animation.core.Animatable import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -19,11 +20,9 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.LaunchedEffect 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 import androidx.compose.ui.draw.clip @@ -31,9 +30,11 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.kuit.ourmenu.R @@ -44,6 +45,7 @@ import com.kuit.ourmenu.ui.theme.NeutralWhite import com.kuit.ourmenu.ui.theme.Primary500Main import com.kuit.ourmenu.ui.theme.ourMenuTypography import kotlinx.coroutines.launch +import kotlin.math.roundToInt @Composable @@ -56,31 +58,64 @@ fun MenuFolderButton( onEditClick: () -> Unit = {}, onDeleteClick: () -> Unit = {} ) { - var offsetX by remember { mutableFloatStateOf(0f) } + val density = LocalDensity.current + val offset = remember { + Animatable(initialValue = 0f) + } val scope = rememberCoroutineScope() - val maxSwipe = 128f // 최대 스와이프 범위 (삭제 + 수정 버튼 너비) + val maxSwipe = with(density) { + 128.dp.toPx() // 최대 스와이프 범위를 dp에서 px로 변환 + } + + LaunchedEffect(isSwiped) { + if (!isSwiped) { + offset.snapTo(0f) + } + } Box( modifier = Modifier .height(132.dp) .fillMaxWidth() .pointerInput(Unit) { +// detectHorizontalDragGestures( +// onDragEnd = { +// scope.launch { +// if (offsetX < -80f) { +// onSwipe() // 다른 버튼을 닫고 이 버튼만 스와이프 +// offsetX = -maxSwipe +// } else { +// offsetX = 0f +// onReset() // 스와이프가 닫히면 상태 초기화 +// } +// } +// } +// ) { change, dragAmount -> +// change.consume() +// offsetX = (offsetX + dragAmount).coerceIn(-maxSwipe, 0f) +// } detectHorizontalDragGestures( - onDragEnd = { + onHorizontalDrag = { _, dragAmount -> scope.launch { - if (offsetX < -80f) { - onSwipe() // 다른 버튼을 닫고 이 버튼만 스와이프 - offsetX = -maxSwipe - } else { - offsetX = 0f - onReset() // 스와이프가 닫히면 상태 초기화 + val newOffset = (offset.value + dragAmount) + .coerceIn(-maxSwipe, 0f) + offset.snapTo(newOffset) + } + }, + onDragEnd = { + if (offset.value < -maxSwipe / 2) { + scope.launch { + offset.animateTo(-maxSwipe) + } + onSwipe() + } else { + scope.launch { + offset.animateTo(0f) } + onReset() } } - ) { change, dragAmount -> - change.consume() - offsetX = (offsetX + dragAmount).coerceIn(-maxSwipe, 0f) - } + ) } ) { Box( @@ -94,10 +129,12 @@ fun MenuFolderButton( // 스와이프 상태일 때만 offset 적용 Box( modifier = Modifier - .offset(x = if (isSwiped) offsetX.dp else 0.dp) - .clickable(onClick = onButtonClick) +// .offset(x = if (isSwiped) offset.value.dp else 0.dp) + .offset { IntOffset(offset.value.roundToInt(), 0) } + ) { MenuFolderContent( + onClick = onButtonClick, menuFolder = menuFolder ) } @@ -161,6 +198,7 @@ fun MenuFolderDeleteButton(onDeleteClick: () -> Unit = {}) { @Composable fun MenuFolderContent( + onClick: () -> Unit = {}, menuFolder: MenuFolderList, ) { val menuCount = menuFolder.menuIds.size @@ -176,6 +214,7 @@ fun MenuFolderContent( modifier = Modifier .fillMaxSize() .clip(RoundedCornerShape(12.dp)) + .clickable(onClick = onClick) ) Box( From a89193f7f6696145078183d027ee95dd72a2e054 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:41:26 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EB=A9=94=EB=89=B4=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=EC=82=AD=EC=A0=9C=20=EB=AA=A8=EB=8B=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/DeleteMenuFolderModal.kt | 143 ++++++++++++++++++ .../ui/onboarding/screen/LandingScreen.kt | 2 +- app/src/main/res/values/strings.xml | 3 + 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/DeleteMenuFolderModal.kt diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/DeleteMenuFolderModal.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/DeleteMenuFolderModal.kt new file mode 100644 index 0000000..a2817e0 --- /dev/null +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/DeleteMenuFolderModal.kt @@ -0,0 +1,143 @@ +package com.kuit.ourmenu.ui.menuFolder.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.kuit.ourmenu.R +import com.kuit.ourmenu.ui.my.component.LogoutModal +import com.kuit.ourmenu.ui.theme.Neutral400 +import com.kuit.ourmenu.ui.theme.Neutral500 +import com.kuit.ourmenu.ui.theme.Neutral900 +import com.kuit.ourmenu.ui.theme.NeutralWhite +import com.kuit.ourmenu.ui.theme.Primary500Main +import com.kuit.ourmenu.ui.theme.ourMenuTypography + +@Composable +fun DeleteMenuFolderModal( + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + Column( + modifier = Modifier + .background(NeutralWhite, shape = RoundedCornerShape(16.dp)) + .padding(20.dp) + .width(288.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // 닫기 아이콘 + Icon( + painter = painterResource(R.drawable.ic_close_24_n400), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier + .align(Alignment.End) + .clickable { onDismiss() } + .size(24.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // 제목 + Text( + text = stringResource(R.string.delete_menu_folder_modal_title), + style = ourMenuTypography().pretendard_700_18, + color = Neutral900, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = stringResource(R.string.delete_menu_folder_modal_content), + style = ourMenuTypography().pretendard_500_14, + color = Neutral500, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // 버튼 Row + Column( + modifier = Modifier.fillMaxWidth() + ) { + + Button( + onClick = { + onConfirm() + onDismiss() + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Primary500Main, + contentColor = NeutralWhite + ), + ) { + Text( + text = stringResource(R.string.delete), + style = ourMenuTypography().pretendard_700_18, + color = NeutralWhite + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { + onDismiss() + }, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Neutral400, + contentColor = NeutralWhite + ), + ) { + Text( + text = stringResource(R.string.cancel), + style = ourMenuTypography().pretendard_700_18, + color = NeutralWhite + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun DeleteMenuFolderModalPreview() { + DeleteMenuFolderModal( + onDismiss = {}, + onConfirm = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LandingScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LandingScreen.kt index b23e2c5..8695262 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LandingScreen.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/onboarding/screen/LandingScreen.kt @@ -63,7 +63,7 @@ fun LandingRoute( val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() val context = LocalContext.current as Activity -// LaunchedEffect(Unit) { navigateToHome() } + LaunchedEffect(Unit) { navigateToHome() } LaunchedEffect(uiState.kakaoState) { Log.d("KakaoModule", uiState.kakaoState.toString()) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a70adc5..edad81b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,6 +108,9 @@ 상황 + 정말로 삭제하시겠어요? + 삭제하면 복구가 어려울 수 있어요.\n다시 한 번 확인해 주세요. + 현재 설정하신 식사 시간입니다! 식사 시간 수정하기 From 605db4e2d492a4665b4464949ca7ae64aa589371 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Fri, 1 Aug 2025 17:44:25 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EB=A9=94=EB=89=B4=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=EC=82=AD=EC=A0=9C=20=EB=AA=A8=EB=8B=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/menuFolder/screen/MenuFolderScreen.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt index bce53a3..9ba951f 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -28,6 +29,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.kuit.ourmenu.R import com.kuit.ourmenu.ui.menuFolder.component.AddButton +import com.kuit.ourmenu.ui.menuFolder.component.DeleteMenuFolderModal import com.kuit.ourmenu.ui.menuFolder.component.MenuFolderButton import com.kuit.ourmenu.ui.menuFolder.component.MenuFolderTopAppBar import com.kuit.ourmenu.ui.menuFolder.viewmodel.MenuFolderViewModel @@ -47,6 +49,8 @@ fun MenuFolderScreen( val menuFolders by viewModel.menuFolders.collectAsStateWithLifecycle() val totalMenuCount by viewModel.menuCount.collectAsStateWithLifecycle() + var showDeleteModel by remember { mutableStateOf(false) } + var deleteIndex by remember { mutableIntStateOf(-1) } Log.d("MenuFolderScreen", "menuFolders: $menuFolders") @@ -59,6 +63,20 @@ fun MenuFolderScreen( ) } ) { innerPadding -> + if (showDeleteModel) { + DeleteMenuFolderModal( + onDismiss = { + deleteIndex = -1 + showDeleteModel = false + }, + onConfirm = { + deleteIndex = -1 + viewModel.deleteMenuFolder(deleteIndex) + swipedIndex = -1 + } + ) + } + LazyColumn( modifier = Modifier .padding(innerPadding) @@ -104,7 +122,8 @@ fun MenuFolderScreen( onNavigateToDetail(folder.menuFolderId) }, onDeleteClick = { - viewModel.deleteMenuFolder(folder.menuFolderId) + showDeleteModel = true + deleteIndex = folder.menuFolderId } ) } From 810a4bf6af35cc7c805b9140b4af11a9acbc2877 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Sat, 2 Aug 2025 00:04:18 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EB=93=9C=EB=9E=98=EA=B7=B8=20?= =?UTF-8?q?=EC=95=A4=20=EB=93=9C=EB=9E=8D=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20#74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/MenuFolderIndexRequest.kt | 8 ++ .../data/repository/MenuFolderRepository.kt | 11 ++ .../ourmenu/data/service/MenuFolderService.kt | 9 ++ .../menuFolder/component/MenuFolderButton.kt | 4 +- .../ui/menuFolder/screen/MenuFolderScreen.kt | 64 ++++++++- .../viewmodel/MenuFolderViewModel.kt | 37 ++++++ .../utils/dragndrop/DragAndDropListState.kt | 121 ++++++++++++++++++ .../com/kuit/ourmenu/utils/dragndrop/Ext.kt | 32 +++++ 8 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/request/MenuFolderIndexRequest.kt create mode 100644 app/src/main/java/com/kuit/ourmenu/utils/dragndrop/DragAndDropListState.kt create mode 100644 app/src/main/java/com/kuit/ourmenu/utils/dragndrop/Ext.kt diff --git a/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/request/MenuFolderIndexRequest.kt b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/request/MenuFolderIndexRequest.kt new file mode 100644 index 0000000..e37e4f5 --- /dev/null +++ b/app/src/main/java/com/kuit/ourmenu/data/model/menuFolder/request/MenuFolderIndexRequest.kt @@ -0,0 +1,8 @@ +package com.kuit.ourmenu.data.model.menuFolder.request + +import kotlinx.serialization.Serializable + +@Serializable +data class MenuFolderIndexRequest( + val index: Int, +) \ No newline at end of file diff --git a/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt b/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt index 5bbd007..76f2536 100644 --- a/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt +++ b/app/src/main/java/com/kuit/ourmenu/data/repository/MenuFolderRepository.kt @@ -1,6 +1,7 @@ package com.kuit.ourmenu.data.repository import com.kuit.ourmenu.data.model.base.handleBaseResponse +import com.kuit.ourmenu.data.model.menuFolder.request.MenuFolderIndexRequest import com.kuit.ourmenu.data.service.MenuFolderService import javax.inject.Inject import javax.inject.Singleton @@ -46,4 +47,14 @@ class MenuFolderRepository @Inject constructor( ) = runCatching { menuFolderService.deleteMenuFolder(menuFolderId).handleBaseResponse().getOrThrow() } + + suspend fun updateMenuFolderIndex( + menuFolderId: Long, + request: MenuFolderIndexRequest + ) = runCatching { + menuFolderService.updateMenuFolderIndex( + menuFolderId = menuFolderId, + request = request + ).handleBaseResponse().getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt b/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt index 57873d3..e7d1aa1 100644 --- a/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt +++ b/app/src/main/java/com/kuit/ourmenu/data/service/MenuFolderService.kt @@ -1,11 +1,14 @@ package com.kuit.ourmenu.data.service import com.kuit.ourmenu.data.model.base.BaseResponse +import com.kuit.ourmenu.data.model.menuFolder.request.MenuFolderIndexRequest import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderAllResponse import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderDetailResponse import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderResponse +import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.PATCH import retrofit2.http.Path import retrofit2.http.Query @@ -33,4 +36,10 @@ interface MenuFolderService { suspend fun deleteMenuFolder( @Path("menuFolderId") menuFolderId: Long ): BaseResponse + + @PATCH("api/menu-folders/{menuFolderId}/index") + suspend fun updateMenuFolderIndex( + @Path("menuFolderId") menuFolderId: Long, + @Body request: MenuFolderIndexRequest + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt index db569fd..deef4d6 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/component/MenuFolderButton.kt @@ -50,6 +50,7 @@ import kotlin.math.roundToInt @Composable fun MenuFolderButton( + modifier: Modifier = Modifier, menuFolder: MenuFolderList, isSwiped: Boolean, // 현재 버튼이 스와이프된 상태인지 확인 onSwipe: () -> Unit, // 새로운 버튼이 스와이프될 때 호출 @@ -74,7 +75,7 @@ fun MenuFolderButton( } Box( - modifier = Modifier + modifier = modifier .height(132.dp) .fillMaxWidth() .pointerInput(Unit) { @@ -278,6 +279,7 @@ private fun MenuFolderButtonPreview() { ) MenuFolderButton( + modifier = Modifier, menuFolder = dummyMenuFolder, false, {}, {}) } \ No newline at end of file diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt index 9ba951f..e4a040d 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt @@ -1,15 +1,20 @@ package com.kuit.ourmenu.ui.menuFolder.screen +import android.annotation.SuppressLint import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -18,10 +23,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf 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 import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -36,9 +43,16 @@ import com.kuit.ourmenu.ui.menuFolder.viewmodel.MenuFolderViewModel import com.kuit.ourmenu.ui.theme.NeutralWhite import com.kuit.ourmenu.ui.theme.Primary500Main import com.kuit.ourmenu.ui.theme.ourMenuTypography +import com.kuit.ourmenu.utils.dragndrop.dragModifier +import com.kuit.ourmenu.utils.dragndrop.rememberDragAndDropListState +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +// https://dev.to/mardsoul/how-to-create-lazycolumn-with-drag-and-drop-elements-in-jetpack-compose-part-1-4bn5 +@SuppressLint("UnnecessaryComposedModifier") @Composable fun MenuFolderScreen( + padding: PaddingValues, onNavigateToDetail: (Int) -> Unit, onNavigateToAllMenu: () -> Unit, onNavigateToAddMenu: () -> Unit, @@ -51,8 +65,16 @@ fun MenuFolderScreen( val totalMenuCount by viewModel.menuCount.collectAsStateWithLifecycle() var showDeleteModel by remember { mutableStateOf(false) } var deleteIndex by remember { mutableIntStateOf(-1) } + var dragStartFolderId by remember { mutableIntStateOf(-1) } - Log.d("MenuFolderScreen", "menuFolders: $menuFolders") + val lazyListState = rememberLazyListState() + val dragAndDropListState = + rememberDragAndDropListState(lazyListState) { from, to -> + viewModel.updateMenuFolderList(from, to) + } + + val coroutineScope = rememberCoroutineScope() + var overscrollJob by remember { mutableStateOf(null) } Scaffold( topBar = { @@ -70,8 +92,8 @@ fun MenuFolderScreen( showDeleteModel = false }, onConfirm = { - deleteIndex = -1 viewModel.deleteMenuFolder(deleteIndex) + deleteIndex = -1 swipedIndex = -1 } ) @@ -80,7 +102,41 @@ fun MenuFolderScreen( LazyColumn( modifier = Modifier .padding(innerPadding) - .padding(horizontal = 20.dp), + .padding(horizontal = 20.dp) + .pointerInput(Unit) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragAndDropListState.onDrag(offset) + + if (overscrollJob?.isActive == true) return@detectDragGesturesAfterLongPress + dragAndDropListState + .checkOverscroll() + .takeIf { it != 0f } + ?.let { + overscrollJob = coroutineScope.launch { + dragAndDropListState.lazyListState.scrollBy(it) + } + } ?: kotlin.run { overscrollJob?.cancel() } + + }, + onDragStart = { offset -> + swipedIndex = -1 + dragAndDropListState.onDragStart(offset) + dragStartFolderId = + menuFolders[dragAndDropListState.initialIndex ?: 0].menuFolderId + }, + onDragEnd = { + viewModel.patchMenuFolders( + dragStartFolderId, + dragAndDropListState.endIndex ?: 0 + ) + dragAndDropListState.onDragInterrupted() + }, + onDragCancel = { dragAndDropListState.onDragInterrupted() } + ) + }, + state = dragAndDropListState.lazyListState, verticalArrangement = Arrangement.spacedBy(8.dp) ) { item { @@ -114,6 +170,7 @@ fun MenuFolderScreen( // TODO: 드래그 앤 드롭 구현 itemsIndexed(menuFolders) { index, folder -> MenuFolderButton( + modifier = Modifier.dragModifier(index, dragAndDropListState), menuFolder = folder, isSwiped = swipedIndex == index, onSwipe = { swipedIndex = index }, @@ -144,6 +201,7 @@ fun MenuFolderScreen( @Composable private fun MenuFolderScreenPreview() { MenuFolderScreen( + padding = PaddingValues(0.dp), onNavigateToDetail = {}, onNavigateToAllMenu = {}, onNavigateToAddMenu = {}, diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt index 6999117..2408f1c 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt @@ -1,12 +1,16 @@ package com.kuit.ourmenu.ui.menuFolder.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.kuit.ourmenu.data.model.menuFolder.request.MenuFolderIndexRequest import com.kuit.ourmenu.data.model.menuFolder.response.MenuFolderList import com.kuit.ourmenu.data.repository.MenuFolderRepository +import com.kuit.ourmenu.utils.dragndrop.move import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -71,4 +75,37 @@ class MenuFolderViewModel @Inject constructor( _isLoading.value = false } } + + fun updateMenuFolderList(from: Int, to: Int) { + val newMenuFolders = _menuFolders.value.toMutableList() + newMenuFolders.move(from, to) + _menuFolders.update { newMenuFolders } + } + + fun patchMenuFolders(fromId: Int, to: Int) { + + val toIndex = to.coerceAtMost(_menuFolders.value.size - 1) + + val index = _menuFolders.value[toIndex].index + + viewModelScope.launch { + _isLoading.value = true + _error.value = null + + menuFolderRepository.updateMenuFolderIndex( + fromId.toLong(), + MenuFolderIndexRequest(toIndex) + ) + .fold( + onSuccess = { + getMenuFolders() // Refresh the list after patching + }, + onFailure = { throwable -> + _error.value = throwable.message ?: "메뉴 폴더 순서 변경 중 오류가 발생했습니다." + } + ) + + _isLoading.value = false + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/DragAndDropListState.kt b/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/DragAndDropListState.kt new file mode 100644 index 0000000..b7a055d --- /dev/null +++ b/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/DragAndDropListState.kt @@ -0,0 +1,121 @@ +package com.kuit.ourmenu.utils.dragndrop + +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset + +@Composable +fun rememberDragAndDropListState( + lazyListState: LazyListState, + onMove: (Int, Int) -> Unit +): DragAndDropListState { + return remember { DragAndDropListState(lazyListState, onMove) } +} + +class DragAndDropListState( + val lazyListState: LazyListState, + private val onMove: (Int, Int) -> Unit +) { + private var draggingDistance by mutableFloatStateOf(0f) + private var initialDraggingElement by mutableStateOf(null) + + val initialIndex: Int? + get() = initialDraggingElement?.index?.takeIf { it > 0 }?.minus(1) ?: -1 + val endIndex: Int? + get() = currentIndexOfDraggedItem?.takeIf { it >= 0 } + var currentIndexOfDraggedItem by mutableStateOf(null) + private val initialOffsets: Pair? + get() = initialDraggingElement?.let { Pair(it.offset, it.offsetEnd) } + val elementDisplacement: Float? + get() = currentIndexOfDraggedItem?.let { + lazyListState.getVisibleItemInfo(it + 1) + }?.let { itemInfo -> + (initialDraggingElement?.offset ?: 0f).toFloat() + draggingDistance - itemInfo.offset + } + private val currentElement: LazyListItemInfo? + get() = currentIndexOfDraggedItem?.let { + lazyListState.getVisibleItemInfo(it + 1) + } + + fun onDragStart(offset: Offset) { + lazyListState.layoutInfo.visibleItemsInfo + .firstOrNull { item -> offset.y.toInt() in item.offset..item.offsetEnd } + ?.also { + if (it.index > 0) { + initialDraggingElement = it + currentIndexOfDraggedItem = it.index - 1 + } + } + } + + fun onDragInterrupted() { + initialDraggingElement = null + currentIndexOfDraggedItem = null + draggingDistance = 0f + } + + fun onDrag(offset: Offset) { + draggingDistance += offset.y + + initialOffsets?.let { (top, bottom) -> + val startOffset = top.toFloat() + draggingDistance + val endOffset = bottom.toFloat() + draggingDistance + + currentElement?.let { current -> + lazyListState.layoutInfo.visibleItemsInfo + .filterNot { item -> + item.offsetEnd < startOffset || item.offset > endOffset || current.index == item.index + } + .firstOrNull { item -> + val delta = startOffset - current.offset + when { + delta < 0 -> item.offset > startOffset + else -> item.offsetEnd < endOffset + } + } + }?.also { item -> + currentIndexOfDraggedItem?.let { current -> + if (item.index > 0) { + onMove.invoke(current, item.index - 1) + } + } + if (item.index > 0) { + currentIndexOfDraggedItem = item.index - 1 + } + } + } + } + + fun checkOverscroll(): Float { + return initialDraggingElement?.let { + val startOffset = it.offset + draggingDistance + val endOffset = it.offsetEnd + draggingDistance + + return@let when { + draggingDistance > 0 -> { + (endOffset - lazyListState.layoutInfo.viewportEndOffset).takeIf { diff -> diff > 0 } + + } + + draggingDistance < 0 -> { + (startOffset - lazyListState.layoutInfo.viewportStartOffset).takeIf { diff -> diff < 0 } + } + + else -> null + } + } ?: 0f + } + + private fun LazyListState.getVisibleItemInfo(itemPosition: Int): LazyListItemInfo? { + return this.layoutInfo.visibleItemsInfo.getOrNull(itemPosition - this.firstVisibleItemIndex) + } + + private val LazyListItemInfo.offsetEnd: Int + get() = this.offset + this.size +} \ No newline at end of file diff --git a/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/Ext.kt b/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/Ext.kt new file mode 100644 index 0000000..f830e58 --- /dev/null +++ b/app/src/main/java/com/kuit/ourmenu/utils/dragndrop/Ext.kt @@ -0,0 +1,32 @@ +package com.kuit.ourmenu.utils.dragndrop + +import android.util.Log +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.zIndex + +fun MutableList.move(from: Int, to: Int) { + if (from == to) return + if (to > this.size - 1) return + Log.d("DragAndDrop", "Moving item from $from to $to , ${this.size}") + val element = this.removeAt(from) + try { + this.add(to, element) + } catch (e: IndexOutOfBoundsException) { + this.add(element) + } +} + +fun Modifier.dragModifier(index: Int, dragAndDropListState: DragAndDropListState) = composed { + val isDragging = index == dragAndDropListState.currentIndexOfDraggedItem + val offsetOrNull = dragAndDropListState.elementDisplacement.takeIf { isDragging } + + this + .zIndex(if (isDragging) 1f else 0f) + .graphicsLayer { + translationY = offsetOrNull ?: 0f + scaleX = if (isDragging) 1.03f else 1f + scaleY = if (isDragging) 1.03f else 1f + } +} \ No newline at end of file From c1ecc31917e7993d2b5d3511c8aee0315b440242 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Sat, 2 Aug 2025 00:48:51 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EC=8A=A4=ED=81=AC=EB=A6=B0?= =?UTF-8?q?=EC=97=90=20=ED=8C=A8=EB=94=A9=20=EC=B6=94=EA=B0=80=20#74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ourmenu/ui/menuFolder/navigation/MenuFolderNavigation.kt | 4 ++++ .../main/java/com/kuit/ourmenu/ui/navigator/MainNavHost.kt | 1 + 2 files changed, 5 insertions(+) diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/navigation/MenuFolderNavigation.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/navigation/MenuFolderNavigation.kt index e014e3a..15819a0 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/navigation/MenuFolderNavigation.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/navigation/MenuFolderNavigation.kt @@ -1,5 +1,7 @@ package com.kuit.ourmenu.ui.menuFolder.navigation +import android.R.attr.padding +import androidx.compose.foundation.layout.PaddingValues import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions @@ -33,6 +35,7 @@ fun NavController.navigateToMenuInfo(menuId: Int) { } fun NavGraphBuilder.menuFolderNavGraph( + padding: PaddingValues, navigateBack: () -> Unit, navigateToMenuFolderDetail: (Int) -> Unit, navigateToMenuFolderAllMenu: () -> Unit, @@ -41,6 +44,7 @@ fun NavGraphBuilder.menuFolderNavGraph( ) { composable { MenuFolderScreen( + padding = padding, onNavigateToDetail = navigateToMenuFolderDetail, onNavigateToAllMenu = navigateToMenuFolderAllMenu, onNavigateToAddMenu = navigateToAddMenu, diff --git a/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavHost.kt b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavHost.kt index 2a914ea..fc8bf8f 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavHost.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/navigator/MainNavHost.kt @@ -60,6 +60,7 @@ fun MainNavHost( ) menuFolderNavGraph( + padding = padding, navigateBack = navController::navigateUp, navigateToMenuFolderDetail = navController::navigateToMenuFolderDetail, navigateToMenuFolderAllMenu = navController::navigateToMenuFolderAllMenu, From 7d6b5e0ea26b4a2b239f4980f006aa58aec468c2 Mon Sep 17 00:00:00 2001 From: ikseong00 <127182222+ikseong00@users.noreply.github.com> Date: Sat, 2 Aug 2025 14:17:11 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20#74?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt | 5 +++-- .../ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt index e4a040d..fd918e9 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/screen/MenuFolderScreen.kt @@ -123,8 +123,9 @@ fun MenuFolderScreen( onDragStart = { offset -> swipedIndex = -1 dragAndDropListState.onDragStart(offset) - dragStartFolderId = - menuFolders[dragAndDropListState.initialIndex ?: 0].menuFolderId + dragStartFolderId = dragAndDropListState.initialIndex?.let { index -> + menuFolders.getOrNull(index)?.menuFolderId ?: -1 + } ?: -1 }, onDragEnd = { viewModel.patchMenuFolders( diff --git a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt index 2408f1c..7c89fcc 100644 --- a/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt +++ b/app/src/main/java/com/kuit/ourmenu/ui/menuFolder/viewmodel/MenuFolderViewModel.kt @@ -86,8 +86,6 @@ class MenuFolderViewModel @Inject constructor( val toIndex = to.coerceAtMost(_menuFolders.value.size - 1) - val index = _menuFolders.value[toIndex].index - viewModelScope.launch { _isLoading.value = true _error.value = null