diff --git a/core/designsystem/src/main/res/drawable/image_jeju_success.png b/core/designsystem/src/main/res/drawable/image_jeju_success.png
new file mode 100644
index 00000000..8a8b8651
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/image_jeju_success.png differ
diff --git a/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png b/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png
new file mode 100644
index 00000000..71af63aa
Binary files /dev/null and b/core/designsystem/src/main/res/drawable/image_onboarding_jeju.png differ
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
index 30298fab..4757ff2b 100644
--- a/core/designsystem/src/main/res/values/strings.xml
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -1,3 +1,12 @@
mission-mate
+
+ 월
+ 화
+ 수
+ 목
+ 금
+ 토
+ 일
+
\ No newline at end of file
diff --git a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt
index 7b36c68e..6708b8bd 100644
--- a/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt
+++ b/core/navigation/src/main/java/com/goalpanzi/mission_mate/core/navigation/RouteModel.kt
@@ -14,6 +14,9 @@ sealed interface OnboardingRouteModel {
@Serializable
data object BoardSetup : OnboardingRouteModel
+ @Serializable
+ data object BoardSetupSuccess : OnboardingRouteModel
+
@Serializable
data object InvitationCode : OnboardingRouteModel
}
\ No newline at end of file
diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml
index 703653c4..5d913de9 100644
--- a/feature/main/src/main/AndroidManifest.xml
+++ b/feature/main/src/main/AndroidManifest.xml
@@ -4,7 +4,8 @@
+ android:theme="@style/Theme.Missionmate"
+ android:windowSoftInputMode="adjustResize">
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 fc0cb642..12156545 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
@@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.navigation.compose.NavHost
import com.goalpanzi.mission_mate.feature.login.loginNavGraph
import com.goalpanzi.mission_mate.feature.onboarding.boardSetupNavGraph
+import com.goalpanzi.mission_mate.feature.onboarding.boardSetupSuccessNavGraph
import com.goalpanzi.mission_mate.feature.onboarding.invitationCodeNavGraph
import com.goalpanzi.mission_mate.feature.onboarding.onboardingNavGraph
@@ -32,11 +33,23 @@ internal fun MainNavHost(
onBackClick = { navigator.popBackStack() }
)
onboardingNavGraph(
- onClickBoardSetup = { },
- onClickInvitationCode = { },
+ onClickBoardSetup = { navigator.navigationToBoardSetup() },
+ onClickInvitationCode = { navigator.navigationToInvitationCode() },
onClickSetting = { }
)
- boardSetupNavGraph()
+ boardSetupNavGraph(
+ onSuccess = {
+ navigator.navigationToBoardSetupSuccess()
+ },
+ onBackClick = {
+ navigator.popBackStack()
+ }
+ )
+ boardSetupSuccessNavGraph(
+ onClickStart = {
+
+ }
+ )
invitationCodeNavGraph()
}
}
diff --git a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt
index 7e4ca198..8b24a29e 100644
--- a/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt
+++ b/feature/main/src/main/java/com/goalpanzi/mission_mate/core/main/component/MainNavigator.kt
@@ -7,6 +7,7 @@ import androidx.navigation.compose.rememberNavController
import com.goalpanzi.mission_mate.core.navigation.RouteModel
import com.goalpanzi.mission_mate.feature.login.navigateToLogin
import com.goalpanzi.mission_mate.feature.onboarding.navigateToBoardSetup
+import com.goalpanzi.mission_mate.feature.onboarding.navigateToBoardSetupSuccess
import com.goalpanzi.mission_mate.feature.onboarding.navigateToInvitationCode
import com.goalpanzi.mission_mate.feature.onboarding.navigateToOnboarding
@@ -33,6 +34,10 @@ class MainNavigator(
navController.navigateToBoardSetup()
}
+ fun navigationToBoardSetupSuccess() {
+ navController.navigateToBoardSetupSuccess()
+ }
+
fun navigationToInvitationCode() {
navController.navigateToInvitationCode()
}
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 6d29deb4..4d067059 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
@@ -5,6 +5,9 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.goalpanzi.mission_mate.core.navigation.OnboardingRouteModel
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
fun NavController.navigateToOnboarding() {
this.navigate(RouteModel.Onboarding)
@@ -14,14 +17,18 @@ fun NavController.navigateToBoardSetup() {
this.navigate(OnboardingRouteModel.BoardSetup)
}
+fun NavController.navigateToBoardSetupSuccess() {
+ this.navigate(OnboardingRouteModel.BoardSetupSuccess)
+}
+
fun NavController.navigateToInvitationCode() {
this.navigate(OnboardingRouteModel.InvitationCode)
}
fun NavGraphBuilder.onboardingNavGraph(
- onClickBoardSetup : () -> Unit,
- onClickInvitationCode : () -> Unit,
- onClickSetting : () -> Unit
+ onClickBoardSetup: () -> Unit,
+ onClickInvitationCode: () -> Unit,
+ onClickSetting: () -> Unit
) {
composable {
OnboardingRoute(
@@ -32,14 +39,29 @@ fun NavGraphBuilder.onboardingNavGraph(
}
}
-fun NavGraphBuilder.boardSetupNavGraph() {
+fun NavGraphBuilder.boardSetupNavGraph(
+ onSuccess: () -> Unit,
+ onBackClick: () -> Unit
+) {
composable {
+ BoardSetupRoute(
+ onSuccess = onSuccess,
+ onBackClick = onBackClick
+ )
+ }
+}
+fun NavGraphBuilder.boardSetupSuccessNavGraph(
+ onClickStart: () -> Unit
+) {
+ composable {
+ BoardSetupSuccessScreen(
+ onClickStart = onClickStart
+ )
}
}
fun NavGraphBuilder.invitationCodeNavGraph() {
composable {
-
}
}
\ 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
new file mode 100644
index 00000000..046ab23d
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/BoardSetupNavigationBar.kt
@@ -0,0 +1,67 @@
+package com.goalpanzi.mission_mate.feature.onboarding.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+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.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
+import com.goalpanzi.mission_mate.core.designsystem.theme.MissionMateTypography
+import com.goalpanzi.mission_mate.feature.onboarding.R
+
+@Composable
+fun BoardSetupNavigationBar(
+ onBackClick: () -> Unit,
+ currentStep: () -> Int,
+ modifier: Modifier = Modifier,
+ maxStep: Int = 3,
+) {
+ Column(
+ modifier = modifier
+ ) {
+ IconButton(
+ modifier = Modifier.padding(start = 4.dp),
+ onClick = onBackClick
+ ) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ imageVector = Icons.Default.KeyboardArrowLeft, // merge 전까지 임시 사용
+ contentDescription = null
+ )
+ }
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ text = stringResource(
+ id = when (currentStep()) {
+ 1 -> R.string.onboarding_board_setup_mission_title
+ 2 -> R.string.onboarding_board_setup_schedule_title
+ else -> R.string.onboarding_board_setup_verification_time_title
+ }
+ ),
+ modifier = Modifier
+ .wrapContentHeight()
+ .weight(1f)
+ .padding(start = 8.dp, end = 8.dp),
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray1_FF404249
+ )
+ OutlinedTextBox(
+ text = "${currentStep()}/$maxStep",
+ textStyle = MissionMateTypography.body_lg_regular
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt
new file mode 100644
index 00000000..960f71b4
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/component/DatePickerDialog.kt
@@ -0,0 +1,75 @@
+package com.goalpanzi.mission_mate.feature.onboarding.component
+
+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.material3.DatePicker
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.SelectableDates
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.rememberDatePickerState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButton
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateButtonType
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.localDateToMillis
+import java.time.LocalDate
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DatePickerDialog(
+ selectedDate: LocalDate?,
+ selectableStartDate: LocalDate?,
+ selectableEndDate: LocalDate?,
+ onSuccess: (Long) -> Unit,
+ onDismiss: () -> Unit
+) {
+ val datePickerState = rememberDatePickerState(
+ initialSelectedDateMillis = selectedDate?.let { localDateToMillis(it) },
+ selectableDates = object : SelectableDates {
+ override fun isSelectableDate(utcTimeMillis: Long): Boolean {
+ val startMillis = localDateToMillis(selectableStartDate)
+ val endMillis = localDateToMillis(selectableEndDate)
+ val currentMillis = utcTimeMillis
+ return (startMillis ?: Long.MIN_VALUE) <= currentMillis && (endMillis
+ ?: Long.MAX_VALUE) >= currentMillis
+ }
+ }
+ )
+ Dialog(
+ properties = DialogProperties(usePlatformDefaultWidth = false),
+ onDismissRequest = onDismiss
+ ) {
+ Surface(
+ modifier = Modifier
+ .padding(horizontal = 4.dp, vertical = 12.dp)
+ .fillMaxWidth()
+ ) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ DatePicker(state = datePickerState)
+ MissionMateButton(
+ buttonType = if (datePickerState.selectedDateMillis == null) MissionMateButtonType.DISABLED
+ else MissionMateButtonType.SECONDARY,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 12.dp),
+ onClick = {
+ datePickerState.selectedDateMillis?.let { onSuccess(it) }
+ }
+ ) {
+ Text(text = "Ok")
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt
new file mode 100644
index 00000000..f042ca25
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/model/VerificationTimeType.kt
@@ -0,0 +1,12 @@
+package com.goalpanzi.mission_mate.feature.onboarding.model
+
+import androidx.annotation.StringRes
+import com.goalpanzi.mission_mate.feature.onboarding.R
+
+enum class VerificationTimeType(
+ @StringRes val titleId : Int
+) {
+ Am(R.string.onboarding_board_setup_verification_time_input_content_am),
+ Pm(R.string.onboarding_board_setup_verification_time_input_content_pm),
+ All(R.string.onboarding_board_setup_verification_time_input_content_all)
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt
similarity index 95%
rename from feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingScreen.kt
rename to feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt
index b9acaf1e..7e2a2b85 100644
--- a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/OnboardingScreen.kt
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/OnboardingScreen.kt
@@ -1,4 +1,4 @@
-package com.goalpanzi.mission_mate.feature.onboarding
+package com.goalpanzi.mission_mate.feature.onboarding.screen
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -9,6 +9,7 @@ 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.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -24,6 +25,7 @@ import androidx.compose.ui.unit.dp
import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray1_FF404249
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.OnboardingNavigationButton
import com.goalpanzi.mission_mate.feature.onboarding.component.OutlinedTextBox
import com.goalpanzi.mission_mate.feature.onboarding.component.StableImage
@@ -63,12 +65,12 @@ fun OnboardingScreen(
contentScale = ContentScale.FillWidth
)
Column(
- modifier = modifier,
+ modifier = modifier.statusBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
IconButton(
modifier = Modifier
- .padding(end = 10.dp, top = 24.dp)
+ .padding(end = 10.dp)
.align(Alignment.End),
onClick = onClickSetting
) {
@@ -85,7 +87,7 @@ fun OnboardingScreen(
color = ColorGray1_FF404249
)
OutlinedTextBox(
- text = stringResource(id = R.string.onboarding_level_1),
+ text = stringResource(id = R.string.onboarding_level_1),
modifier = Modifier.padding(bottom = 23.dp)
)
Box(
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt
new file mode 100644
index 00000000..10642630
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupMission.kt
@@ -0,0 +1,43 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.component.MissionMateTextFieldGroup
+import com.goalpanzi.mission_mate.feature.onboarding.R
+
+@Composable
+fun BoardSetupMission(
+ missionTitle : String,
+ onTitleChange : (String) -> Unit,
+ modifier: Modifier = Modifier
+){
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp)
+ ) {
+ BoardSetupDescription(
+ text = stringResource(id = R.string.onboarding_board_setup_mission_description),
+ colorTargetTexts = listOf(
+ stringResource(R.string.onboarding_board_setup_mission_description_color_target1),
+ stringResource(R.string.onboarding_board_setup_mission_description_color_target2)
+ )
+ )
+ MissionMateTextFieldGroup(
+ modifier = modifier.fillMaxWidth(),
+ text = missionTitle,
+ onValueChange = onTitleChange,
+ useMaxLength = true,
+ maxLength = 12,
+ titleId = R.string.onboarding_board_setup_mission_input_title,
+ hintId = R.string.onboarding_board_setup_mission_input_hint,
+ guidanceId = R.string.onboarding_board_setup_mission_input_guide,
+ )
+ }
+}
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt
new file mode 100644
index 00000000..36915ced
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSchedule.kt
@@ -0,0 +1,228 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+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.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorBlack_FF000000
+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.ColorGray5_80F5F6F9
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorGray5_FFF5F6F9
+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.util.getStringId
+import java.time.DayOfWeek
+
+@Composable
+fun BoardSetupSchedule(
+ startDate: String,
+ endDate: String,
+ selectedDays: List,
+ enabledDaysOfWeek : Set,
+ count : String,
+ onClickStartDate: () -> Unit,
+ onClickEndDate: () -> Unit,
+ onSelectDay: (DayOfWeek) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp)
+ ) {
+ BoardSetupDescription(
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_description,count),
+ colorTargetTexts = listOf(
+ stringResource(R.string.onboarding_board_setup_schedule_description_color_target1),
+ stringResource(R.string.onboarding_board_setup_schedule_description_color_target2)
+ ),
+ count = count + stringResource(id = R.string.onboarding_board_setup_schedule_description_style_target)
+ )
+ Period(
+ startDate = startDate,
+ endDate = endDate,
+ modifier = Modifier.fillMaxWidth(),
+ onClickStartDate = onClickStartDate,
+ onClickEndDate = onClickEndDate
+ )
+ Frequency(
+ modifier = Modifier
+ .padding(top = 40.dp)
+ .fillMaxWidth(),
+ enabledDaysOfWeek = enabledDaysOfWeek,
+ selectedDays = selectedDays,
+ onClickDay = onSelectDay
+ )
+ }
+}
+
+@Composable
+fun Period(
+ startDate: String,
+ endDate: String,
+ modifier: Modifier = Modifier,
+ onClickStartDate: () -> Unit,
+ onClickEndDate: () -> Unit
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_period_input_title),
+ style = MissionMateTypography.body_md_bold,
+ color = ColorGray3_FF727484
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Button(
+ modifier = Modifier
+ .height(60.dp)
+ .weight(1f),
+ border = if(startDate.isBlank()) null else BorderStroke(1.dp, ColorGray4_FFE5E5E5) ,
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if(startDate.isBlank()) ColorGray5_80F5F6F9 else ColorWhite_FFFFFFFF
+ ),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ onClick = onClickStartDate
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = startDate.ifBlank { stringResource(id = R.string.onboarding_board_setup_schedule_period_start_hint) },
+ color = if(startDate.isBlank()) ColorGray3_FF727484 else ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_regular
+ )
+ }
+ Text(
+ modifier = Modifier.padding(horizontal = 7.dp),
+ text = "~",
+ color = ColorBlack_FF000000,
+ style = MissionMateTypography.body_lg_regular
+ )
+ Button(
+ modifier = Modifier
+ .height(60.dp)
+ .weight(1f),
+ border = if(endDate.isBlank()) null else BorderStroke(1.dp, ColorGray4_FFE5E5E5) ,
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if(endDate.isBlank()) ColorGray5_80F5F6F9 else ColorWhite_FFFFFFFF
+ ),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ onClick = onClickEndDate
+ ) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = endDate.ifBlank { stringResource(id = R.string.onboarding_board_setup_schedule_period_end_hint) },
+ color = if(endDate.isBlank()) ColorGray3_FF727484 else ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_regular
+ )
+ }
+ }
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_period_input_guide),
+ style = MissionMateTypography.body_md_regular,
+ color = ColorGray2_FF4F505C
+ )
+ }
+}
+
+@Composable
+fun Frequency(
+ selectedDays: List,
+ enabledDaysOfWeek : Set,
+ onClickDay: (DayOfWeek) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier.alpha(
+ if(enabledDaysOfWeek.isNotEmpty()) 1f else 0.3f
+ ),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Text(
+ modifier = Modifier.padding(bottom = 4.dp),
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_day_input_title),
+ style = MissionMateTypography.body_md_bold,
+ color = ColorGray3_FF727484
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.5.dp)
+ ) {
+ DayOfWeek.entries.forEach {
+ DayItem(
+ modifier = Modifier
+ .weight(1f)
+ .aspectRatio(1f),
+ dayOfWeek = it,
+ enabled = enabledDaysOfWeek.contains(it),
+ selected = it in selectedDays,
+ onClick = {
+ onClickDay(it)
+ }
+ )
+ }
+ }
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_schedule_day_input_guide),
+ style = MissionMateTypography.body_md_regular,
+ color = ColorGray2_FF4F505C
+ )
+ }
+}
+
+
+@Composable
+fun DayItem(
+ dayOfWeek : DayOfWeek,
+ enabled : Boolean,
+ onClick : () -> Unit,
+ modifier: Modifier = Modifier,
+ shape: Shape = CircleShape,
+ selected : Boolean = false
+){
+ TextButton(
+ modifier = modifier,
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ shape = shape,
+ colors = ButtonDefaults.textButtonColors(
+ containerColor = if(selected) ColorGray1_FF404249 else ColorGray5_FFF5F6F9,
+ disabledContainerColor = ColorGray5_FFF5F6F9.copy(0.3f)
+ ),
+ enabled = enabled,
+ onClick = onClick,
+ ) {
+ Text(
+ text = stringResource(id = dayOfWeek.getStringId()),
+ color = if (selected) ColorWhite_FFFFFFFF
+ else ColorGray1_FF404249,
+ style = MissionMateTypography.body_lg_regular
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt
new file mode 100644
index 00000000..0b9ddc4e
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupScreen.kt
@@ -0,0 +1,271 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material3.ExperimentalMaterial3Api
+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.Modifier
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.text.font.FontWeight
+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.ColorGray2_FF4F505C
+import com.goalpanzi.mission_mate.core.designsystem.theme.ColorOrange_FFFF5732
+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.BoardSetupNavigationBar
+import com.goalpanzi.mission_mate.feature.onboarding.component.DatePickerDialog
+import com.goalpanzi.mission_mate.feature.onboarding.model.VerificationTimeType
+import com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup.BoardSetupViewModel.Companion.BoardSetupStep
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.dateToString
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.filterDatesByDayOfWeek
+import com.goalpanzi.mission_mate.feature.onboarding.util.styledTextWithHighlights
+import java.time.DayOfWeek
+import java.time.LocalDate
+
+@Composable
+fun BoardSetupRoute(
+ onSuccess : () -> Unit,
+ onBackClick: () -> Unit,
+ viewModel: BoardSetupViewModel = hiltViewModel()
+) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val localFocusManager = LocalFocusManager.current
+ val pagerState = rememberPagerState { BoardSetupStep.entries.size }
+
+ val startDate by viewModel.startDate.collectAsStateWithLifecycle()
+ val endDate by viewModel.endDate.collectAsStateWithLifecycle()
+ val selectedDays by viewModel.selectedDays.collectAsStateWithLifecycle()
+ val selectedVerificationTimeType by viewModel.selectedVerificationTimeType.collectAsStateWithLifecycle()
+
+ val enabledDaysOfWeek by viewModel.enabledDaysOfWeek.collectAsStateWithLifecycle()
+ val enabledButton by viewModel.enabledButton.collectAsStateWithLifecycle()
+ val currentStep by viewModel.currentStep.collectAsStateWithLifecycle()
+
+ var isShownStartDateDialog by remember { mutableStateOf(false) }
+ var isShownEndDateDialog by remember { mutableStateOf(false) }
+
+ if (isShownStartDateDialog) {
+ DatePickerDialog(
+ selectedDate = startDate,
+ onSuccess = {
+ viewModel.updateStartDate(it)
+ isShownStartDateDialog = !isShownStartDateDialog
+ },
+ selectableStartDate = LocalDate.now().plusDays(1),
+ selectableEndDate = null,
+ onDismiss = { isShownStartDateDialog = !isShownStartDateDialog }
+ )
+ }
+
+ if (isShownEndDateDialog) {
+ DatePickerDialog(
+ selectedDate = endDate,
+ onSuccess = {
+ viewModel.updateEndDate(it)
+ isShownEndDateDialog = !isShownEndDateDialog
+ },
+ selectableStartDate = startDate,
+ selectableEndDate = startDate?.plusDays(29),
+ onDismiss = { isShownEndDateDialog = !isShownEndDateDialog }
+ )
+ }
+
+ LaunchedEffect(currentStep) {
+ if(currentStep != BoardSetupStep.MISSION){
+ localFocusManager.clearFocus()
+ keyboardController?.hide()
+ }
+ if(currentStep.ordinal in 0 until pagerState.pageCount)
+ pagerState.animateScrollToPage(currentStep.ordinal)
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ viewModel.setupSuccessEvent.collect {
+ onSuccess()
+ }
+ }
+
+ BoardSetupScreen(
+ currentStep = currentStep,
+ missionTitle = viewModel.missionTitle,
+ startDate = startDate?.let {
+ dateToString(it)
+ } ?: "",
+ endDate = endDate?.let {
+ dateToString(it)
+ } ?: "",
+ selectedDays = selectedDays,
+ count = filterDatesByDayOfWeek(startDate, endDate, selectedDays),
+ selectedVerificationTimeType = selectedVerificationTimeType,
+ enabledDaysOfWeek = enabledDaysOfWeek,
+ enabledButton = enabledButton,
+ pagerState = pagerState,
+ onClickNextStep = viewModel::updateCurrentStepToNext,
+ onMissionTitleChange = viewModel::updateMissionTitle,
+ onClickStartDate = {
+ isShownStartDateDialog = true
+ },
+ onClickEndDate = {
+ isShownEndDateDialog = true
+ },
+ onClickDayOfWeek = viewModel::updateSelectedDays,
+ onClickVerificationTimeType = viewModel::updateSelectedVerificationTimeType,
+ onBackClick = {
+ when(currentStep){
+ BoardSetupStep.MISSION -> {
+ onBackClick()
+ }
+ else -> {
+ viewModel.updateCurrentStepToBack()
+ }
+ }
+ }
+ )
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BoardSetupScreen(
+ currentStep: BoardSetupStep,
+ missionTitle: String,
+ startDate: String,
+ endDate: String,
+ selectedDays : List,
+ count : Int,
+ selectedVerificationTimeType: VerificationTimeType?,
+ enabledDaysOfWeek : Set,
+ enabledButton: Boolean,
+ pagerState : PagerState,
+ onClickNextStep: () -> Unit,
+ onMissionTitleChange: (String) -> Unit,
+ onClickStartDate: () -> Unit,
+ onClickEndDate: () -> Unit,
+ onClickDayOfWeek : (DayOfWeek) -> Unit,
+ onClickVerificationTimeType : (VerificationTimeType) -> Unit,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(ColorWhite_FFFFFFFF)
+ .statusBarsPadding()
+ .imePadding()
+ ) {
+ BoardSetupNavigationBar(
+ onBackClick = onBackClick,
+ currentStep = {
+ currentStep.ordinal + 1
+ }
+ )
+
+ HorizontalPager(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ state = pagerState,
+ userScrollEnabled = false
+ ) {
+ when (it) {
+ 0 -> {
+ BoardSetupMission(
+ missionTitle = missionTitle,
+ onTitleChange = onMissionTitleChange,
+ )
+ }
+
+ 1 -> {
+ BoardSetupSchedule(
+ startDate = startDate,
+ endDate = endDate,
+ count = "$count".padStart(2,'0'),
+ enabledDaysOfWeek = enabledDaysOfWeek,
+ onClickStartDate = onClickStartDate,
+ onClickEndDate = onClickEndDate,
+ selectedDays = selectedDays,
+ onSelectDay = onClickDayOfWeek
+ )
+ }
+
+ 2 -> {
+ BoardSetupVerificationTime(
+ selectedTimeType = selectedVerificationTimeType,
+ onClickTime = onClickVerificationTimeType
+ )
+ }
+ }
+ }
+ MissionMateTextButton(
+ modifier = Modifier
+ .padding(vertical = 36.dp, horizontal = 24.dp)
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ buttonType = if (enabledButton) MissionMateButtonType.ACTIVE else MissionMateButtonType.DISABLED,
+ textId = if (currentStep.ordinal == 2) R.string.done else R.string.next,
+ onClick = {
+ onClickNextStep()
+ }
+ )
+ }
+}
+
+@Composable
+fun ColumnScope.BoardSetupDescription(
+ text: String,
+ colorTargetTexts: List
+) {
+ Text(
+ modifier = Modifier.padding(top = 6.dp, bottom = 60.dp),
+ text = styledTextWithHighlights(
+ text = text,
+ colorTargetTexts = colorTargetTexts,
+ textColor = ColorGray2_FF4F505C,
+ targetTextColor = ColorOrange_FFFF5732,
+ targetFontWeight = FontWeight.Bold,
+ ),
+ style = MissionMateTypography.title_xl_regular
+ )
+}
+
+@Composable
+fun ColumnScope.BoardSetupDescription(
+ text: String,
+ count : String,
+ colorTargetTexts: List
+) {
+ Text(
+ modifier = Modifier.padding(top = 6.dp, bottom = 60.dp),
+ text = styledTextWithHighlights(
+ text = text,
+ colorTargetTexts = colorTargetTexts,
+ weightTargetTexts = listOf(count),
+ underlineTargetTexts = listOf(count),
+ textColor = ColorGray2_FF4F505C,
+ targetTextColor = ColorOrange_FFFF5732,
+ targetFontWeight = FontWeight.Bold,
+ ),
+ style = MissionMateTypography.title_xl_regular
+ )
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt
new file mode 100644
index 00000000..a7441d76
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupSuccessScreen.kt
@@ -0,0 +1,79 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+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.ColorGray3_FF727484
+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.StableImage
+
+@Composable
+fun BoardSetupSuccessScreen(
+ onClickStart: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(ColorWhite_FFFFFFFF)
+ .padding(horizontal = 14.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ modifier = Modifier.statusBarsPadding().padding(top = 60.dp),
+ text = stringResource(id = R.string.onboarding_board_setup_success_title),
+ style = MissionMateTypography.heading_sm_bold,
+ color = ColorGray2_FF4F505C
+ )
+
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_success_description),
+ style = MissionMateTypography.title_xl_regular,
+ color = ColorGray3_FF727484,
+ textAlign = TextAlign.Center
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ contentAlignment = Alignment.Center
+ ) {
+ StableImage(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ drawableResId = com.goalpanzi.mission_mate.core.designsystem.R.drawable.image_jeju_success,
+ contentScale = ContentScale.FillWidth
+ )
+ }
+ MissionMateTextButton(
+ modifier = Modifier
+ .padding(vertical = 36.dp, horizontal = 10.dp)
+ .fillMaxWidth()
+ .wrapContentHeight(),
+ buttonType = MissionMateButtonType.ACTIVE,
+ textId = R.string.start,
+ onClick = onClickStart
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt
new file mode 100644
index 00000000..179eb9bf
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupVerificationTime.kt
@@ -0,0 +1,108 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+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.style.TextAlign
+import androidx.compose.ui.unit.dp
+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.ColorGray5_FFF5F6F9
+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.model.VerificationTimeType
+
+@Composable
+fun BoardSetupVerificationTime(
+ selectedTimeType : VerificationTimeType?,
+ onClickTime : (VerificationTimeType) -> Unit,
+ modifier : Modifier = Modifier
+){
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp)
+ ) {
+ BoardSetupDescription(
+ text = stringResource(id = R.string.onboarding_board_setup_verification_time_description),
+ colorTargetTexts = listOf(
+ stringResource(R.string.onboarding_board_setup_verification_time_color_target),
+ )
+ )
+ VerificationTime(
+ modifier = Modifier.fillMaxWidth(),
+ selectedTime = selectedTimeType,
+ onClick = onClickTime
+ )
+ }
+}
+
+
+
+@Composable
+fun VerificationTime(
+ modifier: Modifier = Modifier,
+ selectedTime: VerificationTimeType?,
+ onClick: (VerificationTimeType) -> Unit
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Text(
+ text = stringResource(id = R.string.onboarding_board_setup_verification_time_input_title),
+ style = MissionMateTypography.body_md_bold,
+ color = ColorGray3_FF727484
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ VerificationTimeType.entries.forEach {
+ Button(
+ modifier = Modifier
+ .height(84.dp)
+ .weight(1f),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = if (selectedTime == it) ColorGray1_FF404249
+ else ColorGray5_FFF5F6F9
+ ),
+ onClick = {
+ onClick(it)
+ },
+ contentPadding = PaddingValues()
+ ) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = stringResource(id = it.titleId),
+ color = if (selectedTime != it) ColorGray3_FF727484
+ else ColorWhite_FFFFFFFF,
+ textAlign = TextAlign.Center,
+ style = MissionMateTypography.body_lg_regular
+ )
+
+ }
+ }
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt
new file mode 100644
index 00000000..da343c6b
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/screen/boardsetup/BoardSetupViewModel.kt
@@ -0,0 +1,178 @@
+package com.goalpanzi.mission_mate.feature.onboarding.screen.boardsetup
+
+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.VerificationTimeType
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.isDifferenceTargetDaysOrMore
+import com.goalpanzi.mission_mate.feature.onboarding.util.DateUtils.longToLocalDate
+import dagger.hilt.android.lifecycle.HiltViewModel
+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.stateIn
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import java.time.DayOfWeek
+import java.time.LocalDate
+import javax.inject.Inject
+
+@HiltViewModel
+class BoardSetupViewModel @Inject constructor(
+
+) : ViewModel() {
+
+ private val _setupSuccessEvent = MutableSharedFlow()
+ val setupSuccessEvent: SharedFlow = _setupSuccessEvent.asSharedFlow()
+
+ private val _currentStep = MutableStateFlow(BoardSetupStep.MISSION)
+ val currentStep: StateFlow = _currentStep.asStateFlow()
+
+ var missionTitle by mutableStateOf("")
+ private set
+
+ private val _startDate = MutableStateFlow(null)
+ val startDate: StateFlow = _startDate.asStateFlow()
+
+ private val _endDate = MutableStateFlow(null)
+ val endDate: StateFlow = _endDate.asStateFlow()
+
+ private val _selectedDays = MutableStateFlow>(emptyList())
+ val selectedDays: StateFlow> = _selectedDays.asStateFlow()
+
+ private val _selectedVerificationTimeType = MutableStateFlow(null)
+ val selectedVerificationTimeType: StateFlow =
+ _selectedVerificationTimeType.asStateFlow()
+
+ val enabledDaysOfWeek: StateFlow> =
+ combine(
+ startDate,
+ endDate
+ ) { startDate, endDate ->
+ if (startDate == null || endDate == null) {
+ emptySet()
+ } else if (isDifferenceTargetDaysOrMore(startDate,endDate)) {
+ DayOfWeek.entries.toSet()
+ } else {
+ getUniqueDaysOfWeekInRange(startDate,endDate)
+ }
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = emptySet()
+ )
+
+ val enabledButton: StateFlow =
+ combine(
+ currentStep,
+ snapshotFlow { missionTitle },
+ enabledDaysOfWeek,
+ selectedDays,
+ selectedVerificationTimeType
+ ) { step, title, enabledDaysOfWeek, selectedDays, selectedVerificationTimeType ->
+ when (step) {
+ BoardSetupStep.MISSION -> {
+ title.length in MISSION_TITLE_MIN_LENGTH..MISSION_TITLE_MAX_LENGTH
+ }
+
+ BoardSetupStep.SCHEDULE -> {
+ enabledDaysOfWeek.isNotEmpty() && selectedDays.isNotEmpty()
+ }
+
+ BoardSetupStep.VERIFICATION_TIME -> {
+ selectedVerificationTimeType != null
+ }
+ }
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(500),
+ initialValue = false
+ )
+
+ fun updateCurrentStepToNext() {
+ viewModelScope.launch {
+ if (currentStep.value.ordinal in 0 until BoardSetupStep.entries.lastIndex) {
+ _currentStep.emit(
+ BoardSetupStep.entries[currentStep.value.ordinal + 1]
+ )
+ } else if (currentStep.value == BoardSetupStep.VERIFICATION_TIME) {
+ _setupSuccessEvent.emit(Unit)
+ }
+ }
+ }
+
+ fun updateCurrentStepToBack() {
+ viewModelScope.launch {
+ if (currentStep.value.ordinal in 1 .. BoardSetupStep.entries.lastIndex) {
+ _currentStep.emit(
+ BoardSetupStep.entries[currentStep.value.ordinal - 1]
+ )
+ }
+ }
+ }
+
+ fun updateMissionTitle(title: String) {
+ if (title.length <= MISSION_TITLE_MAX_LENGTH) missionTitle = title
+ }
+
+ fun updateStartDate(date: Long) {
+ viewModelScope.launch {
+ _startDate.emit(longToLocalDate(date))
+ endDate.value?.let { endDate ->
+ if(longToLocalDate(date).isAfter(endDate)) _endDate.emit(null)
+ }
+
+ }
+ }
+
+ fun updateEndDate(date: Long) {
+ viewModelScope.launch {
+ _endDate.emit(longToLocalDate(date))
+ }
+ }
+
+ fun updateSelectedDays(targetDay: DayOfWeek) {
+ viewModelScope.launch {
+ _selectedDays.update { days ->
+ if (days.contains(targetDay)) {
+ days.filter { it != targetDay }
+ } else days + targetDay
+ }
+ }
+ }
+
+ fun updateSelectedVerificationTimeType(timeType: VerificationTimeType) {
+ viewModelScope.launch {
+ _selectedVerificationTimeType.emit(timeType)
+ }
+ }
+
+ private fun getUniqueDaysOfWeekInRange(
+ startDate : LocalDate,
+ endDate: LocalDate
+ ) : Set {
+ return generateSequence(startDate) { date ->
+ if (date.isBefore(endDate)) date.plusDays(1) else null
+ }.map { it.dayOfWeek }
+ .toSet()
+ }
+
+ companion object {
+
+ const val MISSION_TITLE_MIN_LENGTH = 4
+ const val MISSION_TITLE_MAX_LENGTH = 12
+
+
+ enum class BoardSetupStep {
+ MISSION, SCHEDULE, VERIFICATION_TIME
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt
new file mode 100644
index 00000000..e07b230a
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DateUtils.kt
@@ -0,0 +1,55 @@
+package com.goalpanzi.mission_mate.feature.onboarding.util
+
+import java.time.DayOfWeek
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
+import java.util.Locale
+import kotlin.math.absoluteValue
+
+object DateUtils {
+ private fun convertMillisToLocalDateWithFormatter(date: LocalDate, dateTimeFormatter: DateTimeFormatter) : LocalDate {
+ val dateInMillis = LocalDate.parse(date.format(dateTimeFormatter), dateTimeFormatter)
+ .atStartOfDay(ZoneId.systemDefault())
+ .toInstant()
+ .toEpochMilli()
+
+ return Instant
+ .ofEpochMilli(dateInMillis)
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate()
+ }
+ fun dateToString(date: LocalDate): String {
+ val dateFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd", Locale.getDefault())
+ val dateInMillis = convertMillisToLocalDateWithFormatter(date, dateFormatter)
+ return dateFormatter.format(dateInMillis)
+ }
+
+ fun longToLocalDate(milliseconds: Long): LocalDate {
+ val instant = Instant.ofEpochMilli(milliseconds)
+
+ return instant.atZone(ZoneId.systemDefault()).toLocalDate()
+ }
+
+ fun localDateToMillis(localDate: LocalDate?): Long? {
+ val instant = localDate?.atStartOfDay(ZoneId.of("UTC"))?.toInstant() ?: return null
+
+ return instant.toEpochMilli()
+ }
+
+ fun filterDatesByDayOfWeek(startDate: LocalDate?, endDate: LocalDate?, days: List): Int {
+ if(startDate == null || endDate == null) return 0
+ return generateSequence(startDate) { date ->
+ if (date.isBefore(endDate)) date.plusDays(1) else null
+ }.filter { it.dayOfWeek in days }.toList().size
+ }
+
+ fun isDifferenceTargetDaysOrMore(
+ startDate: LocalDate,
+ endDate: LocalDate,
+ targetDifferenceDays : Int = 7
+ ) = ChronoUnit.DAYS.between(startDate, endDate).absoluteValue >= targetDifferenceDays
+
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt
new file mode 100644
index 00000000..046f5211
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/DayOfWeekExt.kt
@@ -0,0 +1,16 @@
+package com.goalpanzi.mission_mate.feature.onboarding.util
+
+import com.goalpanzi.mission_mate.core.designsystem.R
+import java.time.DayOfWeek
+
+fun DayOfWeek.getStringId() : Int {
+ return when(this){
+ DayOfWeek.MONDAY -> R.string.monday_short
+ DayOfWeek.TUESDAY -> R.string.tuesday_short
+ DayOfWeek.WEDNESDAY -> R.string.wednesday_short
+ DayOfWeek.THURSDAY -> R.string.thursday_short
+ DayOfWeek.FRIDAY -> R.string.friday_short
+ DayOfWeek.SATURDAY -> R.string.saturday_short
+ DayOfWeek.SUNDAY -> R.string.sunday_short
+ }
+}
\ No newline at end of file
diff --git a/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt
new file mode 100644
index 00000000..9edcb56b
--- /dev/null
+++ b/feature/onboarding/src/main/java/com/goalpanzi/mission_mate/feature/onboarding/util/StringUtils.kt
@@ -0,0 +1,75 @@
+package com.goalpanzi.mission_mate.feature.onboarding.util
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+
+fun List.getStringRanges(text: String): List {
+ return this.mapNotNull { target ->
+ val index = text.indexOf(target, 0)
+ if (index != -1) {
+ (index until index + target.length)
+ } else {
+ null
+ }
+ }
+}
+
+fun styledTextWithHighlights(
+ text: String,
+ colorTargetTexts: List,
+ textColor: Color,
+ targetTextColor: Color,
+ weightTargetTexts: List = emptyList(),
+ underlineTargetTexts: List = emptyList(),
+ targetFontWeight: FontWeight = FontWeight.Normal
+): AnnotatedString {
+ return styledTextWithHighlightsWithIndices(
+ text = text,
+ colorTargetTextIndices = colorTargetTexts.getStringRanges(text),
+ weightTargetTextIndices = weightTargetTexts.getStringRanges(text),
+ underlineTargetTextIndices = underlineTargetTexts.getStringRanges(text),
+ textColor = textColor,
+ targetTextColor = targetTextColor,
+ targetFontWeight = targetFontWeight
+ )
+}
+
+fun styledTextWithHighlightsWithIndices(
+ text: String,
+ colorTargetTextIndices: List,
+ weightTargetTextIndices: List,
+ underlineTargetTextIndices: List,
+ textColor: Color,
+ targetTextColor: Color,
+ targetFontWeight: FontWeight = FontWeight.Normal,
+): AnnotatedString {
+ return buildAnnotatedString {
+ addStyle(style = SpanStyle(color = textColor), start = 0, end = text.length)
+ append(text)
+ colorTargetTextIndices.forEach { range ->
+ addStyle(
+ style = SpanStyle(color = targetTextColor),
+ start = range.first,
+ end = range.last + 1
+ )
+ }
+ weightTargetTextIndices.forEach { range ->
+ addStyle(
+ style = SpanStyle(fontWeight = targetFontWeight),
+ start = range.first,
+ end = range.last + 1
+ )
+ }
+ underlineTargetTextIndices.forEach { range ->
+ addStyle(
+ style = SpanStyle(textDecoration = TextDecoration.Underline),
+ start = range.first,
+ end = range.last + 1
+ )
+ }
+ }
+}
\ 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 0392472e..e43246be 100644
--- a/feature/onboarding/src/main/res/values/strings.xml
+++ b/feature/onboarding/src/main/res/values/strings.xml
@@ -7,4 +7,40 @@
초대받고 왔지~
LV1. 제주도
+
+ 미션 설정
+ 경쟁인원은\n최소 2명에서 최대 10명입니다!
+ 최소 2명
+ 최대 10명
+ 미션
+ ex) 주3회 러닝하기 / 매일 책3장씩 읽기
+ 4~12자 이내로 입력하세요.
+
+ 기간 및 요일 설정
+ 경쟁 기간내 인증 요일로 계산한\n총 인증 횟수는 %s번 입니다!
+ 경쟁 기간
+ 인증 요일
+ 번
+ 미션 기간
+ 시작일
+ 마감일
+ 내일부터 시작일로 지정할 수 있어요.
+ 인증 요일 (다중선택)
+ 선택한 요일에만 미션 인증할 수 있어요. (ex.월,수,금)
+
+ 인증 시간 설정
+ 해당 시간에만 인증 가능해요!\n신중히 선택해주세요.
+ 해당 시간에만 인증 가능
+ 인증 시간
+ 오전\n00~12시
+ 오후\n12~00시
+ 종일\n00~00시
+
+ 목표가 완성되었어요!
+ 꾸준히 미션를 완수해\n세계 곳곳을 경험해봐요!
+
+ 다음
+ 완성
+ 시작하기
+
\ No newline at end of file