diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1e00da893..847e091b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -178,6 +178,7 @@ dependencies { implementation(libs.coil.core) implementation(libs.profileinstaller) implementation(libs.firebase.messaging.lifecycle.ktx) + implementation(libs.kotlin.collections.immutable) } secrets { diff --git a/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt b/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt index 31daba16c..2ed6b2d57 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt @@ -19,8 +19,7 @@ class NewAttendanceViewModel @Inject constructor( fetchData() } - private val _uiState: MutableStateFlow = - MutableStateFlow(AttendanceUiState.Loading) + private val _uiState: MutableStateFlow = MutableStateFlow(AttendanceUiState.Loading) val uiState: StateFlow = _uiState private var fakeTitle: String = "" diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceCodeDialog.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceCodeDialog.kt new file mode 100644 index 000000000..e8ece72ff --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceCodeDialog.kt @@ -0,0 +1,155 @@ +package org.sopt.official.feature.attendance.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import org.sopt.official.R +import org.sopt.official.designsystem.Black40 +import org.sopt.official.designsystem.Gray60 +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.feature.attendance.compose.component.AttendanceCodeCardList +import org.sopt.official.feature.attendance.model.AttendanceType + +@Composable +fun AttendanceCodeDialog( + codes: ImmutableList, + inputCodes: ImmutableList, + attendanceType: AttendanceType, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + Dialog(onDismissRequest = onDismissRequest) { + Column( + modifier + .background( + color = SoptTheme.colors.onSurface700, + shape = RoundedCornerShape(size = 10.dp) + ) + .padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = stringResource(id = R.string.close), + tint = SoptTheme.colors.onSurface10, + modifier = Modifier + .align(Alignment.End) + .clickable(onClick = onDismissRequest) + ) + Text( + text = stringResource(R.string.attendance_do, attendanceType.type), + style = SoptTheme.typography.heading18B, + color = SoptTheme.colors.onSurface10 + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(R.string.attendance_code_description), + style = SoptTheme.typography.body13M, + color = SoptTheme.colors.onSurface300 + ) + Spacer(modifier = Modifier.height(24.dp)) + AttendanceCodeCardList( + codes = inputCodes, + onTextChange = {}, + onTextFieldFull = {}, + ) + if (codes != inputCodes) { + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = stringResource(R.string.attendance_code_does_not_match), + style = SoptTheme.typography.label12SB, + color = SoptTheme.colors.error + ) + } + Spacer(modifier = Modifier.height(32.dp)) + Button( + onClick = { /*TODO*/ }, + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(size = 6.dp), + colors = ButtonColors( + containerColor = SoptTheme.colors.onSurface10, + contentColor = SoptTheme.colors.onSurface950, + disabledContainerColor = Black40, + disabledContentColor = Gray60, + ), + enabled = codes == inputCodes + ) { + Text( + text = stringResource(R.string.attendance_dialog_button), + style = SoptTheme.typography.body13M, + ) + } + } + } +} + +@Preview +@Composable +private fun AttendanceCodeDialogPreview( + @PreviewParameter(AttendanceCodeDialogPreviewParameterProvider::class) parameter: AttendanceCodeDialogPreviewParameter, +) { + SoptTheme { + AttendanceCodeDialog( + codes = parameter.codes, + inputCodes = parameter.inputCodes, + attendanceType = parameter.attendanceType, + modifier = Modifier.fillMaxWidth(), + onDismissRequest = {} + ) + } +} + +data class AttendanceCodeDialogPreviewParameter( + val codes: ImmutableList, + val inputCodes: ImmutableList, + val attendanceType: AttendanceType, +) + +class AttendanceCodeDialogPreviewParameterProvider : + PreviewParameterProvider { + override val values: Sequence = + sequenceOf( + AttendanceCodeDialogPreviewParameter( + codes = persistentListOf("1", "2", "3", "4", "5"), + inputCodes = persistentListOf("1", "2", "3", null, null), + AttendanceType.FIRST, + ), + AttendanceCodeDialogPreviewParameter( + codes = persistentListOf("1", "2", "3", "4", "5"), + inputCodes = persistentListOf("1", "2", "3", "4", "5"), + AttendanceType.FIRST, + ), + AttendanceCodeDialogPreviewParameter( + codes = persistentListOf("1", "2", "3", "4", "5"), + inputCodes = persistentListOf("1", "2", "3", null, null), + AttendanceType.SECOND, + ), + AttendanceCodeDialogPreviewParameter( + codes = persistentListOf("1", "2", "3", "4", "5"), + inputCodes = persistentListOf("1", "2", "3", "4", "5"), + AttendanceType.SECOND, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt index a86c6e6e1..97e18ea45 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt @@ -3,7 +3,10 @@ package org.sopt.official.feature.attendance.compose import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -15,6 +18,7 @@ import org.sopt.official.feature.attendance.compose.component.AttendanceTopAppBa import org.sopt.official.feature.attendance.model.AttendanceAction import org.sopt.official.feature.attendance.model.AttendanceUiState +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AttendanceRoute(onClickBackIcon: () -> Unit) { val viewModel: NewAttendanceViewModel = viewModel() @@ -27,7 +31,7 @@ fun AttendanceRoute(onClickBackIcon: () -> Unit) { onClickBackIcon = onClickBackIcon, onClickRefreshIcon = viewModel::fetchData, ) - }, + } ) { innerPaddingValues -> Column( modifier = Modifier diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCard.kt new file mode 100644 index 000000000..c7eb86b7b --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCard.kt @@ -0,0 +1,68 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import org.sopt.official.designsystem.SoptTheme + +@Composable +fun AttendanceCodeCard( + text: String, + onTextChange: (String) -> Unit, + onTextFieldFull: () -> Unit, + modifier: Modifier = Modifier, + textMaxLength: Int = 1, +) { + BasicTextField( + value = text, + onValueChange = { newText: String -> + if (newText.length < textMaxLength) { + onTextChange(newText) + } else { + onTextFieldFull() + } + }, + modifier = modifier + .background( + color = if (text.isEmpty()) SoptTheme.colors.onSurface600 + else SoptTheme.colors.onSurface800, + shape = RoundedCornerShape(8.dp) + ) + .border( + width = 1.dp, + color = if (text.isEmpty()) SoptTheme.colors.onSurface500 + else SoptTheme.colors.primary, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 17.dp, vertical = 18.dp) + .width(10.dp), + textStyle = SoptTheme.typography.heading16B.copy(color = SoptTheme.colors.primary) + ) +} + +@Preview +@Composable +private fun AttendanceCodeCardPreview( + @PreviewParameter(AttendanceCodeCardPreviewParameterProvider::class) text: String, +) { + SoptTheme { + AttendanceCodeCard( + text = text, + onTextChange = {}, + onTextFieldFull = {} + ) + } +} + +private class AttendanceCodeCardPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf("", "8") +} diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCardList.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCardList.kt new file mode 100644 index 000000000..b7e442c7f --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCardList.kt @@ -0,0 +1,42 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.designsystem.SoptTheme + +@Composable +fun AttendanceCodeCardList( + codes: List, + onTextChange: (newText: String) -> Unit, + onTextFieldFull: () -> Unit, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + repeat(codes.size) { index -> + AttendanceCodeCard( + text = codes[index] ?: "", + onTextChange = onTextChange, + onTextFieldFull = onTextFieldFull + ) + if (index < codes.size) { + Spacer(modifier = Modifier.width(width = 12.dp)) + } + } + } +} + +@Preview +@Composable +private fun AttendanceCodeCardListPreview() { + SoptTheme { + AttendanceCodeCardList( + codes = listOf("8", "8", "8", null, null), + onTextChange = {}, + onTextFieldFull = {}) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e620642f..12e16e67e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ SOPT를 알차게 즐기고 싶다면? SOPT-AMP! 콕 찌르기 + 닫기 안녕하세요,\nSOPT의 열정이 되어주세요! @@ -102,6 +103,9 @@ 뒤로 가기 새로고침 출석 조회하기 + %1$s하기 + 출석 코드 다섯 자리를 입력해주세요. + 코드가 일치하지 않아요! 알림 diff --git a/core/designsystem/src/main/java/org/sopt/official/designsystem/Color.kt b/core/designsystem/src/main/java/org/sopt/official/designsystem/Color.kt index 3e2bd0b50..bc991c545 100644 --- a/core/designsystem/src/main/java/org/sopt/official/designsystem/Color.kt +++ b/core/designsystem/src/main/java/org/sopt/official/designsystem/Color.kt @@ -40,6 +40,7 @@ val White = Color(0xFFFFFFFF) val Black = Color(0xFF000000) val Black80 = Color(0xFF1C1D1E) val Black60 = Color(0xFF2C2D2E) +val Black40 = Color(0xFF3C3D40) val Gray950 = Color(0xFF0F1012) val Gray900 = Color(0xFF17181C) val Gray800 = Color(0xFF202025) diff --git a/feature/soptamp/src/main/java/org/sopt/official/stamp/designsystem/style/Theme.kt b/feature/soptamp/src/main/java/org/sopt/official/stamp/designsystem/style/Theme.kt index c391a03fd..840c7c041 100644 --- a/feature/soptamp/src/main/java/org/sopt/official/stamp/designsystem/style/Theme.kt +++ b/feature/soptamp/src/main/java/org/sopt/official/stamp/designsystem/style/Theme.kt @@ -66,7 +66,7 @@ class SoptColors( onSurface20: Color, onSurface10: Color, onSurface5: Color, - isLight: Boolean + isLight: Boolean, ) { var white by mutableStateOf(white) private set @@ -217,7 +217,7 @@ fun soptLightColors( onSurface30: Color = Gray300, onSurface20: Color = Gray200, onSurface10: Color = Gray100, - onSurface5: Color = Gray50 + onSurface5: Color = Gray50, ) = SoptColors( white, black, @@ -278,7 +278,7 @@ fun soptDarkColors( onSurface30: Color = Gray300, onSurface20: Color = Gray200, onSurface10: Color = Gray100, - onSurface5: Color = Gray50 + onSurface5: Color = Gray50, ) = SoptColors( white, black, @@ -325,14 +325,18 @@ private val LocalSoptTypography = staticCompositionLocalOf { * Color에 접근하고 싶을때 SoptTheme.colors.primary 이런식으로 접근하면 됩니다. * Typo를 변경하고 싶다면 SoptTheme.typography.h1 이런식으로 접근하면 됩니다. * */ -object SoptTheme { +internal object SoptTheme { val colors: SoptColors @Composable get() = LocalSoptColors.current val typography: SoptTypography @Composable get() = LocalSoptTypography.current } @Composable -fun ProvideSoptColorsAndTypography(colors: SoptColors, typography: SoptTypography, content: @Composable () -> Unit) { +fun ProvideSoptColorsAndTypography( + colors: SoptColors, + typography: SoptTypography, + content: @Composable () -> Unit, +) { val provideColors = remember { colors.copy() } provideColors.update(colors) val provideTypography = remember { typography.copy() }