Skip to content

Commit 39352bd

Browse files
authored
Merge pull request #6 from Noostak/feature/#5-duration-fix
[Feature/#5] : 소요시간 이하 입력 방지
2 parents d754978 + 8f3dc48 commit 39352bd

6 files changed

Lines changed: 185 additions & 68 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.sopt.core.designsystem.component.checkbox
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
5+
import androidx.compose.foundation.layout.PaddingValues
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.foundation.layout.wrapContentHeight
10+
import androidx.compose.foundation.shape.RoundedCornerShape
11+
import androidx.compose.material3.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.text.TextStyle
17+
import androidx.compose.ui.tooling.preview.Preview
18+
import androidx.compose.ui.unit.dp
19+
import com.sopt.core.designsystem.theme.NoostakAndroidTheme
20+
import com.sopt.core.designsystem.theme.NoostakTheme
21+
import com.sopt.core.extension.noRippleClickable
22+
23+
@Composable
24+
fun NoostakCheckbox(
25+
modifier: Modifier = Modifier,
26+
text: String = "",
27+
isChecked: Boolean = false,
28+
onCheckedChange: (Boolean) -> Unit = {},
29+
textStyle: TextStyle = NoostakTheme.typography.b4SemiBold,
30+
borderColor: Color = NoostakTheme.colors.gray500,
31+
backgroundColorChecked: Color = NoostakTheme.colors.gray50,
32+
backgroundColorUnchecked: Color = NoostakTheme.colors.white,
33+
paddingValues: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 15.dp)
34+
) {
35+
Row(
36+
modifier = modifier
37+
.fillMaxWidth()
38+
.wrapContentHeight()
39+
.border(
40+
width = 0.5.dp,
41+
color = borderColor,
42+
shape = RoundedCornerShape(10.dp)
43+
)
44+
.background(
45+
color = if (isChecked) backgroundColorChecked else backgroundColorUnchecked,
46+
shape = RoundedCornerShape(10.dp)
47+
)
48+
.noRippleClickable { onCheckedChange(!isChecked) }
49+
.padding(paddingValues),
50+
verticalAlignment = Alignment.CenterVertically
51+
) {
52+
Text(
53+
text = text,
54+
modifier = Modifier.weight(1f),
55+
style = textStyle,
56+
color = NoostakTheme.colors.gray900
57+
)
58+
CircularCheckbox(
59+
isChecked = isChecked,
60+
onCheckedChange = { onCheckedChange(it) }
61+
)
62+
}
63+
}
64+
65+
@Preview(showBackground = true)
66+
@Composable
67+
fun NoostakCheckboxPreview() {
68+
NoostakAndroidTheme {
69+
NoostakCheckbox(
70+
isChecked = true,
71+
text = "체크박스블라블라"
72+
)
73+
}
74+
}

