본문 바로가기
⛏️ | 개발 기록/⏰ | Schedule Planner

[Android, Kotlin] 일정 item에 menu 추가해서 일정 수정하기 기능 추가하기 (16)

by immgga 2024. 4. 19.

이번에 만들 기능은 일정 추가 기능이다.

일정 수정을 일정 생성할 때 할 수 있었는데, 이를 생성하고 나서도 수정할 수 있게 만들어야 나중에 일정 생성하고도 수정사항이 있을 때, 일정을 새로 생성해야 하는 불편함을 줄일 수 있을 것 같았다.

그러면 바로 만들어 볼까?

 

1. 설계

우선은 일정 수정 기능을 어떻게 만들지 생각해야 한다.

일단 바로 생각난 것은 더보기 아이콘을 이용한 dropdown menu를 생성하는 것이었다. 

일정 정보 item의 오른쪽 끝에 더보기 아이콘을 넣어서 더보기 icon을 클릭하면 dropdown menu를 열어서 일정 수정하기 기능을 만드려고

했었는데....

dropdown menu 특성상 커스텀이 어려웠고, 막상 적용해 보았는데 생각보다 안 어울려서 내가 따로 menu를 만들기로 했다.

 

일정 정보 item의 오른쪽 끝에 아이콘이 들어가는 것은 동일하지만, 화살표 icon을 넣어서 icon을 누르면 아래로 menu들이 나올 수 있도록 만들어 보겠다.

dropdown menu보다 커스터마이징도 간편하고(내가 만들기 때문 ㅎㅎ), 필요한 메뉴를 바로바로 추가할 수 있어서(dropdown menu도 지원)이다. 

 

2. 일정 item menu 제작

우선 일정 item에 들어갈 menu item을 만들어 보겠다.

@Composable
fun PlanMenuItem(
    titleIconImageVector: ImageVector,
    itemColor: Color = Color.Black,
    menuTitle: String,
    menuClick: () -> Unit
) {
    Row(
        modifier = Modifier
            .background(Color.White)
            .padding(horizontal = 15.dp)
            .clickable { menuClick() },
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            modifier = Modifier.padding(10.dp),
            imageVector = titleIconImageVector,
            tint = itemColor,
            contentDescription = "plan menu icon"
        )
        Text(
            modifier = Modifier.weight(1f),
            text = menuTitle,
            color = itemColor
        )
    }
}

text만 들어가면 심심해서 간단하게 icon과 title text를 이용해 만들어 주었다.

 

다음으로 일정 item에 달아준다.

if (planData.isMenuOpen) {
    val titleList = listOf("일정 수정하기", "일정 삭제하기")
    val itemColorList = listOf(Color(0xFFFFDB86), Color.Red)
    val iconList = listOf(Icons.Default.DateRange, Icons.Default.Delete)

    titleList.forEachIndexed { index, title ->
        Divider(
            modifier = Modifier
                .height(0.5.dp)
                .padding(horizontal = 15.dp)
        )

        PlanMenuItem(
            titleIconImageVector = iconList[index],
            itemColor = itemColorList[index],
            menuTitle = title
        ) {
            when (title) {
                "일정 수정하기" -> dialogState.value = true
                "일정 삭제하기" -> onPlanDelete()
            }
        }
    }
}

위 코드는 현재 일정 item에서 menu item 부분만 가져온 코드이다.

먼저 title, itemColor, icon을 설정할 list에 담아주고, for문으로 list의 개수만큼 menu를 추가해 주는 방식으로 구현했다.

그리고 if 문의 조건에 isMenuOpen은 planData data class에 직접 추가한 state이다.

왜 바로 일정 item composable 안에서 state를 생성하지 않고, data class에 생성했냐면...

같은 index의 일정끼리 state를 공유하는 문제가 발생했기 때문이다.(예를 들어 4월 19일의 1번째 일정에서 state를 true로 설정하고 4월 18일로 이동했을 때, 18일의 1번째 일정이 19일과 마찬가지로 true로 유지되는 문제가 있었다)

 

마지막으로 일정 item에서 세부적인 디테일을 추가해 주겠다.

ui를 설계할 때, 일정 item의 icon이 추가된다고 했었는데 이를 더 디테일한 작동을 하도록 약간 변경해보려 한다.

menu가 열렸을 때는 화살표 방향이 위쪽을 향하게 하고, 아닐 때는 화살표 방향을 아래쪽으로 향하게 해 주겠다.

IconButton(
    modifier = Modifier.padding(horizontal = 10.dp),
    onClick = { planData.isMenuOpen = !planData.isMenuOpen }
) {
    Icon(
        modifier = Modifier.rotate(if (planData.isMenuOpen) 180f else 0f),
        imageVector = Icons.Default.KeyboardArrowDown,
        tint = Color(0xFF6EC4A7),
        contentDescription = "more icon"
    )
}

