diff --git a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Dialog.kt b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Dialog.kt index 7a4b72cb..53f0e788 100644 --- a/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Dialog.kt +++ b/core/designsystem/src/main/java/com/goalpanzi/mission_mate/core/designsystem/component/Dialog.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -38,7 +39,7 @@ import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography fun MissionMateDialog( @StringRes titleId: Int, onDismissRequest: () -> Unit, - onClickOk : () -> Unit, + onClickOk: () -> Unit, modifier: Modifier = Modifier, @StringRes descriptionId: Int? = null, @StringRes okTextId: Int? = null, @@ -97,7 +98,8 @@ fun MissionMateDialog( if (cancelTextId != null) { Text( - modifier = Modifier.padding(top = 20.dp) + modifier = Modifier + .padding(top = 20.dp) .clickable( interactionSource = null, indication = null, @@ -117,6 +119,7 @@ fun MissionMateDialog( @Composable fun MissionMateDialog( onDismissRequest: () -> Unit, + onClickOk: () -> Unit, modifier: Modifier = Modifier, @StringRes okTextId: Int? = null, @StringRes cancelTextId: Int? = null, @@ -129,7 +132,7 @@ fun MissionMateDialog( start = 24.dp, end = 24.dp ), - content: @Composable () -> Unit + content: @Composable ColumnScope.() -> Unit ) { Dialog( properties = DialogProperties( @@ -146,25 +149,37 @@ fun MissionMateDialog( horizontalAlignment = Alignment.CenterHorizontally ) { content() - if (okTextId != null) { - MissionMateTextButton( - modifier = Modifier - .fillMaxWidth(), - textId = okTextId, - textStyle = okTextStyle, - onClick = {} - ) - } - if (cancelTextId != null) { - Text( - modifier = Modifier.padding(top = 20.dp), - text = stringResource(id = cancelTextId), - style = cancelTextStyle, - textAlign = TextAlign.Center, - color = ColorGray3_FF727484 - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (okTextId != null) { + MissionMateTextButton( + modifier = Modifier + .fillMaxWidth(), + textId = okTextId, + textStyle = okTextStyle, + onClick = onClickOk + ) + } + + if (cancelTextId != null) { + Text( + modifier = Modifier + .padding(top = 20.dp) + .clickable( + interactionSource = null, + indication = null, + onClick = onDismissRequest + ), + text = stringResource(id = cancelTextId), + style = cancelTextStyle, + textAlign = TextAlign.Center, + color = ColorGray3_FF727484 + ) + } } + } } } @@ -199,7 +214,8 @@ fun PreviewMissionMateContentDialog() { modifier = Modifier.fillMaxWidth(), okTextId = R.string.app_name, cancelTextId = R.string.app_name, - onDismissRequest = {} + onDismissRequest = {}, + onClickOk = {} ) { Spacer( modifier = Modifier diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt index 06e27874..2f501796 100644 --- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt +++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavHost.kt @@ -52,7 +52,11 @@ internal fun MainNavHost( } ) - invitationCodeNavGraph() + invitationCodeNavGraph( + onBackClick = { + navigator.popBackStack() + } + ) profileNavGraph( onSaveSuccess = { navigator.navigationToOnboarding() } ) diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt index 4d067059..106c4059 100644 --- a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingNavigation.kt @@ -8,6 +8,7 @@ import com.goalpanzi.mission_mate.core.navigation.RouteModel import com.goalpanzi.mission_mate.feature.onboarding.screen.OnboardingRoute import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupRoute import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupSuccessScreen +import com.goalpanzi.mission_mate.feature.onboarding.screen.invitation.InvitationCodeRoute fun NavController.navigateToOnboarding() { this.navigate(RouteModel.Onboarding) @@ -61,7 +62,12 @@ fun NavGraphBuilder.boardSetupSuccessNavGraph( } } -fun NavGraphBuilder.invitationCodeNavGraph() { +fun NavGraphBuilder.invitationCodeNavGraph( + onBackClick: () -> Unit +) { composable { + InvitationCodeRoute( + onBackClick = onBackClick + ) } } \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt index 046ab23d..2839ed87 100644 --- a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt @@ -40,7 +40,7 @@ fun BoardSetupNavigationBar( ) } Row( - modifier = Modifier.padding(16.dp), + modifier = Modifier.padding(start = 16.dp,bottom = 16.dp, end = 24.dp), verticalAlignment = Alignment.CenterVertically ) { Text( diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/InvitationCodeTextField.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/InvitationCodeTextField.kt new file mode 100644 index 00000000..2e5a16d3 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/InvitationCodeTextField.kt @@ -0,0 +1,113 @@ +package com.goalpanzi.mission_mate.feature.onboarding.component + +import androidx.compose.foundation.BorderStroke +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_80F5F6F9 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorRed_FFFF5858 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography + +@Composable +fun InvitationCodeTextField( + text: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + hint: String? = null, + isError: Boolean = false, + textStyle: TextStyle = MissionMateTypography.heading_md_bold, + hintStyle: TextStyle = MissionMateTypography.heading_md_bold, + textColor: Color = ColorGray1_FF404249, + hintColor: Color = ColorGray3_FF727484, + containerColor: Color = ColorWhite_FFFFFFFF, + unfocusedHintColor: Color = ColorGray5_80F5F6F9, + borderStroke: BorderStroke = BorderStroke(1.dp, ColorGray5_80F5F6F9), + focusedBorderStroke: BorderStroke = BorderStroke(1.dp, ColorGray4_FFE5E5E5), + errorBorderStroke: BorderStroke = BorderStroke(2.dp, ColorRed_FFFF5858), + shape: Shape = RoundedCornerShape(12.dp), + isSingleLine: Boolean = true, + visualTransformation: VisualTransformation = VisualTransformation.None, + textAlign: Alignment = Alignment.Center, + contentPadding: PaddingValues = PaddingValues(), + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + var isFocused by remember { mutableStateOf(false) } + BasicTextField( + modifier = modifier + .onFocusChanged { + isFocused = it.isFocused + }, + value = text, + singleLine = isSingleLine, + textStyle = textStyle.copy( + color = textColor, + textAlign = TextAlign.Center, + lineHeight = 40.sp + ), + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + onValueChange = onValueChange, + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .clip(shape) + .border( + border = if (isError) errorBorderStroke + else if (isFocused || text.isNotEmpty()) focusedBorderStroke + else borderStroke, + shape = shape + ) + .background( + if (!isFocused && text.isEmpty()) unfocusedHintColor + else containerColor + ) + .padding(contentPadding), + contentAlignment = textAlign + ) { + if (text.isBlank() && !isFocused) { + Text( + modifier = Modifier.fillMaxWidth(), + text = hint ?: "0", + style = hintStyle, + color = hintColor, + textAlign = TextAlign.Center + ) + } + innerTextField() + } + + } + ) +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/CodeResultType.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/CodeResultType.kt new file mode 100644 index 00000000..5f9cdd96 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/CodeResultType.kt @@ -0,0 +1,6 @@ +package com.goalpanzi.mission_mate.feature.onboarding.model + +sealed class CodeResultEvent { + data class Success(val mission : MissionUiModel) : CodeResultEvent() + data object Error : CodeResultEvent() +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/MissionUiModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/MissionUiModel.kt new file mode 100644 index 00000000..4604b4c9 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/MissionUiModel.kt @@ -0,0 +1,8 @@ +package com.goalpanzi.mission_mate.feature.onboarding.model + +data class MissionUiModel( + val missionTitle : String, + val missionPeriod : String, + val missionDays : String, + val missionTime : String +) diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeScreen.kt new file mode 100644 index 00000000..47bcefa2 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeScreen.kt @@ -0,0 +1,267 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.invitation + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +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.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextButton +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorRed_FFFF5858 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorWhite_FFFFFFFF +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.feature.onboarding.R +import com.goalpanzi.mission_mate.feature.onboarding.component.InvitationCodeTextField +import com.goalpanzi.mission_mate.feature.onboarding.model.CodeResultEvent +import com.goalpanzi.mission_mate.feature.onboarding.model.MissionUiModel +import com.goalpanzi.mission_mate.feature.onboarding.screen.invitation.InvitationCodeViewModel.Companion.CodeActionEvent +import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun InvitationCodeRoute( + onBackClick : () -> Unit, + viewModel: InvitationCodeViewModel = hiltViewModel() +) { + val keyboardController = LocalSoftwareKeyboardController.current + val localFocusManager = LocalFocusManager.current + + val isNotCodeValid by viewModel.isNotCodeValid.collectAsStateWithLifecycle() + val enabledButton by viewModel.enabledButton.collectAsStateWithLifecycle() + + var hasInvitationDialogData by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = Unit) { + launch { + viewModel.codeInputActionEvent.collect { + when (it) { + CodeActionEvent.FIRST_DONE, + CodeActionEvent.SECOND_DONE, + CodeActionEvent.THIRD_DONE -> { + delay(80) + localFocusManager.moveFocus(FocusDirection.Next) + } + else -> { + + } + } + } + } + + launch { + viewModel.codeResultEvent.collect { result -> + when(result){ + is CodeResultEvent.Success -> { + hasInvitationDialogData = result.mission + } + is CodeResultEvent.Error -> { + + } + } + } + } + } + hasInvitationDialogData?.let { mission -> + InvitationDialog( + count = 13, + missionTitle = mission.missionTitle, + missionPeriod = mission.missionPeriod, + missionDays = mission.missionDays, + missionTime = mission.missionTime, + onDismissRequest = { + hasInvitationDialogData = null + }, + onClickOk = { + hasInvitationDialogData = null + } + ) + } + InvitationCodeScreen( + codeFirst = viewModel.codeFirst, + codeSecond = viewModel.codeSecond, + codeThird = viewModel.codeThird, + codeFourth = viewModel.codeFourth, + onCodeFirstChange = viewModel::updateCodeFirst, + onCodeSecondChange = viewModel::updateCodeSecond, + onCodeThirdChange = viewModel::updateCodeThird, + onCodeFourthChange = viewModel::updateCodeFourth, + onClickButton = { + keyboardController?.hide() + localFocusManager.clearFocus() + viewModel.checkCode() + }, + onBackClick = onBackClick, + isNotCodeValid = isNotCodeValid, + enabledButton = enabledButton + ) +} + +@Composable +fun InvitationCodeScreen( + codeFirst: String, + codeSecond: String, + codeThird: String, + codeFourth: String, + onCodeFirstChange: (String) -> Unit, + onCodeSecondChange: (String) -> Unit, + onCodeThirdChange: (String) -> Unit, + onCodeFourthChange: (String) -> Unit, + onClickButton : () -> Unit, + onBackClick : () -> Unit, + isNotCodeValid : Boolean, + enabledButton: Boolean, + modifier: Modifier = Modifier, + scrollState: ScrollState = rememberScrollState() +) { + Column( + modifier = modifier + .fillMaxSize() + .background(ColorWhite_FFFFFFFF) + .verticalScroll(scrollState) + .imePadding() + .statusBarsPadding() + .navigationBarsPadding() + ) { + IconButton( + modifier = Modifier.padding(start = 4.dp), + onClick = onBackClick + ) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Default.KeyboardArrowLeft, // merge 전까지 임시 사용 + contentDescription = null + ) + } + Text( + text = stringResource(id = R.string.onboarding_invitation_title), + modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 22.dp), + style = MissionMateTypography.heading_sm_bold, + color = ColorGray1_FF404249 + ) + + Text( + text = styledTextWithHighlights( + text = stringResource(id = R.string.onboarding_invitation_description), + colorTargetTexts = listOf(stringResource(id = R.string.onboarding_invitation_description_color_target)), + textColor = ColorGray2_FF4F505C, + targetTextColor = ColorOrange_FFFF5732, + targetFontWeight = FontWeight.Bold, + ), + modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 54.dp), + style = MissionMateTypography.title_xl_regular, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .wrapContentHeight(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(18.dp) + ) { + InvitationCodeTextField( + modifier = Modifier + .weight(1f) + .aspectRatio(1f), + text = codeFirst, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + isError = isNotCodeValid, + onValueChange = onCodeFirstChange + ) + InvitationCodeTextField( + modifier = Modifier + .weight(1f) + .aspectRatio(1f), + text = codeSecond, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + isError = isNotCodeValid, + onValueChange = onCodeSecondChange + ) + InvitationCodeTextField( + modifier = Modifier + .weight(1f) + .aspectRatio(1f), + text = codeThird, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + isError = isNotCodeValid, + onValueChange = onCodeThirdChange + ) + InvitationCodeTextField( + modifier = Modifier + .weight(1f) + .aspectRatio(1f), + text = codeFourth, + isError = isNotCodeValid, + onValueChange = onCodeFourthChange + ) + } + if(isNotCodeValid){ + val set = mutableSetOf() + set.forEach { + return@forEach + } + Text( + modifier = Modifier.padding(top = 12.dp, start = 24.dp, end = 24.dp ), + text = stringResource(id = R.string.onboarding_invitation_error), + style = MissionMateTypography.body_md_regular, + color = ColorRed_FFFF5858 + ) + } + + Spacer(modifier = Modifier.weight(1f)) + MissionMateTextButton( + modifier = Modifier + .padding(vertical = 36.dp, horizontal = 24.dp) + .fillMaxWidth() + .wrapContentHeight(), + buttonType = if (enabledButton) MissionMateButtonType.ACTIVE else MissionMateButtonType.DISABLED, + textId = R.string.confirm, + onClick = onClickButton + ) + } + +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeViewModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeViewModel.kt new file mode 100644 index 00000000..0bbb7fe7 --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationCodeViewModel.kt @@ -0,0 +1,146 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.invitation + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.goalpanzi.mission_mate.feature.onboarding.model.CodeResultEvent +import com.goalpanzi.mission_mate.feature.onboarding.model.MissionUiModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class InvitationCodeViewModel @Inject constructor( + +) : ViewModel() { + + var codeFirst by mutableStateOf("") + private set + + private val isNotCodeFirstEmpty = + snapshotFlow { codeFirst } + + var codeSecond by mutableStateOf("") + private set + + private val isNotCodeSecondEmpty = + snapshotFlow { codeSecond } + + var codeThird by mutableStateOf("") + private set + + private val isNotCodeThirdEmpty = + snapshotFlow { codeThird } + + var codeFourth by mutableStateOf("") + private set + + private val isNotCodeFourthEmpty = + snapshotFlow { codeFourth } + + private val _isNotCodeValid = MutableStateFlow(false) + val isNotCodeValid: StateFlow = _isNotCodeValid.asStateFlow() + + val enabledButton: StateFlow = + combine( + isNotCodeFirstEmpty.map { it.isNotEmpty() }, + isNotCodeSecondEmpty.map { it.isNotEmpty() }, + isNotCodeThirdEmpty.map { it.isNotEmpty() }, + isNotCodeFourthEmpty.map { it.isNotEmpty() }, + isNotCodeValid + ) { isNotFirstEmpty, isNotSecondEmpty, isNotThirdEmpty, isNotFourthEmpty, isNotValid -> + isNotFirstEmpty && isNotSecondEmpty && isNotThirdEmpty && isNotFourthEmpty && !isNotValid + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(500), + initialValue = false + ) + + val codeInputActionEvent: SharedFlow = + merge( + isNotCodeFirstEmpty.filterNot { it.isEmpty() }.map { CodeActionEvent.FIRST_DONE }, + isNotCodeSecondEmpty.filterNot { it.isEmpty() }.map { CodeActionEvent.SECOND_DONE }, + isNotCodeThirdEmpty.filterNot { it.isEmpty() }.map { CodeActionEvent.THIRD_DONE } + ).shareIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(500) + ) + + + private val _codeResultEvent = MutableSharedFlow() + val codeResultEvent: SharedFlow = _codeResultEvent.asSharedFlow() + + fun updateCodeFirst(code: String) { + if (isNotCodeValid.value) resetCodeValidState() + if (code.length <= 1) codeFirst = code + } + + fun updateCodeSecond(code: String) { + if (isNotCodeValid.value) resetCodeValidState() + if (code.length <= 1) codeSecond = code + } + + fun updateCodeThird(code: String) { + if (isNotCodeValid.value) resetCodeValidState() + if (code.length <= 1) codeThird = code + } + + fun updateCodeFourth(code: String) { + if (isNotCodeValid.value) resetCodeValidState() + if (code.length <= 1) codeFourth = code + } + + fun checkCode() { + viewModelScope.launch { + delay(200) + // 아래는 모두 삭제 예정 + if (isNotCodeValid.value == false) { + _codeResultEvent.emit( + CodeResultEvent.Success( + MissionUiModel( + "유산소 1시간", + "08.15~09.15", + "월/수/금", + "오전 00~12시" + ) + ) + ) + } else { + _isNotCodeValid.emit(isNotCodeValid.value.not()) + } + + } + } + + private fun resetCodeValidState() { + viewModelScope.launch { + _isNotCodeValid.emit(false) + } + } + + companion object { + enum class CodeActionEvent { + FIRST_DONE, + SECOND_DONE, + THIRD_DONE, + FOURTH_DONE, + } + } +} \ No newline at end of file diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationDialog.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationDialog.kt new file mode 100644 index 00000000..3246901e --- /dev/null +++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/invitation/InvitationDialog.kt @@ -0,0 +1,167 @@ +package com.goalpanzi.mission_mate.feature.onboarding.screen.invitation + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +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.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateDialog +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray2_FF4F505C +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray3_FF727484 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray4_FFE5E5E5 +import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732 +import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography +import com.goalpanzi.mission_mate.feature.onboarding.R +import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights + +@Composable +fun InvitationDialog( + count : Int, + missionTitle : String, + missionPeriod : String, + missionDays : String, + missionTime : String, + onDismissRequest: () -> Unit, + onClickOk: () -> Unit, + modifier: Modifier = Modifier, + titleStyle: TextStyle = MissionMateTypography.title_xl_bold, + descriptionStyle: TextStyle = MissionMateTypography.body_lg_regular, + okTextStyle: TextStyle = MissionMateTypography.body_lg_bold, + cancelTextStyle: TextStyle = MissionMateTypography.body_lg_bold +){ + val scrollState = rememberScrollState() + + MissionMateDialog( + modifier = modifier, + onDismissRequest = onDismissRequest, + onClickOk = onClickOk, + okTextId = R.string.check_ok, + cancelTextId = R.string.check_no, + okTextStyle = okTextStyle, + cancelTextStyle = cancelTextStyle + ){ + Column( + modifier = Modifier.weight(1f,false).padding(bottom = 29.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(id = R.string.onboarding_invitation_dialog_title), + style = titleStyle, + textAlign = TextAlign.Center, + color = ColorGray1_FF404249 + ) + Text( + modifier = Modifier.padding(top = 4.dp, bottom = 20.dp), + text = styledTextWithHighlights( + text = stringResource(id = R.string.onboarding_invitation_dialog_description,count), + colorTargetTexts = listOf(stringResource(id = R.string.onboarding_invitation_dialog_description_color_target,count)), + targetTextColor = ColorOrange_FFFF5732, + textColor = ColorGray2_FF4F505C + ), + style = descriptionStyle, + textAlign = TextAlign.Center, + color = ColorGray2_FF4F505C + ) + Column( + modifier = Modifier.verticalScroll(scrollState), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text ( + modifier = Modifier.align(Alignment.Start), + text = stringResource(id = R.string.onboarding_board_setup_mission_input_title), + color = ColorGray3_FF727484, + style = MissionMateTypography.body_md_regular + ) + Text( + modifier = Modifier.align(Alignment.Start), + text = missionTitle, + color = ColorGray1_FF404249, + style = MissionMateTypography.body_lg_bold + ) + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = ColorGray4_FFE5E5E5 + ) + + Text ( + modifier = Modifier.align(Alignment.Start), + text = stringResource(id = R.string.onboarding_board_setup_schedule_period_input_title), + color = ColorGray3_FF727484, + style = MissionMateTypography.body_md_regular + ) + Text( + modifier = Modifier.align(Alignment.Start), + text = missionPeriod, + color = ColorGray1_FF404249, + style = MissionMateTypography.body_lg_bold + ) + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = ColorGray4_FFE5E5E5 + ) + + Text ( + modifier = Modifier.align(Alignment.Start), + text = stringResource(id = R.string.onboarding_invitation_dialog_schedule_day_title), + color = ColorGray3_FF727484, + style = MissionMateTypography.body_md_regular + ) + Text( + modifier = Modifier.align(Alignment.Start), + text = missionDays, + color = ColorGray1_FF404249, + style = MissionMateTypography.body_lg_bold + ) + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp), + color = ColorGray4_FFE5E5E5 + ) + + Text ( + modifier = Modifier.align(Alignment.Start), + text = stringResource(id = R.string.onboarding_board_setup_verification_time_input_title), + color = ColorGray3_FF727484, + style = MissionMateTypography.body_md_regular + ) + Text( + modifier = Modifier.align(Alignment.Start), + text = missionTime, + color = ColorGray1_FF404249, + style = MissionMateTypography.body_lg_bold + ) + } + + } + } +} + +@Preview +@Composable +fun PreviewInvitationDialog(){ + InvitationDialog( + count = 12, + modifier = Modifier.fillMaxWidth(), + missionTitle = "매일 유산소 1시간", + missionPeriod = "2024.07.24~2024.08.14", + missionDays = "월/수/목", + missionTime = "오전 00~12시", + onDismissRequest = {}, + onClickOk = {} + + ) +} \ No newline at end of file diff --git a/feature/onboarding/src/main/res/values/strings.xml b/feature/onboarding/src/main/res/values/strings.xml index e43246be..3107d8ea 100644 --- a/feature/onboarding/src/main/res/values/strings.xml +++ b/feature/onboarding/src/main/res/values/strings.xml @@ -39,8 +39,22 @@ 목표가 완성되었어요! 꾸준히 미션를 완수해\n세계 곳곳을 경험해봐요! + 초대코드 입력 + 친구에게 전송받은\n초대코드 4자리를 입력해주세요! + 초대코드 4자리 + 알맞지 않은 초대코드 입니다! 다시 확인해주세요. + 초대받은 경쟁이 맞나요? + *기간 대비 인증 요일을 계산해\n인증횟수(보드판 수)는 총 %d개 가\n생성되었어요. + 인증횟수(보드판 수)는 총 %d개 + 인증 요일 + 다음 완성 시작하기 + 확인 + + 맞아요 + 아니에요 + \ No newline at end of file