Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 체크박스 컴포넌트로 따로 빼놓은거 너무 좋습니다!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 : 히히 체크박스도 생겻다 감사함니두

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.sopt.core.designsystem.component.checkbox

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
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.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.sopt.core.designsystem.theme.NoostakAndroidTheme
import com.sopt.core.designsystem.theme.NoostakTheme
import com.sopt.core.extension.noRippleClickable

@Composable
fun NoostakCheckbox(
modifier: Modifier = Modifier,
text: String = "",
isChecked: Boolean = false,
onCheckedChange: (Boolean) -> Unit = {},
textStyle: TextStyle = NoostakTheme.typography.b4SemiBold,
borderColor: Color = NoostakTheme.colors.gray500,
backgroundColorChecked: Color = NoostakTheme.colors.gray50,
backgroundColorUnchecked: Color = NoostakTheme.colors.white,
paddingValues: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 15.dp)
) {
Row(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
.border(
width = 0.5.dp,
color = borderColor,
shape = RoundedCornerShape(10.dp)
)
.background(
color = if (isChecked) backgroundColorChecked else backgroundColorUnchecked,
shape = RoundedCornerShape(10.dp)
)
.noRippleClickable { onCheckedChange(!isChecked) }
.padding(paddingValues),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = text,
modifier = Modifier.weight(1f),
style = textStyle,
color = NoostakTheme.colors.gray900
)
CircularCheckbox(
isChecked = isChecked,
onCheckedChange = { onCheckedChange(it) }
)
}
}

