From 48842ad99c9161487c28ca94612486b082fce548 Mon Sep 17 00:00:00 2001 From: gaeulzzang Date: Thu, 9 Jan 2025 23:21:20 +0900 Subject: [PATCH 1/5] =?UTF-8?q?#18=20[FIX]=20=EC=8B=9C=EA=B0=84=ED=96=89?= =?UTF-8?q?=20=ED=81=AC=EA=B8=B0=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timetable/NoostakEditableTimeTable.kt | 8 +- .../component/timetable/NoostakTimeTable.kt | 256 +++++++----------- .../com/sopt/core/util/timetable/TimeTable.kt | 94 +++++++ .../appointment/screen/CurrentStatusScreen.kt | 3 +- 4 files changed, 204 insertions(+), 157 deletions(-) create mode 100644 core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt 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) From 2f1deff051f5f1771e2f6c09bb855fc37896fbec Mon Sep 17 00:00:00 2001 From: gaeulzzang Date: Fri, 10 Jan 2025 01:55:12 +0900 Subject: [PATCH 2/5] =?UTF-8?q?#18=20[FIX]=20entity=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timetable/NoostakEditableTimeTable.kt | 246 +++++++++--------- .../component/timetable/NoostakTimeTable.kt | 135 +++++++--- .../com/sopt/core/util/timetable/TimeTable.kt | 120 +++++---- .../sopt/domain/entity/AvailableTimeEntity.kt | 21 ++ .../com/sopt/domain/entity/PeriodEntity.kt | 7 + .../com/sopt/domain/entity/TimeTableEntity.kt | 18 -- .../appointment/AppointmentRoute.kt | 13 +- .../appointment/AppointmentViewModel.kt | 189 +++++++------- .../appointmentCheck/AppointmentCheckRoute.kt | 40 ++- .../AppointmentCheckViewModel.kt | 34 --- .../appointment/screen/CurrentStatusScreen.kt | 13 +- 11 files changed, 456 insertions(+), 380 deletions(-) create mode 100644 domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt create mode 100644 domain/src/main/java/com/sopt/domain/entity/PeriodEntity.kt delete mode 100644 domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt 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 2af27f9c..db8f9990 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 @@ -26,127 +26,127 @@ import com.sopt.domain.entity.TimeEntity import com.sopt.domain.entity.TimeTableEntity import java.sql.Time -@Composable -fun NoostakEditableTimeTable( - data: TimeTableEntity, - modifier: Modifier = Modifier, - onSelectionChange: (List) -> Unit -) { - val days = data.timeEntity.size - val timeSlots = TimeTable().calculateTimeSlots(data.startTime, data.endTime) - val selectedCells = remember { mutableStateListOf>() } // Row, Column 저장 +//@Composable +//fun NoostakEditableTimeTable( +// data: TimeTableEntity, +// modifier: Modifier = Modifier, +// onSelectionChange: (List) -> Unit +//) { +// val days = data.timeEntity.size +// val timeSlots = TimeTable().calculateTimeSlots(data.startTime, data.endTime) +// val selectedCells = remember { mutableStateListOf>() } // Row, Column 저장 +// +// LazyVerticalGrid( +// modifier = modifier +// .border( +// width = 1.dp, +// color = NoostakTheme.colors.gray200, +// shape = RoundedCornerShape(8.dp) +// ), +// columns = GridCells.Fixed(days + 1) +// ) { +// items((days + 1) * (timeSlots + 1)) { index -> +// val (rowIndex, columnIndex) = index / (days + 1) to index % (days + 1) +// val cellType = TimeTable().determineCellType(rowIndex, columnIndex) +// val isSelected = selectedCells.contains(rowIndex to columnIndex) +// val backgroundColor = +// getEditableBackgroundColor(cellType, isSelected) +// val text = TimeTable().getCellText(cellType, rowIndex, columnIndex, data) +// +// NoostakEditableTimeTableBox( +// index = index, +// days = days, +// timeSlots = timeSlots, +// backgroundColor = backgroundColor, +// text = text, +// onClick = { +// if (cellType == CellType.Data) { +// val cell = rowIndex to columnIndex +// if (selectedCells.contains(cell)) { +// selectedCells.remove(cell) +// } else { +// selectedCells.add(cell) +// } +// onSelectionChange( +// selectedCells.toTimeEntities( +// data.startTime, +// data.timeEntity +// ) +// ) +// } +// } +// ) +// } +// } +//} - LazyVerticalGrid( - modifier = modifier - .border( - width = 1.dp, - color = NoostakTheme.colors.gray200, - shape = RoundedCornerShape(8.dp) - ), - columns = GridCells.Fixed(days + 1) - ) { - items((days + 1) * (timeSlots + 1)) { index -> - val (rowIndex, columnIndex) = index / (days + 1) to index % (days + 1) - val cellType = TimeTable().determineCellType(rowIndex, columnIndex) - val isSelected = selectedCells.contains(rowIndex to columnIndex) - val backgroundColor = - getEditableBackgroundColor(cellType, isSelected) - val text = TimeTable().getCellText(cellType, rowIndex, columnIndex, data) - - NoostakEditableTimeTableBox( - index = index, - days = days, - timeSlots = timeSlots, - backgroundColor = backgroundColor, - text = text, - onClick = { - if (cellType == CellType.Data) { - val cell = rowIndex to columnIndex - if (selectedCells.contains(cell)) { - selectedCells.remove(cell) - } else { - selectedCells.add(cell) - } - onSelectionChange( - selectedCells.toTimeEntities( - data.startTime, - data.timeEntity - ) - ) - } - } - ) - } - } -} - -@Composable -fun getEditableBackgroundColor( - cellType: CellType, - isSelected: Boolean -): Color = when (cellType) { - CellType.Blank, CellType.DateHeader, CellType.TimeHeader -> Color.Transparent - CellType.Data -> if (isSelected) NoostakTheme.colors.blue400 else Color.Transparent -} - -@Composable -fun NoostakEditableTimeTableBox( - index: Int, - days: Int, - timeSlots: Int, - backgroundColor: Color, - text: String, - onClick: () -> Unit -) { - 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 - .border( - width = 0.5.dp, - color = NoostakTheme.colors.gray200, - shape = shape - ) - .background( - color = backgroundColor, - shape = shape - ) - .defaultMinSize(minWidth = 42.dp, minHeight = 36.dp) - .noRippleClickable { onClick() }, - 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 List>.toTimeEntities( - startTime: String, - timeEntityList: List -): List { - val startHour = startTime.split(":")[0].toInt() - - return this.groupBy { it.second }.mapNotNull { (columnIndex, rowIndex) -> - val date = timeEntityList.getOrNull(columnIndex - 1)?.date ?: return@mapNotNull null - val times = rowIndex.map { row -> - val hour = startHour + (row.first - 1) - AvailableTimeEntity( - startTime = "%02d:00".format(hour), - endTime = "%02d:00".format(hour + 1) - ) - } - TimeEntity(date = date, times = times) - } -} +//@Composable +//fun getEditableBackgroundColor( +// cellType: CellType, +// isSelected: Boolean +//): Color = when (cellType) { +// CellType.Blank, CellType.DateHeader, CellType.TimeHeader -> Color.Transparent +// CellType.Data -> if (isSelected) NoostakTheme.colors.blue400 else Color.Transparent +//} +// +//@Composable +//fun NoostakEditableTimeTableBox( +// index: Int, +// days: Int, +// timeSlots: Int, +// backgroundColor: Color, +// text: String, +// onClick: () -> Unit +//) { +// 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 +// .border( +// width = 0.5.dp, +// color = NoostakTheme.colors.gray200, +// shape = shape +// ) +// .background( +// color = backgroundColor, +// shape = shape +// ) +// .defaultMinSize(minWidth = 42.dp, minHeight = 36.dp) +// .noRippleClickable { onClick() }, +// 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 List>.toTimeEntities( +// startTime: String, +// timeEntityList: List +//): List { +// val startHour = startTime.split(":")[0].toInt() +// +// return this.groupBy { it.second }.mapNotNull { (columnIndex, rowIndex) -> +// val date = timeEntityList.getOrNull(columnIndex - 1)?.date ?: return@mapNotNull null +// val times = rowIndex.map { row -> +// val hour = startHour + (row.first - 1) +// AvailableTimeEntity( +// startTime = "%02d:00".format(hour), +// endTime = "%02d:00".format(hour + 1) +// ) +// } +// TimeEntity(date = date, times = times) +// } +//} 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 fdc1729c..376d5667 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 @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -27,16 +28,19 @@ import com.sopt.core.designsystem.theme.NoostakTheme import com.sopt.core.type.CellType import com.sopt.core.util.timetable.TimeTable import com.sopt.domain.entity.AvailableTimeEntity +import com.sopt.domain.entity.MemberAvailableTimeEntity +import com.sopt.domain.entity.PeriodEntity import com.sopt.domain.entity.TimeEntity import com.sopt.domain.entity.TimeTableEntity @Composable fun NoostakTimeTable( - data: TimeTableEntity, + availablePeriods: PeriodEntity, + availableTimes: TimeTableEntity, modifier: Modifier = Modifier ) { - val days = data.timeEntity.size - val timeSlots = TimeTable().calculateTimeSlots(data.startTime, data.endTime) + val days = availablePeriods.dates.size + val timeSlots = TimeTable().calculateTimeSlots(availablePeriods.startTime, availablePeriods.endTime) LazyColumn( modifier = modifier @@ -52,8 +56,8 @@ fun NoostakTimeTable( // 열 반복 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 text = TimeTable().getCellText(cellType, rowIndex, columnIndex, availablePeriods) + val backgroundColor = TimeTable().getBackgroundColor(cellType, rowIndex, columnIndex, availablePeriods, availableTimes) val shape = when (rowIndex to columnIndex) { 0 to 0 -> RoundedCornerShape(topStart = 8.dp) 0 to days -> RoundedCornerShape(topEnd = 8.dp) @@ -126,52 +130,105 @@ fun NoostakTimeTable( @Composable fun NoostakTimeTable1Preview() { NoostakAndroidTheme { - val data = TimeTableEntity( - startTime = "09:00", - endTime = "18:00", - timeEntity = listOf( - TimeEntity( - date = "2024-09-27", + val mockAvailablePeriods = PeriodEntity( + dates = listOf("2024-09-05T10:00:00", "2024-09-06T10:00:00", "2024-09-07T10:00:00"), + startTime = "2024-09-05T10:00:00", + endTime = "2024-09-07T18:00:00" + ) + + val mockAvailableTimes = TimeTableEntity( + members = listOf( + MemberAvailableTimeEntity( + memberId = 1, + memberName = "권장순", times = listOf( AvailableTimeEntity( - startTime = "09:00", - endTime = "10:00", - level = 20 - ), - AvailableTimeEntity( - startTime = "12:00", - endTime = "13:00", - level = 40 + date = "2024-09-05T00:00:00", + times = listOf( + TimeEntity( + memberStartTime = "2024-09-05T11:00:00", + memberEndTime = "2024-09-05T12:00:00" + ), + TimeEntity( + memberStartTime = "2024-09-05T13:00:00", + memberEndTime = "2024-09-05T14:00:00" + ) + ) ), AvailableTimeEntity( - startTime = "15:00", - endTime = "16:00", - level = 60 + date = "2024-09-06T00:00:00", + times = listOf( + TimeEntity( + memberStartTime = "2024-09-06T14:00:00", + memberEndTime = "2024-09-06T15:00:00" + ), + TimeEntity( + memberStartTime = "2024-09-06T16:00:00", + memberEndTime = "2024-09-06T17:00:00" + ) + ) ), AvailableTimeEntity( - startTime = "17:00", - endTime = "18:00", - level = 80 + date = "2024-09-07T00:00:00", + times = listOf( + TimeEntity( + memberStartTime = "2024-09-07T10:00:00", + memberEndTime = "2024-09-07T11:00:00" + ), + TimeEntity( + memberStartTime = "2024-09-07T12:00:00", + memberEndTime = "2024-09-07T13:00:00" + ) + ) ) ) ), - TimeEntity( - date = "2024-09-28", + MemberAvailableTimeEntity( + memberId = 2, + memberName = "김민수", times = listOf( AvailableTimeEntity( - startTime = "11:00", - endTime = "12:00", - level = 20 + date = "2024-09-05T00:00:00", + times = listOf( + TimeEntity( + memberStartTime = "2024-09-05T10:00:00", + memberEndTime = "2024-09-05T11:00:00" + ), + TimeEntity( + memberStartTime = "2024-09-05T12:00:00", + memberEndTime = "2024-09-05T13:00:00" + ), + TimeEntity( + memberStartTime = "2024-09-05T13:00:00", + memberEndTime = "2024-09-05T14:00:00" + ) + ) ), AvailableTimeEntity( - startTime = "14:00", - endTime = "15:00", - level = 40 + date = "2024-09-06T00:00:00", + times = listOf( + TimeEntity( + memberStartTime = "2024-09-06T14:00:00", + memberEndTime = "2024-09-06T15:00:00" + ), + TimeEntity( + memberStartTime = "2024-09-06T15:00:00", + memberEndTime = "2024-09-06T16:00:00" + ) + ) ), AvailableTimeEntity( - startTime = "17:00", - endTime = "18:00", - level = 80 + date = "2024-09-07T00:00:00", + times = listOf( + TimeEntity( + memberStartTime = "2024-09-07T11:00:00", + memberEndTime = "2024-09-07T12:00:00" + ), + TimeEntity( + memberStartTime = "2024-09-07T12:00:00", + memberEndTime = "2024-09-07T13:00:00" + ) + ) ) ) ) @@ -182,7 +239,11 @@ fun NoostakTimeTable1Preview() { .fillMaxSize() .padding(16.dp) ) { - NoostakTimeTable(data = data) + NoostakTimeTable( + availablePeriods = mockAvailablePeriods, + availableTimes = mockAvailableTimes, + modifier = Modifier.fillMaxWidth() + ) } } } \ 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 index 25a681c6..15872409 100644 --- a/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt +++ b/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt @@ -5,50 +5,91 @@ 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.PeriodEntity import com.sopt.domain.entity.TimeTableEntity -import java.time.LocalDate +import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.time.format.TextStyle import java.util.Locale class TimeTable { + // 시간 차이를 계산 (24시간 형식 기준) + fun calculateTimeSlots(startTime: String, endTime: String): Int { + val startHour = extractHour(extractTime(startTime)) + val endHour = extractHour(extractTime(endTime)) + return endHour - startHour + } + + 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 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 + availablePeriods: PeriodEntity, + availableTimes: TimeTableEntity + ): Color { + return when (cellType) { + CellType.Blank, CellType.DateHeader, CellType.TimeHeader -> Color.Transparent + CellType.Data -> { + val startHour = extractHour(extractTime(availablePeriods.startTime)) + val currentHour = startHour + (rowIndex - 1) - 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 + val date = + availablePeriods.dates.getOrNull(columnIndex - 1) ?: return Color.Transparent + // 해당 열(columnIndex) 날짜 데이터 가져오기 + val formattedDate = extractDate(date) + val availableTimesForDate = availableTimes.members.flatMap { member -> + member.times.filter { extractDate(it.date) == formattedDate } + } - getColorByLevel(matchingLevel ?: 0) + // 현재 시간에 해당하는 가능 레벨 계산 + val totalMembers = availableTimes.members.size + val availableMembers = availableTimesForDate.count { availableTime -> + availableTime.times.any { timeEntity -> + val entityStartHour = extractHour(extractTime(timeEntity.memberStartTime)) + val entityEndHour = extractHour(extractTime(timeEntity.memberEndTime)) + currentHour in entityStartHour until entityEndHour + } + } + val percentage = + if (totalMembers == 0) 0 else (availableMembers * 100 / totalMembers) + getColorByLevel(percentage) + } } } + @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 getCellText( cellType: CellType, rowIndex: Int, columnIndex: Int, - data: TimeTableEntity + data: PeriodEntity ): String { - val startHour = data.startTime.split(":")[0].toInt() + // 셀 텍스트 결정 로직 + val startHour = extractHour(extractTime(data.startTime)) return when (cellType) { CellType.Blank -> "\n" CellType.DateHeader -> { - val dateEntity = data.timeEntity.getOrNull(columnIndex - 1) - val date = dateEntity?.date ?: "" - formatDateHeader(date) + if (columnIndex == 0) "" + else formatDateTimeToCustomFormat(data.dates[columnIndex - 1]) } CellType.TimeHeader -> "${startHour + (rowIndex - 1)}시" @@ -56,39 +97,22 @@ class TimeTable { } } - - 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) - + fun formatDateTimeToCustomFormat(dateTime: String): String { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") + val parsedDate = LocalDateTime.parse(dateTime, 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" } + + // 공통적으로 사용하는 시간 추출 로직 + private fun extractTime(dateTime: String): String = dateTime.substringAfter('T') + + // 공통적으로 사용하는 날짜 추출 로직 + private fun extractDate(dateTime: String): String = dateTime.substringBefore('T') + + // 공통적으로 사용하는 시간만 숫자로 변환 + private fun extractHour(time: String): Int = time.substringBefore(':').toInt() } \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt b/domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt new file mode 100644 index 00000000..2d79e8f9 --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt @@ -0,0 +1,21 @@ +package com.sopt.domain.entity + +data class TimeTableEntity( + val members: List +) + +data class MemberAvailableTimeEntity( + val memberId: Int, + val memberName: String, + val times: List +) + +data class AvailableTimeEntity( + val date: String, + val times: List +) + +data class TimeEntity( + val memberStartTime: String, + val memberEndTime: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/entity/PeriodEntity.kt b/domain/src/main/java/com/sopt/domain/entity/PeriodEntity.kt new file mode 100644 index 00000000..9fc0282c --- /dev/null +++ b/domain/src/main/java/com/sopt/domain/entity/PeriodEntity.kt @@ -0,0 +1,7 @@ +package com.sopt.domain.entity + +data class PeriodEntity( + val dates: List, + val startTime: String, + val endTime: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt b/domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt deleted file mode 100644 index 01eb8161..00000000 --- a/domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.sopt.domain.entity - -data class TimeTableEntity( - val startTime: String, - val endTime: String, - val timeEntity: List -) - -data class TimeEntity( - val date: String, - val times: List? = null -) - -data class AvailableTimeEntity( - val startTime: String, - val endTime: String, - val level: Int? = null -) diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt index 5fe8c85b..d0c08d5d 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentRoute.kt @@ -42,6 +42,7 @@ import com.sopt.core.designsystem.theme.NoostakTheme import com.sopt.core.extension.noRippleClickable import com.sopt.core.extension.showIf import com.sopt.domain.entity.AppointmentEntity +import com.sopt.domain.entity.PeriodEntity import com.sopt.domain.entity.TimeTableEntity import com.sopt.presentation.R import com.sopt.presentation.appointment.screen.CurrentStatusScreen @@ -87,7 +88,8 @@ fun AppointmentRoute( onBackButtonClick = appointmentViewModel::navigateUp, onSubmitButtonClick = appointmentViewModel::navigateToAppointmentCheck, onConfirmButtonClick = appointmentViewModel::navigateToAppointmentConfirm, - currentStatus = appointmentViewModel.mockCurrentStatus, + availablePeriods = appointmentViewModel.mockAvailablePeriods, + availableTimes = appointmentViewModel.mockAvailableTimes, recommendations = appointmentViewModel.mockRecommendations ) } @@ -100,7 +102,8 @@ fun AppointmentScreen( onBackButtonClick: () -> Unit, onSubmitButtonClick: (Long, Long, String) -> Unit, onConfirmButtonClick: (Long, Long, Long, String) -> Unit, - currentStatus: TimeTableEntity, + availablePeriods: PeriodEntity, + availableTimes: TimeTableEntity, recommendations: AppointmentEntity ) { var selectedItemIndex by remember { mutableIntStateOf(-1) } @@ -203,7 +206,8 @@ fun AppointmentScreen( } if (selectedItemIndex == -1) { CurrentStatusScreen( - data = currentStatus + availablePeriods = availablePeriods, + availableTimes = availableTimes ) } else { RecommendationScreen( @@ -321,7 +325,8 @@ fun AppointmentScreenPreview() { onBackButtonClick = {}, onSubmitButtonClick = { _, _, _ -> }, onConfirmButtonClick = { _, _, _, _ -> }, - currentStatus = appointmentViewModel.mockCurrentStatus, + availablePeriods = appointmentViewModel.mockAvailablePeriods, + availableTimes = appointmentViewModel.mockAvailableTimes, recommendations = appointmentViewModel.mockRecommendations ) } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt index 8091e821..c48296eb 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt @@ -3,11 +3,14 @@ package com.sopt.presentation.appointment import com.sopt.core.util.BaseViewModel import com.sopt.domain.entity.AppointmentEntity import com.sopt.domain.entity.AvailableTimeEntity +import com.sopt.domain.entity.MemberAvailableTimeEntity +import com.sopt.domain.entity.PeriodEntity import com.sopt.domain.entity.PriorityEntity import com.sopt.domain.entity.RecommendationEntity import com.sopt.domain.entity.TimeEntity import com.sopt.domain.entity.TimeTableEntity import dagger.hilt.android.lifecycle.HiltViewModel +import java.sql.Time import javax.inject.Inject @HiltViewModel @@ -42,104 +45,110 @@ class AppointmentViewModel @Inject constructor() : BaseViewModel Unit, - onConfirmButtonClick: (Long, Long, String) -> Unit, - data: TimeTableEntity + onConfirmButtonClick: (Long, Long, String) -> Unit ) { var selectedData by remember { mutableStateOf(emptyList()) } Scaffold( @@ -114,20 +109,20 @@ fun AppointmentCheckScreen( ) // 타임테이블 (스크롤 가능) - NoostakEditableTimeTable( - data = data, - modifier = Modifier - .constrainAs(timeTable) { - top.linkTo(title.bottom) - start.linkTo(parent.start) - end.linkTo(parent.end) - bottom.linkTo(button.top) - height = Dimension.fillToConstraints - } - ) { - selectedData = it - Timber.d("Selected Data: $selectedData") - } +// NoostakEditableTimeTable( +// data = data, +// modifier = Modifier +// .constrainAs(timeTable) { +// top.linkTo(title.bottom) +// start.linkTo(parent.start) +// end.linkTo(parent.end) +// bottom.linkTo(button.top) +// height = Dimension.fillToConstraints +// } +// ) { +// selectedData = it +// Timber.d("Selected Data: $selectedData") +// } // 버튼 (항상 하단 고정) NoostakBottomButton( @@ -160,8 +155,7 @@ fun PreviewAppointmentConfirmScreen() { appointmentsId = 1, appointmentName = "3차 회의", onBackButtonClick = {}, - onConfirmButtonClick = { _, _, _ -> }, - data = appointmentCheckViewModel.mockTimeTableEntity + onConfirmButtonClick = { _, _, _ -> } ) } } diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt index 98c04282..21b0a526 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt @@ -27,40 +27,6 @@ class AppointmentCheckViewModel @Inject constructor() : emitSideEffect(AppointmentCheckSideEffect.NavigateToGroupDetail(groupId)) } - val mockTimeTableEntity = TimeTableEntity( - startTime = "07:00", - endTime = "24:00", - timeEntity = listOf( - TimeEntity( - date = "2024-09-27", - times = null - ), - TimeEntity( - date = "2024-09-28", - times = null - ), - TimeEntity( - date = "2024-09-29", - times = null - ), - TimeEntity( - date = "2024-09-30", - times = null - ), - TimeEntity( - date = "2024-10-01", - times = null - ), - TimeEntity( - date = "2024-10-02", - times = null - ), - TimeEntity( - date = "2024-10-03", - times = null - ) - ) - ) } sealed class AppointmentCheckSideEffect { 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 dfbf2434..b411d3dd 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 @@ -21,6 +21,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.core.designsystem.component.timetable.NoostakTimeTable import com.sopt.core.designsystem.theme.NoostakTheme import com.sopt.core.util.NoRippleInteractionSource +import com.sopt.domain.entity.PeriodEntity import com.sopt.domain.entity.TimeTableEntity import com.sopt.presentation.R import com.sopt.presentation.appointment.AppointmentViewModel @@ -28,7 +29,8 @@ import com.sopt.presentation.appointment.AppointmentViewModel @Composable fun CurrentStatusScreen( modifier: Modifier = Modifier, - data: TimeTableEntity + availablePeriods: PeriodEntity, + availableTimes: TimeTableEntity ) { Row( modifier = modifier @@ -71,7 +73,11 @@ fun CurrentStatusScreen( } } } - NoostakTimeTable(data = data, modifier = Modifier.fillMaxWidth()) + NoostakTimeTable( + availablePeriods = availablePeriods, + availableTimes = availableTimes, + modifier = Modifier.fillMaxWidth() + ) } @Preview(showBackground = true) @@ -79,6 +85,7 @@ fun CurrentStatusScreen( fun CurrentStatusScreenPreview() { val appointmentViewModel: AppointmentViewModel = hiltViewModel() CurrentStatusScreen( - data = appointmentViewModel.mockCurrentStatus + availablePeriods = appointmentViewModel.mockAvailablePeriods, + availableTimes = appointmentViewModel.mockAvailableTimes ) } From 68b9f55db32a752d7212e3fe6e0044c3543e45cb Mon Sep 17 00:00:00 2001 From: gaeulzzang Date: Fri, 10 Jan 2025 02:15:07 +0900 Subject: [PATCH 3/5] =?UTF-8?q?#18=20[REFACTOR]=20ktlint=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timetable/NoostakEditableTimeTable.kt | 274 +++++++++--------- .../component/timetable/NoostakTimeTable.kt | 48 +-- .../com/sopt/core/util/timetable/TimeTable.kt | 18 +- .../sopt/domain/entity/AvailableTimeEntity.kt | 2 +- .../com/sopt/domain/entity/PeriodEntity.kt | 2 +- .../appointment/AppointmentViewModel.kt | 3 +- .../appointmentCheck/AppointmentCheckRoute.kt | 31 +- .../AppointmentCheckViewModel.kt | 8 +- 8 files changed, 212 insertions(+), 174 deletions(-) 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 db8f9990..d4c2ea82 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 @@ -3,10 +3,14 @@ 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.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 @@ -14,139 +18,147 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember 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.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 +import com.sopt.domain.entity.PeriodEntity -//@Composable -//fun NoostakEditableTimeTable( -// data: TimeTableEntity, -// modifier: Modifier = Modifier, -// onSelectionChange: (List) -> Unit -//) { -// val days = data.timeEntity.size -// val timeSlots = TimeTable().calculateTimeSlots(data.startTime, data.endTime) -// val selectedCells = remember { mutableStateListOf>() } // Row, Column 저장 -// -// LazyVerticalGrid( -// modifier = modifier -// .border( -// width = 1.dp, -// color = NoostakTheme.colors.gray200, -// shape = RoundedCornerShape(8.dp) -// ), -// columns = GridCells.Fixed(days + 1) -// ) { -// items((days + 1) * (timeSlots + 1)) { index -> -// val (rowIndex, columnIndex) = index / (days + 1) to index % (days + 1) -// val cellType = TimeTable().determineCellType(rowIndex, columnIndex) -// val isSelected = selectedCells.contains(rowIndex to columnIndex) -// val backgroundColor = -// getEditableBackgroundColor(cellType, isSelected) -// val text = TimeTable().getCellText(cellType, rowIndex, columnIndex, data) -// -// NoostakEditableTimeTableBox( -// index = index, -// days = days, -// timeSlots = timeSlots, -// backgroundColor = backgroundColor, -// text = text, -// onClick = { -// if (cellType == CellType.Data) { -// val cell = rowIndex to columnIndex -// if (selectedCells.contains(cell)) { -// selectedCells.remove(cell) -// } else { -// selectedCells.add(cell) -// } -// onSelectionChange( -// selectedCells.toTimeEntities( -// data.startTime, -// data.timeEntity -// ) -// ) -// } -// } -// ) -// } -// } -//} +@Composable +fun NoostakEditableTimeTable( + availablePeriods: PeriodEntity, + modifier: Modifier = Modifier +) { + val days = availablePeriods.dates.size + val timeSlots = + TimeTable().calculateTimeSlots(availablePeriods.startTime, availablePeriods.endTime) + val selectedCells = remember { mutableStateListOf>() } -//@Composable -//fun getEditableBackgroundColor( -// cellType: CellType, -// isSelected: Boolean -//): Color = when (cellType) { -// CellType.Blank, CellType.DateHeader, CellType.TimeHeader -> Color.Transparent -// CellType.Data -> if (isSelected) NoostakTheme.colors.blue400 else Color.Transparent -//} -// -//@Composable -//fun NoostakEditableTimeTableBox( -// index: Int, -// days: Int, -// timeSlots: Int, -// backgroundColor: Color, -// text: String, -// onClick: () -> Unit -//) { -// 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 -// .border( -// width = 0.5.dp, -// color = NoostakTheme.colors.gray200, -// shape = shape -// ) -// .background( -// color = backgroundColor, -// shape = shape -// ) -// .defaultMinSize(minWidth = 42.dp, minHeight = 36.dp) -// .noRippleClickable { onClick() }, -// 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 List>.toTimeEntities( -// startTime: String, -// timeEntityList: List -//): List { -// val startHour = startTime.split(":")[0].toInt() -// -// return this.groupBy { it.second }.mapNotNull { (columnIndex, rowIndex) -> -// val date = timeEntityList.getOrNull(columnIndex - 1)?.date ?: return@mapNotNull null -// val times = rowIndex.map { row -> -// val hour = startHour + (row.first - 1) -// AvailableTimeEntity( -// startTime = "%02d:00".format(hour), -// endTime = "%02d:00".format(hour + 1) -// ) -// } -// TimeEntity(date = date, times = times) -// } -//} + LazyColumn( + modifier = modifier + .border( + width = 1.dp, + color = NoostakTheme.colors.gray200, + shape = RoundedCornerShape(8.dp) + ) + ) { + // 행 반복 + items(timeSlots + 1) { rowIndex -> + Row(modifier = Modifier.fillMaxWidth()) { + // 열 반복 + for (columnIndex in 0..days) { + val cellType = TimeTable().determineCellType(rowIndex, columnIndex) + val text = + TimeTable().getCellText(cellType, rowIndex, columnIndex, availablePeriods) + val isSelected = selectedCells.contains(rowIndex to columnIndex) + val backgroundColor = + TimeTable().getEditableBackgroundColor(cellType, isSelected) + 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 + ) + } + } + .noRippleClickable { + if (cellType == CellType.Data) { + val cell = rowIndex to columnIndex + if (selectedCells.contains(cell)) { + selectedCells.remove(cell) + } else { + selectedCells.add(cell) + } + } + }, + contentAlignment = Alignment.Center + ) { + Text( + text = text, + style = NoostakTheme.typography.c4Regular, + color = NoostakTheme.colors.gray600, + textAlign = TextAlign.Center + ) + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun NoostakEditableTimeTable1Preview() { + NoostakAndroidTheme { + val mockAvailablePeriods = PeriodEntity( + dates = listOf("2024-09-05T10:00:00", "2024-09-06T10:00:00", "2024-09-07T10:00:00"), + startTime = "2024-09-05T10:00:00", + endTime = "2024-09-07T18:00:00" + ) + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + NoostakEditableTimeTable( + availablePeriods = mockAvailablePeriods, + modifier = Modifier.fillMaxWidth() + ) + } + } +} 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 376d5667..bb2784d5 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 @@ -18,7 +18,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -40,7 +39,8 @@ fun NoostakTimeTable( modifier: Modifier = Modifier ) { val days = availablePeriods.dates.size - val timeSlots = TimeTable().calculateTimeSlots(availablePeriods.startTime, availablePeriods.endTime) + val timeSlots = + TimeTable().calculateTimeSlots(availablePeriods.startTime, availablePeriods.endTime) LazyColumn( modifier = modifier @@ -56,8 +56,15 @@ fun NoostakTimeTable( // 열 반복 for (columnIndex in 0..days) { val cellType = TimeTable().determineCellType(rowIndex, columnIndex) - val text = TimeTable().getCellText(cellType, rowIndex, columnIndex, availablePeriods) - val backgroundColor = TimeTable().getBackgroundColor(cellType, rowIndex, columnIndex, availablePeriods, availableTimes) + val text = + TimeTable().getCellText(cellType, rowIndex, columnIndex, availablePeriods) + val backgroundColor = TimeTable().getBackgroundColor( + cellType, + rowIndex, + columnIndex, + availablePeriods, + availableTimes + ) val shape = when (rowIndex to columnIndex) { 0 to 0 -> RoundedCornerShape(topStart = 8.dp) 0 to days -> RoundedCornerShape(topEnd = 8.dp) @@ -68,20 +75,25 @@ fun NoostakTimeTable( Box( modifier = when (cellType) { - CellType.Blank -> Modifier - .width(42.dp) // 고정 너비 - .height(36.dp) // 고정 높이 - CellType.TimeHeader -> Modifier - .width(42.dp) - .height(32.dp) + 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) + CellType.DateHeader -> + Modifier + .weight(1f) + .height(36.dp) - else -> Modifier - .weight(1f) // 나머지 셀은 남은 공간 비율로 채움 - .height(32.dp) + else -> + Modifier + .weight(1f) + .height(32.dp) } .background( color = backgroundColor, @@ -91,7 +103,6 @@ fun NoostakTimeTable( val borderWidth = 1.dp.toPx() val borderColor = Gray200 - // 위쪽 선 그리기 if (rowIndex > 0) { drawLine( color = borderColor, @@ -101,7 +112,6 @@ fun NoostakTimeTable( ) } - // 왼쪽 선 그리기 if (columnIndex > 0) { drawLine( color = borderColor, @@ -246,4 +256,4 @@ fun NoostakTimeTable1Preview() { ) } } -} \ 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 index 15872409..e8e9cf3c 100644 --- a/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt +++ b/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt @@ -65,6 +65,15 @@ class TimeTable { } } + @Composable + fun getEditableBackgroundColor( + cellType: CellType, + isSelected: Boolean + ): Color = when (cellType) { + CellType.Blank, CellType.DateHeader, CellType.TimeHeader -> Color.Transparent + CellType.Data -> if (isSelected) NoostakTheme.colors.blue400 else Color.Transparent + } + @Composable fun getColorByLevel(level: Int): Color = when (AvailabilityLevel.entries.firstOrNull { level in it.range }) { @@ -88,8 +97,11 @@ class TimeTable { return when (cellType) { CellType.Blank -> "\n" CellType.DateHeader -> { - if (columnIndex == 0) "" - else formatDateTimeToCustomFormat(data.dates[columnIndex - 1]) + if (columnIndex == 0) { + "" + } else { + formatDateTimeToCustomFormat(data.dates[columnIndex - 1]) + } } CellType.TimeHeader -> "${startHour + (rowIndex - 1)}시" @@ -115,4 +127,4 @@ class TimeTable { // 공통적으로 사용하는 시간만 숫자로 변환 private fun extractHour(time: String): Int = time.substringBefore(':').toInt() -} \ No newline at end of file +} diff --git a/domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt b/domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt index 2d79e8f9..1792b98c 100644 --- a/domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt +++ b/domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt @@ -18,4 +18,4 @@ data class AvailableTimeEntity( data class TimeEntity( val memberStartTime: String, val memberEndTime: String -) \ No newline at end of file +) diff --git a/domain/src/main/java/com/sopt/domain/entity/PeriodEntity.kt b/domain/src/main/java/com/sopt/domain/entity/PeriodEntity.kt index 9fc0282c..669cef97 100644 --- a/domain/src/main/java/com/sopt/domain/entity/PeriodEntity.kt +++ b/domain/src/main/java/com/sopt/domain/entity/PeriodEntity.kt @@ -4,4 +4,4 @@ data class PeriodEntity( val dates: List, val startTime: String, val endTime: String -) \ No newline at end of file +) diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt index c48296eb..93f9692b 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt @@ -10,7 +10,6 @@ import com.sopt.domain.entity.RecommendationEntity import com.sopt.domain.entity.TimeEntity import com.sopt.domain.entity.TimeTableEntity import dagger.hilt.android.lifecycle.HiltViewModel -import java.sql.Time import javax.inject.Inject @HiltViewModel @@ -152,7 +151,7 @@ class AppointmentViewModel @Inject constructor() : BaseViewModel Unit, onConfirmButtonClick: (Long, Long, String) -> Unit ) { @@ -109,20 +113,16 @@ fun AppointmentCheckScreen( ) // 타임테이블 (스크롤 가능) -// NoostakEditableTimeTable( -// data = data, -// modifier = Modifier -// .constrainAs(timeTable) { -// top.linkTo(title.bottom) -// start.linkTo(parent.start) -// end.linkTo(parent.end) -// bottom.linkTo(button.top) -// height = Dimension.fillToConstraints -// } -// ) { -// selectedData = it -// Timber.d("Selected Data: $selectedData") -// } + NoostakEditableTimeTable( + availablePeriods = availablePeriods, + modifier = Modifier + .constrainAs(timeTable) { + top.linkTo(title.bottom) + start.linkTo(parent.start) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + } + ) // 버튼 (항상 하단 고정) NoostakBottomButton( @@ -154,6 +154,7 @@ fun PreviewAppointmentConfirmScreen() { groupId = 1, appointmentsId = 1, appointmentName = "3차 회의", + availablePeriods = appointmentCheckViewModel.mockAvailablePeriods, onBackButtonClick = {}, onConfirmButtonClick = { _, _, _ -> } ) diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt index 21b0a526..4001f108 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt @@ -1,8 +1,7 @@ package com.sopt.presentation.appointment.appointmentCheck import com.sopt.core.util.BaseViewModel -import com.sopt.domain.entity.TimeEntity -import com.sopt.domain.entity.TimeTableEntity +import com.sopt.domain.entity.PeriodEntity import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -27,6 +26,11 @@ class AppointmentCheckViewModel @Inject constructor() : emitSideEffect(AppointmentCheckSideEffect.NavigateToGroupDetail(groupId)) } + val mockAvailablePeriods = PeriodEntity( + dates = listOf("2024-09-05T10:00:00", "2024-09-06T10:00:00", "2024-09-07T10:00:00"), + startTime = "2024-09-05T10:00:00", + endTime = "2024-09-05T18:00:00" + ) } sealed class AppointmentCheckSideEffect { From c46440e4c6a5c64b099e46d17ca5fb8b86746088 Mon Sep 17 00:00:00 2001 From: gaeulzzang Date: Fri, 10 Jan 2025 02:58:04 +0900 Subject: [PATCH 4/5] =?UTF-8?q?#18=20[FEAT]=20=EC=84=A0=ED=83=9D=EB=90=9C?= =?UTF-8?q?=20=EC=8B=9C=EA=B0=84=20=EC=97=94=ED=8B=B0=ED=8B=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=82=B4=EB=B3=B4=EB=82=B4=EB=8A=94=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timetable/NoostakEditableTimeTable.kt | 13 ++++++-- .../com/sopt/core/util/timetable/TimeTable.kt | 33 +++++++++++++++---- ...ilableTimeEntity.kt => TimeTableEntity.kt} | 0 .../appointmentCheck/AppointmentCheckRoute.kt | 5 ++- 4 files changed, 42 insertions(+), 9 deletions(-) rename domain/src/main/java/com/sopt/domain/entity/{AvailableTimeEntity.kt => TimeTableEntity.kt} (100%) 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 d4c2ea82..b58be53d 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 @@ -29,12 +29,14 @@ 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.PeriodEntity @Composable fun NoostakEditableTimeTable( availablePeriods: PeriodEntity, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + onSelectedTimesChanged: (List) -> Unit ) { val days = availablePeriods.dates.size val timeSlots = @@ -124,6 +126,12 @@ fun NoostakEditableTimeTable( } else { selectedCells.add(cell) } + onSelectedTimesChanged( + TimeTable().getSelectedTimes( + selectedCells, + availablePeriods + ) + ) } }, contentAlignment = Alignment.Center @@ -157,7 +165,8 @@ fun NoostakEditableTimeTable1Preview() { ) { NoostakEditableTimeTable( availablePeriods = mockAvailablePeriods, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + onSelectedTimesChanged = { } ) } } 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 index e8e9cf3c..eb46cdd8 100644 --- a/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt +++ b/core/src/main/java/com/sopt/core/util/timetable/TimeTable.kt @@ -5,7 +5,9 @@ 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.AvailableTimeEntity import com.sopt.domain.entity.PeriodEntity +import com.sopt.domain.entity.TimeEntity import com.sopt.domain.entity.TimeTableEntity import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -13,7 +15,6 @@ import java.time.format.TextStyle import java.util.Locale class TimeTable { - // 시간 차이를 계산 (24시간 형식 기준) fun calculateTimeSlots(startTime: String, endTime: String): Int { val startHour = extractHour(extractTime(startTime)) val endHour = extractHour(extractTime(endTime)) @@ -91,7 +92,6 @@ class TimeTable { columnIndex: Int, data: PeriodEntity ): String { - // 셀 텍스트 결정 로직 val startHour = extractHour(extractTime(data.startTime)) return when (cellType) { @@ -109,7 +109,31 @@ class TimeTable { } } - fun formatDateTimeToCustomFormat(dateTime: String): String { + fun getSelectedTimes( + selectedCells: List>, + availablePeriods: PeriodEntity + ): List { + val selectedTimes = mutableListOf() + val selectedCellsByDate = selectedCells.groupBy { it.second } + selectedCellsByDate.forEach { (dateColumnIndex, cells) -> + val date = availablePeriods.dates.getOrNull(dateColumnIndex - 1) ?: return@forEach + val times = cells.map { (rowIndex, _) -> + val startHour = extractHour(extractTime(availablePeriods.startTime)) + (rowIndex - 1) + val endHour = startHour + 1 + TimeEntity( + memberStartTime = "${extractDate(date)}T${String.format("%02d", startHour)}:00:00", + memberEndTime = "${extractDate(date)}T${String.format("%02d", endHour)}:00:00" + ) + } + if (times.isNotEmpty()) { + selectedTimes.add(AvailableTimeEntity(date = date, times = times)) + } + } + + return selectedTimes + } + + private fun formatDateTimeToCustomFormat(dateTime: String): String { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") val parsedDate = LocalDateTime.parse(dateTime, formatter) val dayOfWeek = parsedDate.dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.KOREAN) @@ -119,12 +143,9 @@ class TimeTable { return "$dayOfWeek\n$month/$day" } - // 공통적으로 사용하는 시간 추출 로직 private fun extractTime(dateTime: String): String = dateTime.substringAfter('T') - // 공통적으로 사용하는 날짜 추출 로직 private fun extractDate(dateTime: String): String = dateTime.substringBefore('T') - // 공통적으로 사용하는 시간만 숫자로 변환 private fun extractHour(time: String): Int = time.substringBefore(':').toInt() } diff --git a/domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt b/domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt similarity index 100% rename from domain/src/main/java/com/sopt/domain/entity/AvailableTimeEntity.kt rename to domain/src/main/java/com/sopt/domain/entity/TimeTableEntity.kt diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt index 7ec7387d..89bb152c 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt @@ -29,6 +29,7 @@ import com.sopt.core.designsystem.theme.NoostakTheme import com.sopt.domain.entity.PeriodEntity import com.sopt.domain.entity.TimeEntity import com.sopt.presentation.R +import timber.log.Timber @Composable fun AppointmentCheckRoute( @@ -122,7 +123,9 @@ fun AppointmentCheckScreen( end.linkTo(parent.end) height = Dimension.fillToConstraints } - ) + ) { + Timber.d("selectedData: $it") + } // 버튼 (항상 하단 고정) NoostakBottomButton( From 0c447d97e4bac68c88de93dc0f6d8bb533bf90ba Mon Sep 17 00:00:00 2001 From: gaeulzzang Date: Fri, 10 Jan 2025 03:22:44 +0900 Subject: [PATCH 5/5] =?UTF-8?q?#18=20[FEAT]=20=EB=8B=A4=EC=9D=B4=EC=96=BC?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20false?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/presentation/appointment/AppointmentViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt index 93f9692b..e12f7168 100644 --- a/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt +++ b/presentation/src/main/java/com/sopt/presentation/appointment/AppointmentViewModel.kt @@ -151,7 +151,7 @@ class AppointmentViewModel @Inject constructor() : BaseViewModel