Kotlin

[Kotlin + Compose] 달력 구현(월 이동, 도트찍기, 모달)

mooni_ 2025. 2. 13. 23:03

0. CustomCalendar

@Composable
fun CustomCalendar(onClick: () -> Unit) {
    val today = LocalDate.now()

    var visibleMonth by remember() { mutableStateOf(YearMonth.now()) }

    var selectedDate by remember { mutableStateOf<LocalDate?>(null) }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 30.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.spacedBy(20.dp)
    ) {
        CalendarHeader(visibleMonth,
            onPreviousClick = { visibleMonth = visibleMonth.minusMonths(1) },
            onNextClick = { visibleMonth = visibleMonth.plusMonths(1) })

        CalendarBody(visibleMonth, today, onDateClick = { clickedDate ->
            selectedDate = clickedDate
            onClick()
        })
    }
}

 

  • CalendarHeader : 현재 연도와 월을 알려주고 이전 혹은 이후의 달로 이동 할 수 있음
  • CalendarBody : 현재 월에 맞는 날짜를 보여주고 특정 날짜를 선택할 수 있음

 

1. CalendarHeader

@Composable
fun CalendarHeader(
    visibleMonth: YearMonth,
    onPreviousClick: () -> Unit,
    onNextClick: () -> Unit
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 30.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = ImageVector.vectorResource(R.drawable.previous_icon),
            contentDescription = "Previous",
            tint = IconGray,
            modifier = Modifier.clickable { onPreviousClick() }
        )
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(5.dp)
        ) {
            //영어로 월 출력
            Text(
                text = visibleMonth.month.getDisplayName(TextStyle.FULL, Locale.ENGLISH),
                style = AppTypography.titleMedium,
                color = TextBlack
            )
            
            //연도 출력
            Text(
                text = "${visibleMonth.year}",
                style = AppTypography.labelMedium,
                color = TextDarkGray,
            )
        }
        Icon(
            imageVector = ImageVector.vectorResource(R.drawable.next_icon),
            contentDescription = "Next",
            tint = IconGray,
            modifier = Modifier.clickable { onNextClick() })
    }
    
    //Mon ~ Sun 글자 표시
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 30.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(text = "Mon", color = TextDarkGray, style = AppTypography.bodyMedium)
        Text(text = "Tue", color = TextDarkGray, style = AppTypography.bodyMedium)
        Text(text = "Wed", color = TextDarkGray, style = AppTypography.bodyMedium)
        Text(text = "Thu", color = TextDarkGray, style = AppTypography.bodyMedium)
        Text(text = "Fri", color = TextDarkGray, style = AppTypography.bodyMedium)
        Text(text = "Sat", color = TextDarkGray, style = AppTypography.bodyMedium)
        Text(text = "Sun", color = TextDarkGray, style = AppTypography.bodyMedium)
    }
}
  • getDisplayName(TextStyle.FULL, Locale.ENGLISH) : 영어로 Month를 받을 수 있음, TextStyle을 조정해 짧게도 가능
  • previous, next 아이콘에 월 이동 함수 설정

 

2. CalendarBody

@Composable
fun CalendarBody(
    visibleMonth: YearMonth,
    today: LocalDate,
    selectedDate: LocalDate?,
    onDateClick: (LocalDate) -> Unit
) {
    val firstDayOfMonth = visibleMonth.atDay(1) // 해당 월의 첫 번째 날
    val daysInMonth = visibleMonth.lengthOfMonth() // 해당 월의 총 날짜 개수
    val firstDayOfWeek = (firstDayOfMonth.dayOfWeek.value + 6) % 7 // 월요일(0) ~ 일요일(6)로 변환

    val totalCells = firstDayOfWeek + daysInMonth // 요일 포함한 전체 셀 개수
    val weeks = (totalCells + 6) / 7 // 총 주(week) 개수

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 30.dp)
    ) {
        // 주 단위로 Row 생성
        for (week in 0 until weeks) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(30.dp),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                for (day in 0..6) { // 7일씩 표시
                    val dayIndex = week * 7 + day - firstDayOfWeek + 1

                    if (dayIndex in 1..daysInMonth) {
                        val date = visibleMonth.atDay(dayIndex)
                        val isToday = date == today

                        Column(
                            modifier = Modifier
                                .size(35.dp, 30.dp)
                                .then(if (isToday) Modifier.background(BackgroundSky) else Modifier)
                                .clickable {
                                    onDateClick(date) // 날짜 선택
                                },
                            verticalArrangement = Arrangement.Top,
                            horizontalAlignment = Alignment.CenterHorizontally
                        ) {
                            Text(
                                text = dayIndex.toString(),
                                color = TextBlack,
                                style = AppTypography.bodyMedium
                            )

                            // 임시로 오늘을 기준으로 dot 찍음
                            if (isToday) {
                                EventDot(MainNavy)
                            }
                        }
                    } else {
                        // 빈 칸을 위한 Spacer (첫 주의 공백을 맞추기 위해)
                        Spacer(modifier = Modifier.size(35.dp, 30.dp))
                    }
                }
            }
        }
    }
}
  • visibleMonth에 따라 날짜를 보여줌
  • 월요일부터 일요일을 하나의 행으로 정렬함

 

3. EventDot

@Composable
fun EventDot(color: Color) {
    Box(
        modifier = Modifier
            .size(4.dp)
            .clip(CircleShape)
            .background(color)
    )
}
  • 지정한 color의 Dot 생성

 

4. Screen