@Preview(showBackground = true)
@Composable
fun NoostakCheckboxPreview() {
NoostakAndroidTheme {
NoostakCheckbox(
isChecked = true,
text = "체크박스블라블라"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 이거 뭔디 ㅋㅋㅋㅋㅋ

)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import com.sopt.domain.entity.TimeEntity
fun NoostakEditableTimeTable(
availablePeriods: List<TimeEntity>,
modifier: Modifier = Modifier,
isChecked: Boolean = false,
onSelectedTimesChanged: (List<TimeEntity>) -> Unit
) {
val days = availablePeriods.size
Expand All @@ -43,7 +44,7 @@ fun NoostakEditableTimeTable(
availablePeriods.first().startTime,
availablePeriods.first().endTime
)
val selectedCells = remember { mutableStateListOf<Pair<Int, Int>>() }
val selectedCells = remember(key1 = isChecked) { mutableStateListOf<Pair<Int, Int>>() }

LazyColumn(
modifier = modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
Expand Down Expand Up @@ -34,6 +37,7 @@ import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.sopt.core.designsystem.component.button.NoostakBottomButton
import com.sopt.core.designsystem.component.checkbox.NoostakCheckbox
import com.sopt.core.designsystem.component.dialog.NoostakDialog
import com.sopt.core.designsystem.component.snackbar.NoostakSnackBar
import com.sopt.core.designsystem.component.snackbar.SNACK_BAR_DURATION
Expand Down Expand Up @@ -62,6 +66,7 @@ fun AppointmentCheckRoute(
val context = LocalContext.current
val showErrorDialog by appointmentCheckViewModel.showErrorDialog.collectAsStateWithLifecycle()
var selectedData by remember { mutableStateOf(emptyList<TimeEntity>()) }
var isChecked by remember { mutableStateOf(false) }
val rememberedAvailablePeriods = remember { availablePeriods }
val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
Expand Down Expand Up @@ -99,7 +104,7 @@ fun AppointmentCheckRoute(
)

is AppointmentCheckSideEffect.ShowSnackBar -> onShowFailureSnackBar(
context.getString(sideEffect.message)
context.getString(sideEffect.message, duration / 60)
)
}
}
Expand All @@ -109,16 +114,31 @@ fun AppointmentCheckRoute(
groupId = groupId,
appointmentName = appointmentName,
availablePeriods = rememberedAvailablePeriods,
onSelectedDataChange = { selectedData = it },
onSelectedDataChange = {
selectedData = it
if (it.isNotEmpty()) isChecked = false
},
duration = duration,
isChecked = isChecked,
onCheckedChange = {
isChecked = it
if (isChecked) selectedData = emptyList()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 오호 체크 박스 선택 시 이렇게 초기화했구나! 베리굿!

},
onBackButtonClick = appointmentCheckViewModel::navigateToGroupDetail,
onConfirmButtonClick = {
// TODO: selectedData가 duration 이하인지 체크하는 로직 추가
appointmentCheckViewModel.postTimeTable(
groupId,
appointmentId,
appointmentName,
selectedData
)
if (appointmentCheckViewModel.isSelectedDataValid(
duration / 60,
selectedData,
isChecked
)
) {
appointmentCheckViewModel.postTimeTable(
groupId,
appointmentId,
appointmentName,
selectedData
)
}
},
snackBarHostState = snackBarHostState,
snackBarVisible = snackBarVisible
Expand Down Expand Up @@ -149,6 +169,9 @@ fun AppointmentCheckScreen(
appointmentName: String,
availablePeriods: List<TimeEntity>,
onSelectedDataChange: (List<TimeEntity>) -> Unit = {},
duration: Long,
isChecked: Boolean = false,
onCheckedChange: (Boolean) -> Unit = {},
onBackButtonClick: (Long) -> Unit,
onConfirmButtonClick: () -> Unit,
snackBarHostState: SnackbarHostState,
Expand Down Expand Up @@ -199,15 +222,27 @@ fun AppointmentCheckScreen(
) {
Text(
modifier = Modifier
.padding(top = 11.dp, start = 6.dp, bottom = 16.dp),
text = stringResource(R.string.title_appointment_check),
.padding(vertical = 12.dp),
text = stringResource(R.string.title_appointment_check, duration / 60),
color = NoostakTheme.colors.black,
style = NoostakTheme.typography.h4Bold,
textAlign = TextAlign.Start
)
NoostakCheckbox(
text = stringResource(R.string.cb_appointment_check_impossible),
isChecked = isChecked,
onCheckedChange = {
onCheckedChange(it)
},
textStyle = NoostakTheme.typography.b5Regular,
borderColor = NoostakTheme.colors.gray200,
paddingValues = PaddingValues(horizontal = 12.dp, vertical = 14.dp)
)
Spacer(modifier = Modifier.height(16.dp))
NoostakEditableTimeTable(
availablePeriods = availablePeriods,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
isChecked = isChecked
) {
onSelectedDataChange(it)
Timber.d("selectedData: $it")
Expand Down Expand Up @@ -251,6 +286,7 @@ fun PreviewAppointmentConfirmScreen() {
endTime = "2024-09-07T18:00:00"
)
),
duration = 360,
onBackButtonClick = {},
onConfirmButtonClick = {},
snackBarHostState = SnackbarHostState(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import com.sopt.core.type.DialogType
import com.sopt.core.util.BaseViewModel
import com.sopt.domain.entity.TimeEntity
import com.sopt.domain.repository.AppointmentConfirmRepository
import com.sopt.presentation.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.io.IOException
import java.time.Duration
import java.time.LocalDateTime
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -71,6 +74,46 @@ class AppointmentCheckViewModel @Inject constructor(
}
}

fun isSelectedDataValid(duration: Long, selectedDate: List<TimeEntity>, isChecked: Boolean): Boolean {
if (isChecked) {
return true
}

if (selectedDate.isEmpty()) {
emitSideEffect(AppointmentCheckSideEffect.ShowSnackBar(R.string.sb_appointment_check_invalid))
return false
}

val sorted = selectedDate.sortedBy { LocalDateTime.parse(it.startTime) }
Copy link
Contributor

@youjin09222 youjin09222 Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 시간 정렬하는 방법 너무 좋은디!

var currentStart = LocalDateTime.parse(sorted.first().startTime)
var currentEnd = LocalDateTime.parse(sorted.first().endTime)

for (i in 1 until sorted.size) {
val nextStart = LocalDateTime.parse(sorted[i].startTime)
val nextEnd = LocalDateTime.parse(sorted[i].endTime)

if (nextStart == currentEnd) {
currentEnd = nextEnd
} else {
// 블록이 끊기면 검사
val blockDuration = Duration.between(currentStart, currentEnd).toHours()
if (blockDuration < duration) {
emitSideEffect(AppointmentCheckSideEffect.ShowSnackBar(R.string.sb_appointment_check_invalid))
return false
}
currentStart = nextStart
currentEnd = nextEnd
}
}
Comment on lines +91 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 오 이렇게 연속 여부 판단했구나 대단한디?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지피티는 정말 대단한 친구야 흙흙

val finalBlockDuration = Duration.between(currentStart, currentEnd).toHours()
if (finalBlockDuration < duration) {
emitSideEffect(AppointmentCheckSideEffect.ShowSnackBar(R.string.sb_appointment_check_invalid))
return false
}
Comment on lines +108 to +112
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: 마지막 블록까지 꼼꼼히 체크한 거 너무 좋습니다!


return true
}

fun showErrorDialog(show: Boolean, dialogType: DialogType) {
_showErrorDialog.update { it.copy(first = show, second = dialogType) }
}
Expand All @@ -79,16 +122,6 @@ class AppointmentCheckViewModel @Inject constructor(
emitSideEffect(AppointmentCheckSideEffect.NavigateUp)
}

fun navigateToAppointment(groupId: Long, appointmentId: Long, appointmentName: String) {
emitSideEffect(
AppointmentCheckSideEffect.NavigateToAppointment(
groupId,
appointmentId,
appointmentName
)
)
}

fun navigateToGroupDetail(groupId: Long) {
emitSideEffect(AppointmentCheckSideEffect.NavigateToGroupDetail(groupId))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -17,11 +15,9 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
Expand All @@ -41,7 +37,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.sopt.core.designsystem.component.button.NoostakBottomButton
import com.sopt.core.designsystem.component.checkbox.CircularCheckbox
import com.sopt.core.designsystem.component.checkbox.NoostakCheckbox
import com.sopt.core.designsystem.component.progressbar.NoostakProgressBar
import com.sopt.core.designsystem.component.snackbar.NoostakSnackBar
import com.sopt.core.designsystem.component.snackbar.SNACK_BAR_DURATION
Expand All @@ -51,7 +47,6 @@ import com.sopt.core.designsystem.component.timepicker.NoostakTimePicker
import com.sopt.core.designsystem.component.topappbar.NoostakTopAppBar
import com.sopt.core.designsystem.theme.NoostakAndroidTheme
import com.sopt.core.designsystem.theme.NoostakTheme
import com.sopt.core.extension.noRippleClickable
import com.sopt.presentation.R
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -190,45 +185,19 @@ fun AppointmentCreateTimePickerScreen(
Spacer(modifier = Modifier.height(18.dp))
NoostakProgressBar(progressBar = listOf(false, false, true))
NoostakHeaderText(text = stringResource(R.string.text_calendar_appointment_time_choose))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 18.dp)
.height(54.dp)
.border(
width = 0.5.dp,
color = NoostakTheme.colors.gray500,
shape = RoundedCornerShape(10.dp)
)
.background(
color = if (isChecked) NoostakTheme.colors.gray50 else NoostakTheme.colors.white,
shape = RoundedCornerShape(10.dp)
)
.noRippleClickable {
isChecked = !isChecked
showPicker = !isChecked
}
.padding(horizontal = 12.dp, vertical = 15.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.text_calendar_appointment_time_select),
modifier = Modifier.weight(1f),
style = NoostakTheme.typography.b4SemiBold,
color = NoostakTheme.colors.gray900
)
CircularCheckbox(
isChecked = isChecked,
onCheckedChange = {
isChecked = it
showPicker = !it
if (isChecked) {
selectedStartHour = null
selectedEndHour = null
}
NoostakCheckbox(
modifier = Modifier.padding(top = 18.dp),
text = stringResource(R.string.text_calendar_appointment_time_select),
isChecked = isChecked,
onCheckedChange = {
isChecked = it
showPicker = !isChecked
if (isChecked) {
selectedStartHour = null
selectedEndHour = null
}
)
}
}
)
Row(
modifier = Modifier
.fillMaxWidth()
Expand Down
Loading