diff --git a/core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakEditableTimeTable.kt b/core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakEditableTimeTable.kt index f43a70a1..2af27f9c 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakEditableTimeTable.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakEditableTimeTable.kt @@ -20,9 +20,11 @@ import androidx.compose.ui.unit.dp import com.sopt.core.designsystem.theme.NoostakTheme import com.sopt.core.extension.noRippleClickable import com.sopt.core.type.CellType +import com.sopt.core.util.timetable.TimeTable import com.sopt.domain.entity.AvailableTimeEntity import com.sopt.domain.entity.TimeEntity import com.sopt.domain.entity.TimeTableEntity +import java.sql.Time @Composable fun NoostakEditableTimeTable( @@ -31,7 +33,7 @@ fun NoostakEditableTimeTable( onSelectionChange: (List) -> Unit ) { val days = data.timeEntity.size - val timeSlots = calculateTimeSlots(data.startTime, data.endTime) + val timeSlots = TimeTable().calculateTimeSlots(data.startTime, data.endTime) val selectedCells = remember { mutableStateListOf>() } // Row, Column 저장 LazyVerticalGrid( @@ -45,11 +47,11 @@ fun NoostakEditableTimeTable( ) { items((days + 1) * (timeSlots + 1)) { index -> val (rowIndex, columnIndex) = index / (days + 1) to index % (days + 1) - val cellType = determineCellType(rowIndex, columnIndex) + val cellType = TimeTable().determineCellType(rowIndex, columnIndex) val isSelected = selectedCells.contains(rowIndex to columnIndex) val backgroundColor = getEditableBackgroundColor(cellType, isSelected) - val text = getCellText(cellType, rowIndex, columnIndex, data) + val text = TimeTable().getCellText(cellType, rowIndex, columnIndex, data) NoostakEditableTimeTableBox( index = index, diff --git a/core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakTimeTable.kt b/core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakTimeTable.kt index 8a24ec71..fdc1729c 100644 --- a/core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakTimeTable.kt +++ b/core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakTimeTable.kt @@ -3,31 +3,32 @@ package com.sopt.core.designsystem.component.timetable import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape 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.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.sopt.core.designsystem.theme.Gray200 import com.sopt.core.designsystem.theme.NoostakAndroidTheme import com.sopt.core.designsystem.theme.NoostakTheme -import com.sopt.core.type.AvailabilityLevel import com.sopt.core.type.CellType +import com.sopt.core.util.timetable.TimeTable import com.sopt.domain.entity.AvailableTimeEntity import com.sopt.domain.entity.TimeEntity import com.sopt.domain.entity.TimeTableEntity -import java.time.LocalDate -import java.time.format.DateTimeFormatter -import java.time.format.TextStyle -import java.util.Locale @Composable fun NoostakTimeTable( @@ -35,154 +36,95 @@ fun NoostakTimeTable( modifier: Modifier = Modifier ) { val days = data.timeEntity.size - val timeSlots = calculateTimeSlots(data.startTime, data.endTime) + val timeSlots = TimeTable().calculateTimeSlots(data.startTime, data.endTime) - LazyVerticalGrid( - modifier = modifier, - columns = GridCells.Fixed(days + 1), - contentPadding = PaddingValues(0.dp) - ) { - items((days + 1) * (timeSlots + 1)) { index -> - val (rowIndex, columnIndex) = index / (days + 1) to index % (days + 1) - - val cellType = determineCellType(rowIndex, columnIndex) - val backgroundColor = getBackgroundColor(cellType, rowIndex, columnIndex, data) - val text = getCellText(cellType, rowIndex, columnIndex, data) - - NoostakTimeTableBox( - index = index, - days = days, - timeSlots = timeSlots, - backgroundColor = backgroundColor, - text = text - ) - } - } -} - -@Composable -fun getBackgroundColor( - cellType: CellType, - rowIndex: Int, - columnIndex: Int, - data: TimeTableEntity -): Color = when (cellType) { - CellType.Blank, CellType.DateHeader, CellType.TimeHeader -> Color.Transparent - CellType.Data -> { - val startHour = data.startTime.split(":")[0].toInt() - val currentHour = startHour + (rowIndex - 1) - val availableTimes = data.timeEntity.getOrNull(columnIndex - 1)?.times - - val matchingLevel = availableTimes?.find { timeEntity -> - val entityStartHour = timeEntity.startTime.split(":")[0].toInt() - val entityEndHour = timeEntity.endTime.split(":")[0].toInt() - currentHour in entityStartHour until entityEndHour - }?.level - - getColorByLevel(matchingLevel ?: 0) - } -} - -fun getCellText( - cellType: CellType, - rowIndex: Int, - columnIndex: Int, - data: TimeTableEntity -): String { - val startHour = data.startTime.split(":")[0].toInt() - - return when (cellType) { - CellType.Blank -> "\n" - CellType.DateHeader -> { - val dateEntity = data.timeEntity.getOrNull(columnIndex - 1) - val date = dateEntity?.date ?: "" - formatDateHeader(date) - } - - CellType.TimeHeader -> "${startHour + (rowIndex - 1)}시" - CellType.Data -> "" - } -} - -@Composable -fun NoostakTimeTableBox( - index: Int, - days: Int, - timeSlots: Int, - backgroundColor: Color, - text: String -) { - val shape = when (index) { - 0 -> RoundedCornerShape(topStart = 10.dp) - days -> RoundedCornerShape(topEnd = 10.dp) - (days + 1) * timeSlots -> RoundedCornerShape(bottomStart = 10.dp) - (days + 1) * (timeSlots + 1) - 1 -> RoundedCornerShape(bottomEnd = 10.dp) - else -> RoundedCornerShape(0.dp) - } - - Box( - modifier = Modifier + LazyColumn( + modifier = modifier .border( - width = 0.5.dp, - color = NoostakTheme.colors.gray100, - shape = shape + width = 1.dp, + color = NoostakTheme.colors.gray200, + shape = RoundedCornerShape(8.dp) ) - .background( - color = backgroundColor, - shape = shape - ) - .defaultMinSize(minWidth = 42.dp, minHeight = 36.dp), - contentAlignment = Alignment.Center ) { - Text( - modifier = Modifier.padding(horizontal = 5.dp, vertical = 3.dp), - text = text, - color = NoostakTheme.colors.gray600, - style = NoostakTheme.typography.c4Regular, - textAlign = TextAlign.Center, - maxLines = 2 - ) - } -} - -fun determineCellType(rowIndex: Int, columnIndex: Int): CellType = when { - rowIndex == 0 && columnIndex == 0 -> CellType.Blank - rowIndex == 0 -> CellType.DateHeader - columnIndex == 0 -> CellType.TimeHeader - else -> CellType.Data -} - -@Composable -fun getColorByLevel(level: Int): Color = - when (AvailabilityLevel.entries.firstOrNull { level in it.range }) { - AvailabilityLevel.NONE -> Color.Transparent - AvailabilityLevel.FEW -> NoostakTheme.colors.blue50 - AvailabilityLevel.SOME -> NoostakTheme.colors.blue200 - AvailabilityLevel.MANY -> NoostakTheme.colors.blue400 - AvailabilityLevel.MOST -> NoostakTheme.colors.blue700 - else -> NoostakTheme.colors.blue800 + // 행 반복 + items(timeSlots + 1) { rowIndex -> + Row(modifier = Modifier.fillMaxWidth()) { + // 열 반복 + for (columnIndex in 0..days) { + val cellType = TimeTable().determineCellType(rowIndex, columnIndex) + val backgroundColor = TimeTable().getBackgroundColor(cellType, rowIndex, columnIndex, data) + val text = TimeTable().getCellText(cellType, rowIndex, columnIndex, data) + val shape = when (rowIndex to columnIndex) { + 0 to 0 -> RoundedCornerShape(topStart = 8.dp) + 0 to days -> RoundedCornerShape(topEnd = 8.dp) + timeSlots to days -> RoundedCornerShape(bottomEnd = 8.dp) + timeSlots to 0 -> RoundedCornerShape(bottomStart = 8.dp) + else -> RoundedCornerShape(0.dp) + } + + Box( + modifier = when (cellType) { + CellType.Blank -> Modifier + .width(42.dp) // 고정 너비 + .height(36.dp) // 고정 높이 + CellType.TimeHeader -> Modifier + .width(42.dp) + .height(32.dp) + + CellType.DateHeader -> Modifier + .weight(1f) // 날짜 셀은 남은 공간 비율로 채움 + .height(36.dp) + + else -> Modifier + .weight(1f) // 나머지 셀은 남은 공간 비율로 채움 + .height(32.dp) + } + .background( + color = backgroundColor, + shape = shape + ) + .drawBehind { + val borderWidth = 1.dp.toPx() + val borderColor = Gray200 + + // 위쪽 선 그리기 + if (rowIndex > 0) { + drawLine( + color = borderColor, + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = borderWidth + ) + } + + // 왼쪽 선 그리기 + if (columnIndex > 0) { + drawLine( + color = borderColor, + start = Offset(0f, 0f), + end = Offset(0f, size.height), + strokeWidth = borderWidth + ) + } + }, + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = NoostakTheme.typography.c4Regular, + color = NoostakTheme.colors.gray600, + textAlign = TextAlign.Center + ) + } + } + } + } } - -fun calculateTimeSlots(startTime: String, endTime: String): Int { - val startHour = startTime.split(":")[0].toInt() - val endHour = endTime.split(":")[0].toInt() - return endHour - startHour -} - -fun formatDateHeader(date: String): String { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") - val parsedDate = LocalDate.parse(date, formatter) - - val dayOfWeek = parsedDate.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN) - val month = "%02d".format(parsedDate.monthValue) - val day = "%02d".format(parsedDate.dayOfMonth) - - return "$dayOfWeek\n$month/$day" } @Preview(showBackground = true) @Composable -fun NoostakTimeTablePreview() { +fun NoostakTimeTable1Preview() { NoostakAndroidTheme { val data = TimeTableEntity( startTime = "09:00", @@ -205,6 +147,11 @@ fun NoostakTimeTablePreview() { startTime = "15:00", endTime = "16:00", level = 60 + ), + AvailableTimeEntity( + startTime = "17:00", + endTime = "18:00", + level = 80 ) ) ), @@ -230,7 +177,12 @@ fun NoostakTimeTablePreview() { ) ) ) - - NoostakTimeTable(data = data) + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + NoostakTimeTable(data = data) + } } -} +} \ No newline at end of file diff --git a/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt b/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt new file mode 100644 index 00000000..25a681c6 --- /dev/null +++ b/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt @@ -0,0 +1,94 @@ +package com.sopt.core.util.timetable + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.sopt.core.designsystem.theme.NoostakTheme +import com.sopt.core.type.AvailabilityLevel +import com.sopt.core.type.CellType +import com.sopt.domain.entity.TimeTableEntity +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.TextStyle +import java.util.Locale + +class TimeTable { + @Composable + fun getBackgroundColor( + cellType: CellType, + rowIndex: Int, + columnIndex: Int, + data: TimeTableEntity + ): Color = when (cellType) { + CellType.Blank, CellType.DateHeader, CellType.TimeHeader -> Color.Transparent + CellType.Data -> { + val startHour = data.startTime.split(":")[0].toInt() + val currentHour = startHour + (rowIndex - 1) + val availableTimes = data.timeEntity.getOrNull(columnIndex - 1)?.times + + val matchingLevel = availableTimes?.find { timeEntity -> + val entityStartHour = timeEntity.startTime.split(":")[0].toInt() + val entityEndHour = timeEntity.endTime.split(":")[0].toInt() + currentHour in entityStartHour until entityEndHour + }?.level + + getColorByLevel(matchingLevel ?: 0) + } + } + + fun getCellText( + cellType: CellType, + rowIndex: Int, + columnIndex: Int, + data: TimeTableEntity + ): String { + val startHour = data.startTime.split(":")[0].toInt() + + return when (cellType) { + CellType.Blank -> "\n" + CellType.DateHeader -> { + val dateEntity = data.timeEntity.getOrNull(columnIndex - 1) + val date = dateEntity?.date ?: "" + formatDateHeader(date) + } + + CellType.TimeHeader -> "${startHour + (rowIndex - 1)}시" + CellType.Data -> "" + } + } + + + fun determineCellType(rowIndex: Int, columnIndex: Int): CellType = when { + rowIndex == 0 && columnIndex == 0 -> CellType.Blank + rowIndex == 0 -> CellType.DateHeader + columnIndex == 0 -> CellType.TimeHeader + else -> CellType.Data + } + + @Composable + fun getColorByLevel(level: Int): Color = + when (AvailabilityLevel.entries.firstOrNull { level in it.range }) { + AvailabilityLevel.NONE -> Color.Transparent + AvailabilityLevel.FEW -> NoostakTheme.colors.blue50 + AvailabilityLevel.SOME -> NoostakTheme.colors.blue200 + AvailabilityLevel.MANY -> NoostakTheme.colors.blue400 + AvailabilityLevel.MOST -> NoostakTheme.colors.blue700 + else -> NoostakTheme.colors.blue800 + } + + fun calculateTimeSlots(startTime: String, endTime: String): Int { + val startHour = startTime.split(":")[0].toInt() + val endHour = endTime.split(":")[0].toInt() + return endHour - startHour + } + + fun formatDateHeader(date: String): String { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val parsedDate = LocalDate.parse(date, formatter) + + val dayOfWeek = parsedDate.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN) + val month = "%02d".format(parsedDate.monthValue) + val day = "%02d".format(parsedDate.dayOfMonth) + + return "$dayOfWeek\n$month/$day" + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/screen/CurrentStatusScreen.kt b/presentation/src/main/java/com/sopt/presentation/appointment/screen/CurrentStatusScreen.kt index 0f7dafde..dfbf2434 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/screen/CurrentStatusScreen.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/screen/CurrentStatusScreen.kt @@ -3,7 +3,6 @@ package com.sopt.presentation.appointment.screen import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button @@ -72,7 +71,7 @@ fun CurrentStatusScreen( } } } - NoostakTimeTable(data = data, modifier = Modifier.fillMaxSize()) + NoostakTimeTable(data = data, modifier = Modifier.fillMaxWidth()) } @Preview(showBackground = true)