위 코드는 일정 item의 오른쪽 끝에 위치하는 icon 코드를 가져온 것이다.

iconButton의 onClick에 menuOpen State를 현재 state의 반대로 변경되게 해 주고, 그 state에 맞게 icon의 각도가 변할 수 있도록 modifier.rotate로 설정해 주었다.

 

3. 일정 수정 dialog 생성 및 적용

이제 일정 수정용 ui를 만들어 기능을 달아주면 된다.

수정할 때는 dialog를 이용해보려고 한다.

@Composable
fun PlanModifyDialog(
    planData: PlanData,
    onSaveClick: (title: String, description: String) -> Unit,
    onDismissRequest: () -> Unit,
    onCancelClick: () -> Unit
) {
    val titleState = remember { mutableStateOf(planData.title) }
    val descriptionState = remember { mutableStateOf(planData.description) }
    Dialog(onDismissRequest = onDismissRequest) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .shadow(elevation = 1.dp, shape = RoundedCornerShape(8.dp))
                .background(Color(0xFFFAFAFA))
                .clip(RoundedCornerShape(8.dp)),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            PlannerV2TextField(
                modifier = Modifier.padding(horizontal = 15.dp, vertical = 10.dp),
                value = titleState.value,
                hint = "변경할 제목을 입력하세요.",
                singleLine = true,
                maxLines = 1,
                textStyle = TextStyle(
                    fontSize = 19.sp,
                    fontWeight = FontWeight.Medium
                ),
                hintTextStyle = TextStyle(
                    fontSize = 19.sp,
                    fontWeight = FontWeight.Medium
                ),
                onValueChange = { titleState.value = it }
            )

            PlannerV2TextField(
                modifier = Modifier
                    .padding(horizontal = 15.dp)
                    .height(85.dp),
                value = descriptionState.value,
                hint = "변경할 상세 내용을 입력하세요.",
                singleLine = false,
                maxLines = 4,
                onValueChange = { descriptionState.value = it }
            )

            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 10.dp, horizontal = 15.dp)
            ) {
                Button(
                    modifier = Modifier.width(100.dp),
                    shape = RoundedCornerShape(8.dp),
                    colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF6EC4A7)),
                    onClick = onCancelClick
                ) { Text(text = "취소") }

                Spacer(modifier = Modifier.weight(1f))

                Button(
                    modifier = Modifier.width(100.dp),
                    shape = RoundedCornerShape(8.dp),
                    colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFFDB86)),
                    onClick = { onSaveClick(titleState.value, descriptionState.value) }
                ) { Text(text = "저장") }
            }
        }
    }
}

composable 파라미터에 저장 클릭, 취소 클릭, dialog 종료 로직을 담은 고차 함수를 생성해 주었다.

그리고 textField에서 변경될 title, description을 저장할 state도 생성해 주었다.

일정을 저장했을 때, 변경된 title, description을 고차 함수를 통해 넘겨서 변경된 text 정보를 받아서 수정 로직을 수행하기 위해서이다.

이거 말고는 그냥 평범한 ui 구현이다.

 

dialog를 만들었으니, 바로 적용해 볼까?

val dialogState = remember { mutableStateOf(false) }

...

if (dialogState.value) {
    PlanModifyDialog(
        planData = planData,
        onDismissRequest = { dialogState.value = false },
        onSaveClick = { title, description ->
            dialogState.value = false
            onPlanModify(title, description)
        },
        onCancelClick =  { dialogState.value = false }
    )
}

저장 클릭에 넘겨받은 title, description을 이용해 firestore 데이터를 수정하는 함수를 호출한다(onPlanModify).

그리고 dialog를 종료한다(dialogState를 false로 변경하면 dialog 종료)

 

또한 코드로는 보여주지 않았지만, 이전 포스팅에서 구현한 일정 삭제 기능을 plan item menu로 이전했다.

직접 끌어보지 않는 이상 모른다는 점(눈으로 보이지 않음)이 가장 컸던 것 같다.

 

4. 결과 및 정리 

간단하게 구현 결과를 확인해 보자.

 

일정 item 하단에 menu들도 잘 나타나고, dialog의 modify 로직과 일정 삭제 로직도 정상 작동하였다.

여기까지 일정 menu를 추가해서 일정 수정 기능을 구현하고, 삭제 기능도 옮기는 작업을 했다.

이제 슬슬 Planner V2도 마무리하려고 한다.

일단 생각하고 있는 기능이 하나 더 있어서 이거 하나 구현하면 진짜로 개발 끝일 것 같다.

또한 역시 커스터마이징 하는 것이 가장 좋은 것 같다.  

728x90