core/src/main/java/com/sopt/core/designsystem/component/timetable/NoostakEditableTimeTable.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.sopt.domain.entity.TimeEntity
3535
fun NoostakEditableTimeTable(
3636
availablePeriods: List<TimeEntity>,
3737
modifier: Modifier = Modifier,
38+
isChecked: Boolean = false,
3839
onSelectedTimesChanged: (List<TimeEntity>) -> Unit
3940
) {
4041
val days = availablePeriods.size
@@ -43,7 +44,7 @@ fun NoostakEditableTimeTable(
4344
availablePeriods.first().startTime,
4445
availablePeriods.first().endTime
4546
)
46-
val selectedCells = remember { mutableStateListOf<Pair<Int, Int>>() }
47+
val selectedCells = remember(key1 = isChecked) { mutableStateListOf<Pair<Int, Int>>() }
4748

4849
LazyColumn(
4950
modifier = modifier

presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckRoute.kt

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import androidx.compose.animation.slideInVertically
55
import androidx.compose.animation.slideOutVertically
66
import androidx.compose.foundation.layout.Box
77
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.PaddingValues
9+
import androidx.compose.foundation.layout.Spacer
810
import androidx.compose.foundation.layout.fillMaxSize
911
import androidx.compose.foundation.layout.fillMaxWidth
12+
import androidx.compose.foundation.layout.height
1013
import androidx.compose.foundation.layout.navigationBarsPadding
1114
import androidx.compose.foundation.layout.padding
1215
import androidx.compose.foundation.layout.statusBarsPadding
@@ -34,6 +37,7 @@ import androidx.compose.ui.zIndex
3437
import androidx.hilt.navigation.compose.hiltViewModel
3538
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3639
import com.sopt.core.designsystem.component.button.NoostakBottomButton
40+
import com.sopt.core.designsystem.component.checkbox.NoostakCheckbox
3741
import com.sopt.core.designsystem.component.dialog.NoostakDialog
3842
import com.sopt.core.designsystem.component.snackbar.NoostakSnackBar
3943
import com.sopt.core.designsystem.component.snackbar.SNACK_BAR_DURATION
@@ -62,6 +66,7 @@ fun AppointmentCheckRoute(
6266
val context = LocalContext.current
6367
val showErrorDialog by appointmentCheckViewModel.showErrorDialog.collectAsStateWithLifecycle()
6468
var selectedData by remember { mutableStateOf(emptyList<TimeEntity>()) }
69+
var isChecked by remember { mutableStateOf(false) }
6570
val rememberedAvailablePeriods = remember { availablePeriods }
6671
val snackBarHostState = remember { SnackbarHostState() }
6772
val coroutineScope = rememberCoroutineScope()
@@ -99,7 +104,7 @@ fun AppointmentCheckRoute(
99104
)
100105

101106
is AppointmentCheckSideEffect.ShowSnackBar -> onShowFailureSnackBar(
102-
context.getString(sideEffect.message)
107+
context.getString(sideEffect.message, duration / 60)
103108
)
104109
}
105110
}
@@ -109,16 +114,31 @@ fun AppointmentCheckRoute(
109114
groupId = groupId,
110115
appointmentName = appointmentName,
111116
availablePeriods = rememberedAvailablePeriods,
112-
onSelectedDataChange = { selectedData = it },
117+
onSelectedDataChange = {
118+
selectedData = it
119+
if (it.isNotEmpty()) isChecked = false
120+
},
121+
duration = duration,
122+
isChecked = isChecked,
123+
onCheckedChange = {
124+
isChecked = it
125+
if (isChecked) selectedData = emptyList()
126+
},
113127
onBackButtonClick = appointmentCheckViewModel::navigateToGroupDetail,
114128
onConfirmButtonClick = {
115-
// TODO: selectedData가 duration 이하인지 체크하는 로직 추가
116-
appointmentCheckViewModel.postTimeTable(
117-
groupId,
118-
appointmentId,
119-
appointmentName,
120-
selectedData
121-
)
129+
if (appointmentCheckViewModel.isSelectedDataValid(
130+
duration / 60,
131+
selectedData,
132+
isChecked
133+
)
134+
) {
135+
appointmentCheckViewModel.postTimeTable(
136+
groupId,
137+
appointmentId,
138+
appointmentName,
139+
selectedData
140+
)
141+
}
122142
},
123143
snackBarHostState = snackBarHostState,
124144
snackBarVisible = snackBarVisible
@@ -149,6 +169,9 @@ fun AppointmentCheckScreen(
149169
appointmentName: String,
150170
availablePeriods: List<TimeEntity>,
151171
onSelectedDataChange: (List<TimeEntity>) -> Unit = {},
172+
duration: Long,
173+
isChecked: Boolean = false,
174+
onCheckedChange: (Boolean) -> Unit = {},
152175
onBackButtonClick: (Long) -> Unit,
153176
onConfirmButtonClick: () -> Unit,
154177
snackBarHostState: SnackbarHostState,
@@ -199,15 +222,27 @@ fun AppointmentCheckScreen(
199222
) {
200223
Text(
201224
modifier = Modifier
202-
.padding(top = 11.dp, start = 6.dp, bottom = 16.dp),
203-
text = stringResource(R.string.title_appointment_check),
225+
.padding(vertical = 12.dp),
226+
text = stringResource(R.string.title_appointment_check, duration / 60),
204227
color = NoostakTheme.colors.black,
205228
style = NoostakTheme.typography.h4Bold,
206229
textAlign = TextAlign.Start
207230
)
231+
NoostakCheckbox(
232+
text = stringResource(R.string.cb_appointment_check_impossible),
233+
isChecked = isChecked,
234+
onCheckedChange = {
235+
onCheckedChange(it)
236+
},
237+
textStyle = NoostakTheme.typography.b5Regular,
238+
borderColor = NoostakTheme.colors.gray200,
239+
paddingValues = PaddingValues(horizontal = 12.dp, vertical = 14.dp)
240+
)
241+
Spacer(modifier = Modifier.height(16.dp))
208242
NoostakEditableTimeTable(
209243
availablePeriods = availablePeriods,
210-
modifier = Modifier.fillMaxWidth()
244+
modifier = Modifier.fillMaxWidth(),
245+
isChecked = isChecked
211246
) {
212247
onSelectedDataChange(it)
213248
Timber.d("selectedData: $it")
@@ -251,6 +286,7 @@ fun PreviewAppointmentConfirmScreen() {
251286
endTime = "2024-09-07T18:00:00"
252287
)
253288
),
289+
duration = 360,
254290
onBackButtonClick = {},
255291
onConfirmButtonClick = {},
256292
snackBarHostState = SnackbarHostState(),

presentation/src/main/java/com/sopt/presentation/appointment/appointmentCheck/AppointmentCheckViewModel.kt

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import com.sopt.core.type.DialogType
66
import com.sopt.core.util.BaseViewModel
77
import com.sopt.domain.entity.TimeEntity
88
import com.sopt.domain.repository.AppointmentConfirmRepository
9+
import com.sopt.presentation.R
910
import dagger.hilt.android.lifecycle.HiltViewModel
1011
import kotlinx.coroutines.flow.MutableStateFlow
1112
import kotlinx.coroutines.flow.StateFlow
1213
import kotlinx.coroutines.flow.asStateFlow
1314
import kotlinx.coroutines.flow.update
1415
import kotlinx.coroutines.launch
1516
import java.io.IOException
17+
import java.time.Duration
18+
import java.time.LocalDateTime
1619
import javax.inject.Inject
1720

1821
@HiltViewModel
@@ -71,6 +74,46 @@ class AppointmentCheckViewModel @Inject constructor(
7174
}
7275
}
7376

77+
fun isSelectedDataValid(duration: Long, selectedDate: List<TimeEntity>, isChecked: Boolean): Boolean {
78+
if (isChecked) {
79+
return true
80+
}
81+
82+
if (selectedDate.isEmpty()) {
83+
emitSideEffect(AppointmentCheckSideEffect.ShowSnackBar(R.string.sb_appointment_check_invalid))
84+
return false
85+
}
86+
87+
val sorted = selectedDate.sortedBy { LocalDateTime.parse(it.startTime) }
88+
var currentStart = LocalDateTime.parse(sorted.first().startTime)
89+
var currentEnd = LocalDateTime.parse(sorted.first().endTime)
90+
91+
for (i in 1 until sorted.size) {
92+
val nextStart = LocalDateTime.parse(sorted[i].startTime)
93+
val nextEnd = LocalDateTime.parse(sorted[i].endTime)
94+
95+
if (nextStart == currentEnd) {
96+
currentEnd = nextEnd
97+
} else {
98+
// 블록이 끊기면 검사
99+
val blockDuration = Duration.between(currentStart, currentEnd).toHours()
100+
if (blockDuration < duration) {
101+
emitSideEffect(AppointmentCheckSideEffect.ShowSnackBar(R.string.sb_appointment_check_invalid))
102+
return false
103+
}
104+
currentStart = nextStart
105+
currentEnd = nextEnd
106+
}
107+
}
108+
val finalBlockDuration = Duration.between(currentStart, currentEnd).toHours()
109+
if (finalBlockDuration < duration) {
110+
emitSideEffect(AppointmentCheckSideEffect.ShowSnackBar(R.string.sb_appointment_check_invalid))
111+
return false
112+
}
113+
114+
return true
115+
}
116+
74117
fun showErrorDialog(show: Boolean, dialogType: DialogType) {
75118
_showErrorDialog.update { it.copy(first = show, second = dialogType) }
76119
}
@@ -79,16 +122,6 @@ class AppointmentCheckViewModel @Inject constructor(
79122
emitSideEffect(AppointmentCheckSideEffect.NavigateUp)
80123
}
81124

82-
fun navigateToAppointment(groupId: Long, appointmentId: Long, appointmentName: String) {
83-
emitSideEffect(
84-
AppointmentCheckSideEffect.NavigateToAppointment(
85-
groupId,
86-
appointmentId,
87-
appointmentName
88-
)
89-
)
90-
}
91-
92125
fun navigateToGroupDetail(groupId: Long) {
93126
emitSideEffect(AppointmentCheckSideEffect.NavigateToGroupDetail(groupId))
94127
}

presentation/src/main/java/com/sopt/presentation/appointmentCreate/appointmentCreateTimePicker/AppointmentCreateTimePickerRoute.kt

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import androidx.compose.animation.AnimatedVisibility
44
import androidx.compose.animation.slideInVertically
55
import androidx.compose.animation.slideOutVertically
66
import androidx.compose.foundation.Image
7-
import androidx.compose.foundation.background
8-
import androidx.compose.foundation.border
97
import androidx.compose.foundation.layout.Box
108
import androidx.compose.foundation.layout.Column
119
import androidx.compose.foundation.layout.Row
@@ -17,11 +15,9 @@ import androidx.compose.foundation.layout.navigationBarsPadding
1715
import androidx.compose.foundation.layout.padding
1816
import androidx.compose.foundation.layout.size
1917
import androidx.compose.foundation.layout.statusBarsPadding
20-
import androidx.compose.foundation.shape.RoundedCornerShape
2118
import androidx.compose.material3.Scaffold
2219
import androidx.compose.material3.SnackbarHost
2320
import androidx.compose.material3.SnackbarHostState
24-
import androidx.compose.material3.Text
2521
import androidx.compose.runtime.Composable
2622
import androidx.compose.runtime.LaunchedEffect
2723
import androidx.compose.runtime.MutableState
@@ -41,7 +37,7 @@ import androidx.compose.ui.tooling.preview.Preview
4137
import androidx.compose.ui.unit.dp
4238
import androidx.hilt.navigation.compose.hiltViewModel
4339
import com.sopt.core.designsystem.component.button.NoostakBottomButton
44-
import com.sopt.core.designsystem.component.checkbox.CircularCheckbox
40+
import com.sopt.core.designsystem.component.checkbox.NoostakCheckbox
4541
import com.sopt.core.designsystem.component.progressbar.NoostakProgressBar
4642
import com.sopt.core.designsystem.component.snackbar.NoostakSnackBar
4743
import com.sopt.core.designsystem.component.snackbar.SNACK_BAR_DURATION
@@ -51,7 +47,6 @@ import com.sopt.core.designsystem.component.timepicker.NoostakTimePicker
5147
import com.sopt.core.designsystem.component.topappbar.NoostakTopAppBar
5248
import com.sopt.core.designsystem.theme.NoostakAndroidTheme
5349
import com.sopt.core.designsystem.theme.NoostakTheme
54-
import com.sopt.core.extension.noRippleClickable
5550
import com.sopt.presentation.R
5651
import kotlinx.coroutines.delay
5752
import kotlinx.coroutines.launch
@@ -190,45 +185,19 @@ fun AppointmentCreateTimePickerScreen(
190185
Spacer(modifier = Modifier.height(18.dp))
191186
NoostakProgressBar(progressBar = listOf(false, false, true))
192187
NoostakHeaderText(text = stringResource(R.string.text_calendar_appointment_time_choose))
193-
Row(
194-
modifier = Modifier
195-
.fillMaxWidth()
196-
.padding(top = 18.dp)
197-
.height(54.dp)
198-
.border(
199-
width = 0.5.dp,
200-
color = NoostakTheme.colors.gray500,
201-
shape = RoundedCornerShape(10.dp)
202-
)
203-
.background(
204-
color = if (isChecked) NoostakTheme.colors.gray50 else NoostakTheme.colors.white,
205-
shape = RoundedCornerShape(10.dp)
206-
)
207-
.noRippleClickable {
208-
isChecked = !isChecked
209-
showPicker = !isChecked
210-
}
211-
.padding(horizontal = 12.dp, vertical = 15.dp),
212-
verticalAlignment = Alignment.CenterVertically
213-
) {
214-
Text(
215-
text = stringResource(R.string.text_calendar_appointment_time_select),
216-
modifier = Modifier.weight(1f),
217-
style = NoostakTheme.typography.b4SemiBold,
218-
color = NoostakTheme.colors.gray900
219-
)
220-
CircularCheckbox(
221-
isChecked = isChecked,
222-
onCheckedChange = {
223-
isChecked = it
224-
showPicker = !it
225-
if (isChecked) {
226-
selectedStartHour = null
227-
selectedEndHour = null
228-
}
188+
NoostakCheckbox(
189+
modifier = Modifier.padding(top = 18.dp),
190+
text = stringResource(R.string.text_calendar_appointment_time_select),
191+
isChecked = isChecked,
192+
onCheckedChange = {
193+
isChecked = it
194+
showPicker = !isChecked
195+
if (isChecked) {
196+
selectedStartHour = null
197+
selectedEndHour = null
229198
}
230-
)
231-
}
199+
}
200+
)
232201
Row(
233202
modifier = Modifier
234203
.fillMaxWidth()

0 commit comments

Comments
 (0)